45-批量输入异常处理(shutdown 函数)

本文解决上一篇文章遇见的 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 (errno == EINTR || errno == 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 函数的用法
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值