针对TCP连接异常断开的分析

时间:2022-10-06 17:36:08

我们知道,一个基于TCP/IP的客户端-服务器的程序中,正常情况下,我会是启动服务器使其在一个端口上监听请求,等待客户端的连接;通过TCP的三次握手,客户端能够通过socket建立一个到服务器的连接;然后,两者就可以基于这个socket连接通信了。连接结束后,客户端(进程)会退出;在不需要继续处理客户请求的情况下,服务器(进程)也将退出。而且,当一个进程退出的时候,内核会关闭所有由这个进程打开的套接字,这里将触发TCP的四次挥手进而关闭一个socket连接。但是,在一些异常的情况下,譬如:服务器进程终止、服务器主机奔溃/奔溃后重启、服务器关机的情况下,客户端向服务器发起请求的时候,将会发生什么呢?下边,我们来看看这几种情况。

注意:一下描述的各种情况所使用的示例程序在文章的最后贴出

  一、服务器进程终止

我们启动客户/服务器对,然后杀死子进程(模拟服务器进程崩溃的情形,我们可从中查看客户端将发生什么)。

1:在同一个主机上启动服务器和客户,并在客户上输入一行文本,以验证一切正常。正常情况下,改行文本将由服务器回射给客户。

2:找到服务器子进程的ID,通过kill命令杀死它。作为进程终止处理的部分工作,子进程中所有打开着的描述字都被关闭。这就导致向客户发送一个FIN,而客户TCP则响应以一个ACK。这就是TCP连接终止的前一半工作。

3:子进程终止时,内核将给父进程递交SIGCHLD信号。

4:客户上没有发生任何特殊之事。客户TCP接受来自服务器TCP的FIN并响应一个ACK,然后问题是客户进程此时阻塞在fgets调用上,等待从终端接受一行文本。它是看不到这个FIN的。

5:此时我们如果运行netstat命令,可以看到如下的套接口的状态:

针对TCP连接异常断开的分析

FIN_WAIT2即为我们杀掉的那个子进程的,因为我们知道主动关闭的那端在发送完fin并接受对端的ack后将进入fin_wait2状态,此时它在等待对端的fin。

6:现在我们在客户上在输入一行文本,我们可以看到如下的输出:

针对TCP连接异常断开的分析

当我们输入“after server close”时,客户TCP接着把数据发送给服务器,TCP允许这么做,因为客户TCP接受到FIN只是表示服务器进程已关闭了连接的服务端,从而不再往其中发送任何数据而已。FIN的接受并没有告知客户TCP服务器进程已经终止(在这个例子中它缺失是终止了)。当服务器TCP接收到来自客户的数据时,既然先前打开那个套接口的进程已经终止,于是响应一个RST。

7:然而客户进程看不到这个RST,因为它在调用write后立即调用read,并且由于第2步中接收到FIN,所调用的read立即返回0(表示)EOF。我们的客户此时并未预期收到EOF,于是以出错信息“server term prematurely.”(服务器过早终止)退出。

8:当客户终止时,它所有打开着的描述字都被关闭。

        我们的上述讨论还取决于程序的时序。客户调用read既可能发生在服务器的RST被客户收到之前,也可能发生在收到之后。如果read发生在收到RST之前(如本例子所示),那么结果是客户得到一个未预期的EOF;否则结果是由readline返回一个ECONNRESET(“connection reset by peer”对方复位连接)错误。

本例子的问题在于:当FIN到达套接口时,客户正阻塞在fgets调用上。客户实际上在应对两个描述字——套接口和用户输入,它不能单纯阻塞在这两个源中某个特定源的输入上,而是应该阻塞在其任何一个源的输入上。(可用select等io复用的函数实现)

二、服务器主机崩溃

我们接着查看当服务器主机崩溃时会发生什么。为了模拟这种情形,我们需要在不同的机器上运行客户与服务器,在首次确认客户服务器能正常工作后,我们从网络上断开服务器主机,并在客户上再输入一行文本。这里同时也模拟了当客户发送数据时服务器主机不可达的情形(机建立连接后某些中间路由器不工作)

1:当服务器主机崩溃时,已有的网络连接上发不出任何东西。这里我们假设的是主机崩溃,而不是执行了关机命令。

2:我们在客户上输入一行文本,它由write写入内核,再由客户TCP作为一个数据分节送出。客户随后阻塞于read调用,等待服务器的应答。

3:这种情况下,客户TCP持续重传数据分节,试图从服务器上接受一个ACK。(源自Berkeley的实现重传该数据分节12次,共等待约9分钟才放弃重传。)当客户TCP最终放弃时(假设这段时间内,服务器主机没有重新启动或者如果是服务器主机为崩溃但从网络上不可达的情况,那么假设主机仍然不可达),返回客户进程一个错误。既然客户阻塞在readline调用上,该调用将返回一个错误。假设服务器已崩溃,从而对客户的数据分节根本没有响应,那么所返回的错误是ETIMEDOUT。然而如果某个中间路由器判定服务器主机已不可达,从而响应以一个“destination unreachable”,那么所返回的错误是EHOSTUNREACH或ENETUNREACH。

