网络编程的难度在于异常状况的处理。
在前面学习 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 的行为。