本文解决上一篇文章遇见的 bug.
1. 程序路径
代码托管在 gitos 上,请使用下面的命令获取:
git clone https://git.oschina.net/ivan_allen/unp.git
如果你已经 clone 过这个代码了,请使用 git pull 更新一下。本节程序所使用的程序路径是 unp/program/echo/multiplexing_select_client_shutdown
。
2. shutdown 函数
终止网络连接一般使用 close,不过 close 有两个限制:
- close 把描述符引用计数减 1,只有该计数为 0 时才关闭套接字。而 shutdown 不管引用计数,只要调用,直接发送 FIN 报文。
- close 终止读和写两个方向的数据传送。
2.1 shutdown 函数原型
#include <sys/socket.h> int shutdown(int sockfd, int howto);
2.2 函数参数
该函数的行为依赖于 howto 的值:
- SHUT_RD: 关闭连接读这一半,而且接收缓冲区中现有数据全部丢弃。进程不能再对这样的套接字调用任何读函数。当然,对端对此毫不知情,任仍可以发送数据过来,关闭读半部的 TCP 收到对端发送的数据半进行确认后,再丢弃掉,而不是放入接收缓冲区。使用该选项,并不会引起任何 TCP 报文的传输。
- SHUT_WR: 关闭连接写这一半。这就是半关闭。该行为首先将发送缓冲区中所有数据发送出去,最后引发 TCP 向对端发送 FIN 报文段。进程不能再对这样的套接字执行任何写操作。
- SHUT_RDWR: 连接的读写全部关闭,这相当于调用
shutdown(sockfd, SHUT_RD); shutdown(sockfd, SHUT_WR)
.
3. 客户端修改
根据 shutdown 函数的特性,显然我们应该使用 SHUT_WR 这个选项来执行半关闭操作。
void doClient(int sockfd) { fd_set rfds, fds; int nr, nw, nready, maxfd, stdinclosed; char buf[4096]; // 客户端关闭标志 stdinclosed = 0; FD_ZERO(&rfds); FD_SET(STDIN_FILENO, &rfds); FD_SET(sockfd, &rfds); maxfd = sockfd; while(1) { fds = rfds; nready = select(maxfd + 1, &fds, NULL, NULL, NULL); if (nready < 0) { if (nready == EINTR || nready == ECONNRESET) continue; } else if (nready == 0) continue; if (FD_ISSET(STDIN_FILENO, &fds)) { if (fgets(buf, 4096, stdin) != NULL) { nw = writen(sockfd, buf, strlen(buf)); if (nw < strlen(buf)) { perror("short write"); } } else { // 不直接 break,而是先执行半关闭操作,然后继续执行循环 shutdown(sockfd, SHUT_WR); FD_CLR(STDIN_FILENO, &rfds); stdinclosed = 1; } } if (FD_ISSET(sockfd, &fds)) { nr = readline(sockfd, buf, 4096); if (nr == 0) { if (stdinclosed) { fputs("peer closed\n", stderr); } else { // 服务器先执行关闭 fputs("server exception!\n", stderr); } break; } else if (nr < 0) ERR_EXIT("readline"); write(STDOUT_FILENO, buf, nr); } } }
4. 实验过程
- flower 上启动服务器
flower $ ./echo -s -h flower
- sun 上启动客户端
sun $ ./echo -h flower <input >output && ll
5. 结果及分析
图1 运行结果
图 1 中左上角是客户端,我们可以看到客户端输出的 output 文件已经正常了,它的大小和 input 一样大。
再看 tcpdump 的输出,第一个红色框框是 sun 客户端执行 shutdown 是发送的 FIN 段,不过发现这个 FIN 是随着数据一起捎带过去的。第二个红色框框是 flower 对该 FIN 段的确认,为什么该确认不是 ack = 28785,要知道,FIN 段本身也会消耗一个字节号,所以 ack = 29786 了。
最后一个红色框框,是服务器在 readline 的时候,读取到了 EOF,也就是返回了 0,因此它就知道对端已经没有数据要发送了,然后也执行了 close。
接下来再看看客户端与服务器通信的时序图(简化),如图 2.
图2 使用 shutdown 执行半关闭
当客户端关闭写通道时,服务器仍然在向客户端发送数据,而客户端也要做好接收对端数据的准备,不能随意就退出程序。
6. 总结
- 掌握 shutdown 函数的用法