38-连接断开异常(服务器进程终止)

代码托管在 gitos 上,请使用下面的命令获取:

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

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

本次实验所使用的程序路径仍然是 unp/program/echo/processzombie。processzombie 程序看起来似乎已经完美无缺了,但是做完此实验后,你仍然会发现问题。

我们的目的不是让客户端主动发起连接断开,而是由服务器主动断开。接下来再看会有什么情况发生。

1. 实验步骤及结果

  • 在 flower 主机上启动服务器
flower $ ./echo -s -h flower
  • 在 sun 主机上开启 tcpdump 准备抓包
sun $ sudo tcpdump -ttt -i ens33 tcp and host flower and port 8000
  • 在 sun 主机上再开启一个终端,启动服务器
sun $ ./echo -h flower

接下来,我们在客户端键入 'hello',得到服务器的回射。

  • 杀死服务器子进程,主动断开连接
flower $ ctrl z // 将服务器放到后台 ^Z [1]+ 已停止        ./echo -s -h flower flower $ ps // 查看服务器进程    PID TTY          TIME CMD   3082 pts/0    00:00:00 bash   3363 pts/0    00:00:00 echo   3364 pts/0    00:00:00 echo   3373 pts/0    00:00:00 ps flower $ kill -9 3364 // 杀死服务器子进程 flower $ fg // 将服务器切换到前台

具体详见图 1.

为了方便后面阅读起来更加清楚,我用 flower 指代服务器,用 sun 指代客户端,以便大家对照图。


这里写图片描述
图1 杀死 flower 子进程

当我们杀死 flower 服务器的子进程后,父进程收到子进程退出信号,并进行了回收。另一方面,我们看 tcpdump 抓取的数据包(观察最后两行)。flower 子进程退出后,给 sun 客户端发送了 FIN 报文,并正确得到了对端的 ACK. 转而进入 FIN_WAIT2 状态。

但是,我们的 sun 客户端应用层对此毫不知情,仍然阻塞在标准输入上!

  • 接下来,在 sun 客户端键入 'world' 字符,回车发送过去


这里写图片描述
图2 sun 客户端再次键入 world 发送给对方

很不幸的是,sun 客户端并没有收到对端的回射信息,而是在 readline 的时候,读取到了对端(flower)发来的 FIN 段,返回了 0,因此打印了一行 peer closed.

为了能方便分析结果,这里贴出 sun 客户端的代码

void doClient(int sockfd) {   int nr, nw;   char buf[4096];    while(fgets(buf, 4096, stdin)) {     nw = writen(sockfd, buf, strlen(buf));     if (nw < strlen(buf)) puts("short write");      nr = readline(sockfd, buf, 4096);     if (nr == 0) {       puts("peer closed");       break;     }     else if (nr < 0) ERR_EXIT("readline");      write(STDOUT_FILENO, buf, nr);   } } 

再观察 tcpdump 的输出:


这里写图片描述
图3 tcpdump 多了 6 行输出

我们看图 3 红色框框中的第一行,这是 sun 客户端发送 'world'字符的 TCP 报文段,没有任何异样,它对应于代码

nw = writen(sockfd, buf, strlen(buf));

虽然 sun 客户端在此前已经接收到了对端的 FIN 段,但是这只表明对端(flower)不再会发送数据过来,因此 sun 客户端此时执行 writen 并不觉得自己有什么错误。可是,flower 服务器进程此时已经不在了,一旦 flower 服务器那边收到了 'world'报文段,立即回送 RST 段。

但是 sun 客户端的执行速度太快了,在还没有收到对端的 RST 段之前,它执行的 readline 函数已经返回,而且返回了 0,它得出的结论是对端 flower 已经关闭,于是打印 peer closed,然后退出循环,执行 close(sockfd),向对端发送 FIN 段(图 3 中红色框框的第 2 行),不过我们看到了 sun 主机连续发送了两个 FIN 段,第二个 FIN 是在第一次发送 FIN 后 44 ms 左右发出去的,实际上这是一次重传。

接下来,我们看到了 flower 连续回送了 3 个 RST 段,第一个 RST 段显然是对 sun 客户端 writen 的答复,也就是 'world' 报文的答复,后面两个 RST 是对 sun 连续两个 FIN 的答复。

2. 结果分析

上面的实验,展示了一次 TCP 的异常关闭过程,这是一种状态的,不优雅的关闭,虽然最后并未造成多大的影响。所谓优雅的关闭一个 TCP 连接,指的是经历一次完整的四次挥手的过程,而上面的实验,最后却以 RST 报文而告终。

优雅的关闭一个 TCP 连接就像和平分手,大家好聚好散。而异常关闭,就像双方分手还撕破脸皮,双方都没有好处。

当 flower 上的服务器被我们手工 kill 后,很绅士的发送了 FIN 段给客户端,并在接收到 ACK 后进入 FIN_WAIT2 状态。可是,服务器进程此时已经退出了,不能再接收新的数据了,它期待的是对端 sun 发送 FIN.

客户端在 writen('world') 后,并不会察觉到什么错误。flower 接收到了'world' 后,自然立即回送 RST 段,因为 flower 期待的是 FIN 而不是一个普通的数据报文。

接下来 sun 执行 readline,这个时候,就看服务器回送 RST 什么时候到达了,如果在 readline 返回前 RST 到达了,readline 必然就会返回错误。在我们实验中,sun 的 readline 返回后,RST 还没到达。

3. 总结

  • 知道为什么实验中最后一次 flower 服务器会发送 RST

思考:有没有办法让客户端在收到 FIN 端后立即能够得到通知?(提示:用我们学过的知识,比如多线程,IO 复用)

本文我们先不给出具体的解决方案,因为后面还有一个实验。