1、网络中断造成的对端无 FIN 包
很多原因都会造成网络中断,在这种情况下,TCP 程序并不能及时感知到异常信息。除非网络中的其他设备,如路由器发出一条 ICMP 报文,说明目的网络或主机不可达,这个时候通过 read 或 write 调用就会返回 Unreachable 的错误。
可惜大多数时候并不是如此,在没有 ICMP 报文的情况下,TCP 程序并不能理解感应到连接异常。如果程序是阻塞在 read 调用上,那么很不幸,程序无法从异常中恢复。这显然是非常不合理的,不过,我们可以通过给 read 操作设置超时来解决。
如果程序先调用了 write 操作发送了一段数据流,接下来阻塞在 read 调用上,结果会非常不同。Linux 系统的 TCP 协议栈会不断尝试将发送缓冲区的数据发送出去,大概在重传 12 次、合计时间约为 9 分钟之后,协议栈会标识该连接异常,这时,阻塞的 read 调用会返回一条 TIMEOUT 的错误信息。如果此时程序还执着地往这条连接写数据,写操作会立即失败,返回一个 SIGPIPE 信号给应用程序。
2、系统崩溃造成的对端无 FIN 包
当系统突然崩溃,如断电时,网络连接上来不及发出任何东西。这里和通过系统调用杀死应用程序非常不同的是,没有任何 FIN 包被发送出来。
这种情况和网络中断造成的结果非常类似,在没有 ICMP 报文的情况下,TCP 程序只能通过 read 和 write 调用得到网络连接异常的信息,超时错误是一个常见的结果。
不过还有一种情况需要考虑,那就是系统在崩溃之后又重启,当重传的 TCP 分组到达重启后的系统,由于系统中没有该 TCP 分组对应的连接数据,系统会返回一个 RST 重置分节,TCP 程序通过 read 或 write 调用可以分别对 RST 进行错误处理。
如果是阻塞的 read 调用,会立即返回一个错误,错误信息为连接重置(Connection Reset)。
如果是一次 write 操作,也会立即失败,应用程序会被返回一个 SIGPIPE 信号。
3、对端有 FIN 包发出
对端如果有 FIN 包发出,可能的场景是对端调用了 close 或 shutdown 显式地关闭了连接,也可能是对端应用程序崩溃,操作系统内核代为清理所发出的。从应用程序角度上看,无法区分是哪种情形。
阻塞的 read 操作在完成正常接收的数据读取之后,FIN 包会通过返回一个 EOF 来完成通知,此时,read 调用返回值为 0。这里强调一点,收到 FIN 包之后 read 操作不会立即返回。你可以这样理解,收到 FIN 包相当于往接收缓冲区里放置了一个 EOF 符号,之前已经在接收缓冲区的有效数据不会受到影响。
服务器端和客户端程序。
//服务端程序 int main(int argc, char **argv) { int connfd; char buf[1024]; connfd = tcp_server(SERV_PORT); for (;;) { int n = read(connfd, buf, 1024); if (n < 0) { error(1, errno, "error read"); } else if (n == 0) { error(1, 0, "client closed \n"); } sleep(5); int write_nc = send(connfd, buf, n, 0); printf("send bytes: %zu \n", write_nc); if (write_nc < 0) { error(1, errno, "error write"); } } exit(0); }
服务端程序是一个简单的应答程序,在收到数据流之后回显给客户端,在此之前,休眠 5 秒,以便完成后面的实验验证。
客户端程序从标准输入读入,将读入的字符串传输给服务器端:
//客户端程序 int main(int argc, char **argv) { if (argc != 2) { error(1, 0, "usage: reliable_client01 <IPaddress>"); } int socket_fd = tcp_client(argv[1], SERV_PORT); char buf[128]; int len; int rc; while (fgets(buf, sizeof(buf), stdin) != NULL) { len = strlen(buf); rc = send(socket_fd, buf, len, 0); if (rc < 0) error(1, errno, "write failed"); rc = read(socket_fd, buf, sizeof(buf)); if (rc < 0) error(1, errno, "read failed"); else if (rc == 0) error(1, 0, "peer connection closed\n"); else fputs(buf, stdout); } exit(0); }
4、read 直接感知 FIN 包
我们依次启动服务器端和客户端程序,在客户端输入 good 字符之后,迅速结束掉服务器端程序,这里需要赶在服务器端从睡眠中苏醒之前杀死服务器程序。
屏幕上打印出:peer connection closed。客户端程序正常退出。
$./reliable_client01 127.0.0.1 $ good $ peer connection closed
这说明客户端程序通过 read 调用,感知到了服务端发送的 FIN 包,于是正常退出了客户端程序。
5、通过 write 产生 RST,read 调用感知 RST
这一次,我们仍然依次启动服务器端和客户端程序,在客户端输入 bad 字符之后,等待一段时间,直到客户端正确显示了服务端的回应“bad”字符之后,再杀死服务器程序。客户端再次输入 bad2,这时屏幕上打印出”peer connection closed“。
屏幕输出和时序图。
$./reliable_client01 127.0.0.1 $bad $bad $bad2 $peer connection closed
故障分为两大类,一类是对端无 FIN 包,需要通过巡检或超时来发现;另一类是对端有 FIN 包发出,需要通过增强 read 或 write 操作的异常处理,帮助我们发现此类异常。