44-批量输入异常

1. 程序路径

代码托管在 gitos 上,请使用下面的命令获取:

git clone https://git.oschina.net/ivan_allen/unp.git

如果你已经 clone 过这个代码了,请使用 git pull 更新一下。本节程序所使用的程序路径是unp/program/echo/multiplexing_select_client

2. 使用重定向完成批量输入

上一节,我们使用了 select 修改过的程序看起来似乎没有什么异常,然而,我们按下了葫芦却起了瓢,更大的陷阱却等在我们后头。

bug 复现的方法,在文件夹 multiplexing_select_client 下面,有一个输入文件 input,一共有 28784 个字节。当我们启动客户端的时候,将客户端的标准输入重定向到此文件中,另一方面,我们将客户端的标准输出重定向到 output 文件。

这样做的目的是希望将 input 文件中所有数据发送给服务器,然后再从服务器接收回射数据,重定向到 output 文件中,这样得到的 output 文件就是全部经过大写转换后的数据,最重要的一点是,这个 output 文件的大小和 input 应当是一致的,也是 28784 字节。但是实际的结果却并非如此。

图 1 形象的说明了我们希望做出的功能。


这里写图片描述
图1 批量输入

3. 实验步骤

  • 在 flower 上启动服务器
flower $ ./echo -s -h flower
  • 在 sun 上启动客户端,将标准输入和标准输出分别重定向到文件 input 和 output.
// 执行完客户端后,立即执行 ll 命令 sun $ ./echo -h flower <input >output && ll

4. 实验结果

我连续执行了四次客户端程序,结果如图 2。


这里写图片描述
图2 运行了四次客户端

四次运行的结果 output 文件的大小竟然不一样!!!第一次是 20 字节,第二次是 0 字节,第三次却是 7489 字节,第 4 字又是 0 字节!而且这四次的运行结果,距离我们想要的 28784 字节差的太远了!!!

再看看服务器的情况,如图 3.


这里写图片描述
图3 服务器运行情况

服务器有两次报 short write 异常(即服务器实际发送的数据量少于期望写入的数据量),三次捕获到了 SIGPIPE 信号,还有一次 readline 返回错误。全特么报的异常!!!

再看看抓包情况,如图 4.


这里写图片描述
图 4 tcpdump 部分输出

天哪!!!这可是重大 bug,我们的服务器被越改越挫,这可能还不如之前不使用 IO 复用的情况!!!(尽管那样,也可能会出现这种错误)。

5. 结果分析

对于这样的结果,先不要慌,bug 虽大,但是原因很简单。我们回忆一下,上一篇文章改进的客户段伪代码:

void doClient(int sockfd) {   fds.add(STDIN_FILENO);   fds.add(sockfd);   maxfd = max(STDIN_FILENO, sockfd);    while(1) {     rfds = fds;     // 只监听了标准输入和套接字 sockfd     nready = select(maxfd + 1, &rfds, NULL, NULL, NULL);     if (STDIN_FILENO in rfds) {       // 问题出在这里       if (fgets(buf, stdin) != NULL) {         write(sockfd, buf);       }       else {         break;       }     }      if (sockfd in rfds) {       if (read(sockfd, buf) == 0) {         puts("peer closed");         break;       }       puts(buf);     }   } }

当我们手工从键盘输入数据到客户端,显然不会有太大问题,问题就出在将标准输入重定向到了 input 文件。

要知道,cpu 的执行的速度是相当快的,可能在一瞬间,就把 input 文件中所有数据 fgets 完并发送给对端,甚至在对方还没来得及收到数据的情况下,客户端已经 close 退出了。

经过一段时间后。服务器收到了客户端发来的数据,然后处理完成(转换为大写),接着就立即发送给客户端,不幸的是服务器此时并不知道客户端进程已经关闭了套接字退出了……于是引发了“血案”。

6. 如何改正错误

我们希望,客户端在发送完数据后,并不立即 close 退出,而是等服务器处理完数据后,并接收完服务器回射的所有数据后再退出。

比如代码可以改成下面这样?

void doClient(int sockfd) {   fds.add(STDIN_FILENO);   fds.add(sockfd);   maxfd = max(STDIN_FILENO, sockfd);    // 添加一个 isClosed 标记   int isClosed = 0;   while(1) {     rfds = fds;     // 只监听了标准输入和套接字 sockfd     nready = select(maxfd + 1, &rfds, NULL, NULL, NULL);     if (STDIN_FILENO in rfds) {       // 问题出在这里       if (fgets(buf, stdin) != NULL) {         write(sockfd, buf);       }       else {         // 先不退出循环         isClosed = 1;       }     }      if (sockfd in rfds) {       if (read(sockfd, buf) == 0) {         puts("peer closed");         break;       }       puts(buf);        // 这里实现起来太麻烦       if (数据读取完成) {          // 退出循环并关闭套接字          break;       }     }   } }

这种想法,确实没有错,但是这太麻烦了,我们几乎无法判断数据有没有读取完成。

另一方面如果你不告诉服务器你已经没有要数据发送了(除非你关闭连接),服务器永远也不知道它该不该关闭连接,这就尴尬了!!!似乎陷入了自相矛盾。

别忘记了,TCP 断开连接的 4 次挥手是怎样的。即使其中一方发送了 FIN 段关闭了连接,另一方也可以继续传送数据,这就是所谓的半关闭。如果你忘记了这个知识点,请再复习一下前面的知识——《TCP 协议(断开连接)》.

然而 close 函数并不能实现半关闭,一旦 close,两个方向的数据传送通道全部被关闭,所以,我们只能使用 shutdown 函数。下一讲,我们使用 shutdown 函数修复这个程序的 bug.

7. 总结

  • 为什么批量输入会出现 bug
  • 回忆 tcp 的半关闭特性

说明:本文转自blog.csdn.net,用于学习交流分享,仅代表原文作者观点。如有疑问,请联系我们删除~