0
点赞
收藏
分享

微信扫一扫

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


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

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

1. 实验代码

1.1 代码托管地址

git clone

如果你已经 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. 实验结果

  • 服务器


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


图1 服务器在 readline 处收到异常


  • 客户端


34-异常处理(accept 返回前连接中止)_服务器_02


图2 客户端 connect 成功后立即发送 RST


  • tcpdump


34-异常处理(accept 返回前连接中止)_客户端_03


图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

可以看到,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 的行为。


举报

相关推荐

0 条评论