尽管我们的客户最后还是发现对端主机已崩溃或不可达,不过有时候我们需要更快地检测出这种情况,而不是不得不等待9分钟。所用的方法就是对read调用设置一个超时。

另外我们刚讨论的情形只有在向服务器主机发送数据时,才能检测出它已经崩溃,如果我们不主动发送主句也想检测出服务器主机的崩溃,那么就需要用到SO_KEEPALIVE这个套接口选项。

三、服务器主机崩溃后重启

在前一节的分析中,当我们发送数据时,服务器主机仍然处于崩溃状态;这节,我们将在发送数据前重新启动崩溃了的服务器主机。模拟这种情况的简单方法就是:建立连接,再从网络上端口服务器主机,将它关机后再重启,最后把它重新连接到网络中。

如前一节所述,如果在服务器主机崩溃时客户不主动给服务器发送数据,那么客户不会知道服务器主机已经崩溃。所发生的步骤如下:

1:启动客户服务器,在客户上输入一行文本已确认连接已建立。

2:服务器主机崩溃并重启。

3:在客户上输入一行文本,它将作为一个TCP数据分节发送到服务器主机。

4:当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应以一个RST。

5:当客户TCP收到该RST时,客户正阻塞于read调用,导致该调用返回ECONNRESET错误。

    四、服务器主机关机

这节我们看看当服务器关机时将会发生什么。

Unix系统关机时,init进程通常先给所有进程发送SIGTERM信号(该信号可被捕获),再等待一段固定的时间(一般在5~20秒之间),然后给所有仍在运行的进程发送SIGKILL信号(该信号不能被捕获)。这么做是留给所有运行中的进程一小段时间来清除和终止。如果我们不捕获SIGTERM信号并终止,我们的服务器将由SIGKILL信号终止。当服务器进程终止时,它的所有打开着的描述字都被关闭,随后发生的步骤与第一节中讨论过的一样。正如第一节中所述的情形,我们必须在客户中使用select或poll函数,使得服务器进程的终止已经发生,客户马上检测到。

五、示例程序

//client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <errno.h>
#include <error.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h> void str_cli(FILE *fp, int sfd ) {
char sendline[], recvline[];
memset(recvline, , sizeof(sendline));
memset(sendline, , sizeof(recvline));
while( fgets(sendline, , fp) != NULL ) {
write(sfd, sendline, strlen(sendline));
if( read(sfd, recvline, ) == ) {
printf("server term prematurely.\n");
}
fputs(recvline, stdout);
memset(recvline, , sizeof(sendline));
memset(sendline, , sizeof(recvline));
}
} int main() {
int s;
if( (s = socket(AF_INET, SOCK_STREAM, )) < ) {
int e = errno;
perror("create socket fail.\n");
exit();
} struct sockaddr_in server_addr, child_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons();
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); if( connect(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < ) {
perror("connect fail.");
exit();
}
str_cli(stdin, s);
exit();
}
//server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <errno.h>
#include <error.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h> //using namespace std; typedef void sigfunc(int); void func_wait(int signo) {
pid_t pid;
int stat;
pid = wait(&stat);
printf( "child %d exit\n", pid );
return;
}
void func_waitpid(int signo) {
pid_t pid;
int stat;
while( (pid = waitpid(-, &stat, WNOHANG)) > ) {
printf( "child %d exit\n", pid );
}
return;
} sigfunc* signal( int signo, sigfunc *func ) {
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = ;
if ( signo == SIGALRM ) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 4.4BSD */
#endif
}
if ( sigaction(signo, &act, &oact) < ) {
return SIG_ERR;
}
return oact.sa_handler;
} void str_echo( int cfd ) {
ssize_t n;
char buf[];
//char t[] = "SERVER ECHO: ";
again:
memset(buf, , sizeof(buf));
while( (n = read(cfd, buf, )) > ) {
write(cfd, buf, n);
}
if( n < && errno == EINTR ) {
goto again;
} else {
printf("str_echo: read error\n");
}
} int main() { signal(SIGCHLD, &func_waitpid); int s, c;
pid_t child;
if( (s = socket(AF_INET, SOCK_STREAM, )) < ) {
int e = errno;
perror("create socket fail.\n");
exit();
} struct sockaddr_in server_addr, child_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons();
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if( bind(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < ) {
int e = errno;
perror("bind address fail.\n");
exit();
} if( listen(s, ) < ) {
int e = errno;
perror("listen fail.\n");
exit();
}
while() {
socklen_t chilen = sizeof(child_addr);
if ( (c = accept(s, (struct sockaddr *)&child_addr, &chilen)) < ) {
perror("listen fail.");
exit();
} if( (child = fork()) == ) {
close(s);
str_echo(c);
exit();
}
close(c);
}
}