一. 服务器关闭与TCP异常
1.1 服务器正常关闭
“正常”关闭:调用close()关闭socket、没close但进程正常结束、进程core掉(进程由于内存越界等等而终止)、在shell命令行中kill掉进程,都可抽象成“正常”关闭。因为即使core掉,内核也会马上帮应用程序回收(close)socket文件描述符。
“正常”关闭,默认情况下(非默认即设置Linger,后续会介绍),关闭端即服务器端的TCP层会发FIN包,而对端即客户端的TCP层收到后,回ACK,服务器端进入FIN_WAIT2状态。此时,只有客户端socket“正常”关闭,才会发出这个FIN包。至此,服务器端进入TIME_WAIT状态。“正常”关闭:调用close()关闭socket、没close但进程正常结束、进程core掉(进程由于内存越界等等而终止)、在shell命令行中kill掉进程,都可抽象成“正常”关闭。因为即使core掉,内核也会马上帮应用程序回收(close)socket文件描述符。
“正常”关闭,默认情况下(非默认即设置Linger,后续会介绍),关闭端即服务器端的TCP层会发FIN包,而对端即客户端的TCP层收到后,回ACK,服务器端进入FIN_WAIT2状态。此时,只有客户端socket“正常”关闭,才会发出这个FIN包。至此,服务器端进入TIME_WAIT状态。
1.1.1 对FIN包处理
如果服务器在“正常”关闭的情况下,客户端收到FIN包后,不做常规的处理(所谓常规处理,即关闭自己的套接字,向服务器发送FIN包),而是继续发送数据给服务器,会如何呢?比如客户端的程序如下(摘录自 《unix网络编程》 5.12节):
void str_cli(int sockfd)
{
char sendline[MAXLINE],recvline[MAXLINE];
while(fgets(sendline, MAXLINE, stdin)!=NULL) //从标准输入获得字符串
{
writen(sockfd, sendline, strlen(sendline)); //将字符串写入socket
if(readline(sockfd, recvline, MAXLINE) == 0) //从socket读取数据
{
perror("server terminated permaturely");
eixt(0);
}
fputs(reavline, stdout); //将数据显示在标准输出
}
}<span style="font-family:Simsun;"><span style="background-color: rgb(255, 255, 255);"></span></span>
a) 当服务器终止后,我们键入其他字符串,客户TCP将数据发送给服务器。让服务器TCP收到客户端的数据时,因为自己套接字所在进程已经终止,于是回应一个RST。
b) 然而客户端进程看不到RST,由于writen之后立即调用readline,而之前已经收到服务器的FIN, 所以调用readline会立即返回0,从而显示出错信息(服务器过早终止)而退出,
这个writen到 客户端退出的时间极短,所以基本看不到RST回应。
1.1.2 不对FIN包处理
如果客户端不理会readline函数的返回错误,反而写入更多的数据到服务器上,那会发生什么呢?
第一次writen时,服务器将返回RST,当客户端再次writen时,内核将向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此该进程必须捕获它以免不情愿地被终止。 不论该进程是捕获了该进程并从其inhao处理函数返回,还是简单的忽略该信号,写操作都会返回EPIPE错误。
客户端的程序修改如下(摘录自 《unix网络编程》 5.13节):
void str_cli(int sockfd)
{
char sendline[MAXLINE],recvline[MAXLINE];
while(fgets(sendline, MAXLINE, stdin)!=NULL) //从标准输入获得字符串
{
writen(sockfd, sendline, 1)); //写入一个字符,引发一个RST
sleep(1);
writen(sockfd, sendline, strlen(sendline)); //剩余字符写入socket,产生SIGPIPE信号
if(readline(sockfd, recvline, MAXLINE) == 0) //从socket读取数据
{
perror("server terminated permaturely");
eixt(0);
}
fputs(reavline, stdout); //将数据显示在标准输出
}
}
上述代码片,调用了两次writen:一次把数据的第一个字节写入套接字,目的是引发一个RST;第二次把剩余字节写入套接字,此时操作系统将产生SIGPIPE。
1.2 服务器崩溃(非正常关闭)
“非”正常关闭:服务器崩溃了(或者网络上断开服务器主机),此时肯定发不出FIN包了(当然啦,内核都没机会帮应用程序回收资源了)。这种情况,服务器端有如下两种情况:
- 客户端发送的数据,应为服务器已经崩溃了,客户端会不断的重传数据包,试图从服务器上接收一个ACK。Berkeley的实现重传数据包为8次,相对第一次传的15分钟后仍然没有收到ACK,则返回ETIMEDOUT(超时)或EHOSTUNREAC(网络不可达)错误。
- 上述方式需要客户端主动发数据来检测服务器主机的崩溃。如果不主动向它发数据,依靠应用层的心跳机制或者keepalive,也能发觉TCP断链。
1.3 服务器崩溃后重启
正如前一节所讲的,如果客户在服务器主机崩溃时不主动发数据给服务器,那么客户是不知道服务器已经崩溃的(假设这里不用套接字选项SO_KEEPALIVE)。该种情况下TCP的流程如下:
a). 启动服务器和客户,保证两者间连接的确立;
b). 服务器主机崩溃并重启;
c). 客户端发送数据给服务器;
d). 当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,所以服务器TCP对接收的客户数据包以RST响应。
e). 当RST到达时,客户端阻塞于send调用,导致它返回ECONNRST (连接重启)错误。
如果服务器不是关闭后重启,而是服务器端将网线拔除后,重新连接上,此时服务器上还存在连接信息,重新连接上后两端通信正常。
二. RST出现情况
2.1 FIN与RST区别
FIN是正常关闭,它会根据缓冲区的顺序来发的,即缓冲区FIN之前的包都发出去后再发送FIN包,这与RST不同。
RST表示复位,用来异常的关闭连接,就像上面所说,发送RST包关闭时不必等缓冲区的包都发出去,直接就求其缓冲过去而发送RST包,而接收端收到RST包后,也不必发送ACK包来确认。
2.2 RST出现情况分析
1. 端口未打开
服务器程序端口未打开而客户端来连接。telnet连接一个未打开的TCP的端口可能会出现这种错误。这个和操作系统的实现有关。在某些情况下,操作系统也会完全不理会这些发到未打开端口请求。
2. 请求超时
在建立了socket之后,用setsockopt的SO_RCVTIMEO选项设置了recv的超时时间。如果接收数据超时了。会发送RST包表示拒绝进一步发送数据。
如果双方连接未完全建立,此时客户端发送一个RST,将会导致accept返回一个错误。该情况的模拟情形:启动服务器,调用sokcet,bind和listen,然后在调用accept之前睡眠一段时间;启动客户端,调用socket和connect,一旦connect返回,就设置SO_LINGER套接字选项以产生这个RST(下篇讲解),然后终止。
这回导致accept返回一个非致命的错误。对于中止的连接处理,不同协议处理方式不同。源自Berkeley的实现完全在内核中处理,服务器进程根本看不到。SVR4实现返回一个错误给服务器进程,作为accept的返回结果,而POSIX指出返回的errno值必须使ECONNABORTED。服务器遇到该错误时,可以忽略它,然后再次调用accept就行。
3. 在一个已经关闭的socket上收到数据
以下通过实例代码分析该情况(代码来源于网络博文《几种TCP连接中出现RST的情况》)
服务器端程序如下:
int main(int argc, char** argv)
{
int listen_fd, real_fd;
struct sockaddr_in listen_addr, client_addr;
socklen_t len = sizeof(struct sockaddr_in);
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if(listen_fd == -1)
{
perror("socket failed ");
return -1;
}
bzero(&listen_addr,sizeof(listen_addr));
listen_addr.sin_family = AF_INET;
listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);
listen_addr.sin_port = htons(SERV_PORT);
bind(listen_fd,(struct sockaddr *)&listen_addr, len);
listen(listen_fd, WAIT_COUNT);
while(1)
{
real_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
if(real_fd == -1)
{
perror("accpet fail ");
return -1;
}
if(fork() == 0) //产生子进程处理连接
{
close(listen_fd);
char pcContent[4096]; //客户端发送5000个字节,接收端接收4096个
read(real_fd,pcContent,4096);
close(real_fd); //立即关闭连接套接字
exit(0);
}
close(real_fd);
}
return 0;
}
客户端程序如下:
#include <sys/socket.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <strings.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char** argv)
{
int send_sk;
struct sockaddr_in s_addr;
socklen_t len = sizeof(s_addr);
send_sk = socket(AF_INET, SOCK_STREAM, 0);
if(send_sk == -1)
{
printf("socket failed ");
return -1;
}
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
inet_pton(AF_INET,"172.16.130.240",&s_addr.sin_addr);
s_addr.sin_port = htons(8000);
if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)
{
printf("connect fail ");
return -1;
}
char pcContent[5000]={0};
write(send_sk,pcContent,5000);
sleep(1);
close(send_sk);
}
客户端在服务端已经关闭掉socket之后,仍然在发送数据(数据是分组发送的)。这时服务端会回应RST(内核也会向客户进程发送SIGPIPE信号)。下图是笔者通过tcpdump抓到的数据包:
其中,包所携带的标志如下:
S=SYN 发起连接标志
P=PUSH 传送数据标志
F=FIN 关闭连接标志
ack 表示确认包
RST 异常关闭连接
. 表示没有任何标志
其中,前三个信令交互为TCP连接的三次握手, 后续为数据的交互以及FIN、RST的交互。具体交互流程可见下图所示: