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 的行为。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值