34-异常处理(accept 返回前连接中止)

网络编程的难度在于异常状况的处理。

在前面学习 TCP 协议的时候,我们就分析过各种连接异常,断开异常等等,大家要把各种情况烂记于心。本文我们探讨一种比较特殊的情况,即客户端连接建立成功后(进入 ESTABLISHED 状态),立即关闭连接退出。而此时服务器中的 accept 函数还没调用或者还没有返回。

1. 实验代码

1.1 代码托管地址

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

如果你已经 clone 过这个代码了,请使用 git pull 更新一下。

1.2 程序路径

unp/program/echo/exception_accept

1.3 代码说明

这一份程序主要基于 ehch/basic 进行了少量的修改,大家在阅读后面解释的时候,记得对照着源代码看。修改内容主要有以下几个地方:

  • 添加了一个命令行参数选项 -r

如果指定该选项,表示客户端以异常方式关闭连接,关闭时直接发送 RST 段给对方;服务器会在 accept 前等待 10 秒。'-r' 选项对应程序中 Options 的 isLinger 字段。

  • accept 函数调用处修改为:
// 模拟异常,在 accept 前收到 RST 报文 if (g_option.isLinger) sleep(10);  cliaddrlen = sizeof cliaddr; sockfd = accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddrlen); if (sockfd < 0) {   // 添加了一行错误处理,如果在 accept 前收到 RST,可能会出现此错误,这依赖于操作系统实现。   if (errno == ECONNABORTED) puts("accept: connect reset by peer");   ERR_EXIT("accept"); }
  • 服务器 doServer 少量修改
else if (nr < 0) {   // 如果 readline 返回小于 0,判断是否是因为收到 RST   if (errno == ECONNRESET) {     puts("readline: reset by peer");     break;   }   ERR_EXIT("readline"); }
  • 客户端修改为以下代码
void client_routine() {   // 省略无关内容    // 创建 linger 对象   struct linger lgr;    sockfd = socket(AF_INET, SOCK_STREAM, 0);   if (sockfd < 0) ERR_EXIT("socket");    // 如果命令行指定了 '-r' 选项,就为套接字打开 SO_LINGER 选项   // 这个知识点由于还没讲到,所以大家现在就认为只要执行了下面这段程序,客户端在 close 的时候不是发送 FIN,而是 RST.   if (g_option.isLinger) {     lgr.l_onoff = 1;     lgr.l_linger = 0;     ret = setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lgr, sizeof lgr);     if (ret < 0) ERR_EXIT("set linger");   }    ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof servaddr);   if (ret < 0) ERR_EXIT("connect");    // 如果命令行指定了 '-r' 选项,测试连接成功后立即发送 RST   if (g_option.isLinger) {     puts("connect successful, now exiting...");     close(sockfd);     return;   }    // ... }

2. 实验步骤

  • 在 sun 主机上,打开 Linux 的抓包工具 tcpdump,这个工具之前没有教大家使用过,因为之前我们一直用的 OmniPeek,它比较适合初学者。现在我们已经算是半个入门者了,在 Linux 下使用 tcpdump 压力不大。
/*  * sudo tcpdump, 表示需要 root 权限运行  * (tcp) [-ttt] [-i ens33] and (host sun) and (port 8000)  * and 表示使用与的方式进行过滤  * tcp 表示只抓取 tcp 包  * [-ttt] [-i ens33] 是可选项,意思就是可以不写  * -i ens33 表示使用 ens33 这个网卡。如何查看你的网卡名称?使用 ifconfig 命令就能看见。  * host sun 表示只抓取 sun 这台机器上的数据包,你也可以使用 ip 地址而不是主机名  * port 8000 表示只抓取目的端口或源端口 8000 上的数据包  */  $ sudo tcpdump tcp -ttt -i ens33 and host sun and port 8000
  • 在 sun 主机上打开服务器,记得打开 '-r' 选项
$ ./echo -s -h sun -r
  • 在 flower 主机上打开客户端,记得打开 '-r' 选项
$ ./echo -h sun -r

3. 实验结果

  • 服务器


这里写图片描述
图1 服务器在 readline 处收到异常

  • 客户端


这里写图片描述
图2 客户端 connect 成功后立即发送 RST

  • tcpdump


这里写图片描述
图3 sun 主机(服务器端)上抓取的数据

  • 结果分析

我使用的 Linux 内核版本是 3.10,CentOS 7。很遗憾的是,并没有在 accept 函数处捕捉到异常。反而程序在第一次 readline 的时候,返回了错误。

先来看看图 3,我简化一下:

// 左侧时间表示时间差,距离上一个报文多久后发送的  (1) 0     ms: flower ->    sun: [S], seq (2) 0.127 ms: sun    -> flower: [S], seq, ack (3) 11.55 ms: flower ->    sun: [.], ack (4) 0.065 ms: flower ->    sun: [R], seq

可以看到,flower 主机(客户端)在三次握手完成后,立即发送了一个 RST 段(4 号数据包)。而此时,服务器还未执行 accept(正处于 10 s 等待中),sleep 返回后,立即 accept,不幸的是,accept 成功返回了!!!服务器并没有因为收到了 RST 段让 accept 报错。

实际上,这是由操作系统实现来决定的,有些操作系统可能在 accept 时,悄无声息的把这个连接给 kill 掉,有些可能会让 accept 返回 ECONNABORTED。man 手册上给出的解释是:不同的 Linux 内核也可能会返回 ECONNABORTED。

不过,服务器在接收到 RST 后,在 readline 时做出了响应,它返回了一个 ECONNRESET 错误。

4. 总结

  • 掌握服务器接收到 RST 时,accept 和 read 的行为。
说明:本文转自--Allen--,用于学习交流分享,仅代表原文作者观点。如有侵权,请联系我们删除~