网络编程:TCP故障模式
故障模式总结
异常情况可归结为两大类:
第一类,是对端无FIN包发送出来的情况;第二类是对端有FIN包发出来
对端无FIN包发送出
- 网络终端造成对端无FIN包
很多原因都会造成网络中断,这种情况,TCP程序并不能及时感知异常信息。除非网络中的其他设备,如路由器发送出一条ICMP报文,说明目的网络或主机不可达,此时,通过read或write调用就会返回unreachable的错误
在没有ICMP报文的情况下,TCP程序并不能感应到连接异常。如果程序是阻塞在read调用上,程序无法从异常中恢复,可以通过read操作设置超时来解决
如果程序先调用了write操作发送了一段数据流,接下来阻塞在read调用上,结果又是另一种情况。Linux系统的TCP协议栈会不断尝试将发送缓冲区的数据发送出去,大概在重传12次,合计时间约9分钟之后,协议栈会标识连接异常,此时,阻塞的read调用会返回一个TIMEOUT的错误信息,如果程序还继续往这条连接上写数据,写操作会立即失败,返回一个SIGPIPE信号给应用程序
- 系统崩溃造成的对端无FIN包
当系统突然奔溃,如断电,网络连接来不及发出任何东西,和通过应用调用杀死应用程序非常不同的是,没有任何FIN包被发送出来。
这种情况和网络中端造成的结果非常类似,在没有ICMP报文的情况下,TCP程序只能通过read和write调用的到网络连接异常的消息,超时错误是一种常见的结果。
不过还有一种情况需要考虑,那就是系统在崩溃之后又重启,当重传的 TCP 分组到达重启后的系统,由于系统中没有该 TCP 分组对应的连接数据,系统会返回一个 RST 重置分节,TCP 程序通过 read 或 write 调用可以分别对 RST 进行错误处理。如果是阻塞的 read 调用,会立即返回一个错误,错误信息为连接重置(Connection Reset)。如果是一次 write 操作,也会立即失败,应用程序会被返回一个 SIGPIPE 信号。
对端有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);
}
- read直接感知FIN包
依次启动服务器端和客户端程序,在客户端输入 good 字符之后,迅速结束掉服务器端程序,这里需要赶在服务器端从睡眠中苏醒之前杀死服务器程序。
屏幕上打印出:peer connection closed。客户端程序正常退出
$./reliable_client01 127.0.0.1
$ good
$ peer connection closed
这说明客户端程序通过 read 调用,感知到了服务端发送的 FIN 包,于是正常退出了客户端程序。
- 通过 write 产生 RST,read 调用感知 RST
们仍然依次启动服务器端和客户端程序,在客户端输入 bad 字符之后,等待一段时间,直到客户端正确显示了服务端的回应“bad”字符之后,再杀死服务器程序。客户端再次输入 bad2,这时屏幕上打印出”peer connection closed“。
$./reliable_client01 127.0.0.1
$bad
$bad
$bad2
$peer connection closed
在很多书籍和文章中,对这个程序的解读是,收到 FIN 包的客户端继续合法地向服务器端发送数据,服务器端在无法定位该 TCP 连接信息的情况下,发送了 RST 信息,当程序调用 read 操作时,内核会将 RST 错误信息通知给应用程序。这是一个典型的 write 操作造成异常,再通过 read 操作来感知异常的样例。
*向一个已关闭连接连续写,最终导致 SIGPIPE
为模拟该过程,对服务端程序和客户端程序进行了修改:
服务端:
nt main(int argc, char **argv) {
int connfd;
char buf[1024];
int time = 0;
connfd = tcp_server(SERV_PORT);
while (1) {
int n = read(connfd, buf, 1024);
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed \n");
}
time++;
fprintf(stdout, "1K read for %d \n", time);
usleep(1000);
}
exit(0);
}
服务器端每次读取 1K 数据后休眠 1 秒,以模拟处理数据的过程。
客户端:
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: reliable_client02 <IPaddress>");
}
int socket_fd = tcp_client(argv[1], SERV_PORT);
signal(SIGPIPE, SIG_IGN);
char *msg = "network programming";
ssize_t n_written;
int count = 10000000;
while (count > 0) {
n_written = send(socket_fd, msg, strlen(msg), 0);
fprintf(stdout, "send into buffer %ld \n", n_written);
if (n_written <= 0) {
error(1, errno, "send error");
return -1;
}
count--;
}
return 0;
}
果在服务端读取数据并处理过程中,突然杀死服务器进程,我们会看到客户端很快也会退出,并在屏幕上打印出“Connection reset by peer”的提示。
$./reliable_client02 127.0.0.1
$send into buffer 5917291
$send into buffer -1
$send: Connection reset by peer
这是因为服务端程序被杀死之后,操作系统内核会做一些清理的事情,为这个套接字发送一个 FIN 包,但是,客户端在收到 FIN 包之后,没有 read 操作,还是会继续往这个套接字写入数据。这是因为根据 TCP 协议,连接是双向的,收到对方的 FIN 包只意味着对方不会再发送任何消息。 在一个双方正常关闭的流程中,收到 FIN 包的一端将剩余数据发送给对面(通过一次或多次 write),然后关闭套接字。
当数据到达服务器端时,操作系统内核发现这是一个指向关闭的套接字,会再次向客户端发送一个 RST 包,对于发送端而言如果此时再执行 write 操作,立即会返回一个 RST 错误信息。
全过程的描述图:
小结
因为可能故障的存在,TCP并不是那么的“可靠”。故障分为两大类,一类是对端无 FIN 包,需要通过巡检或超时来发现;另一类是对端有 FIN 包发出,需要通过增强 read 或 write 操作的异常处理,帮助我们发现此类异常。