一. 服务器关闭与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>
1.1.2 不对FIN包处理
客户端的程序修改如下(摘录自 《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 服务器崩溃后重启
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的交互。具体交互流程可见下图所示:
参考阅读:
TCP断开连接:http://www.cppblog.com/toMyself/archive/2010/08/11/123072.html
几种TCP连接中出现RST的情况:http://www.360doc.com/content/13/0702/10/1073512_297069771.shtml
《UNIX网络编程》 Ch5 TCP客户端程序示例