本章我们讲述一下tcpip中的rst报文段。

1. TCP连接中的rst报文段

应该没有人会质疑,现在是一个网络时代了。应该不少程序员在编程中需要考虑多机、局域网、广域网的各种问题。所以网络知识也是避免不了学习的。而且笔者一直觉得TCP/IP网络知识在一个程序员知识体系中必须占有一席之地的。

在TCP协议中RST表示复位,用来异常的关闭连接,在TCP的设计中它是不可或缺的。发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓冲区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。

其实在网络编程过程中,各种RST错误其实是比较难排查和找到原因的。下面我列出几种会出现RST的情况。

1.1 端口未打开

服务器程序端口未打开而客户端来连接。这种情况是最为常见和好理解的一种了。去telnet一个未打开的TCP端口可能会出现这种错误。这个和操作系统的实现有关。在某些情况下,操作系统也会完全不理会这些发到未打开端口的请求。

比如在下面这种情况下,主机241主机114发送一个SYNC请求,表示想要连接主机114的40000端口,但是主机114上根本没有打开40000端口,于是就想主机241发送了一个RST。这种情况很常见,特别是服务器程序core dump之后重启之前连续出现RST的情况会经常发生。

tcp-rst-case1

当然在某些操作系统的主机上,未必是这样的表现。比如向一台WINDOWS7的主机发送一个连接不存在的端口的请求,这台主机就不会回应。

1.2 请求超时

曾经遇到过这样一个情况:一个客户端连接服务器,connect返回-1并且errno为EINPROGRESS。直接telnet发现网络连接没有问题,ping也没有出现丢包。用抓包工具查看,客户端是在收到服务器发出的SYNC之后就莫名其妙的发送了RST。

比如像下面这样:

tcp-rst-case2

有89、27两台主机。主机89向主机27发送了一个SYNC包,表示希望连接主机27的8888端口,主机27回应了主机89一个SYNC表示可以连接。但是主机27却很不友好,莫名奇妙的发送了一个RST表示我不想连接你了。

后来经过排查发现,主机89上的程序在创建了socket之后,用setsockopt()的SO_RCVTIMEO选项设置了recv的超时时间未100ms。而我们看上面的抓包结果显示:从主机89发出SYNC到接收到SYNC+ACK的回应的时间多达110ms(从15:01:27.799961到15:01:27.961886, 小数点之后的单位是微秒)。因此主机89上的程序认为接收超时,所以发送了RST拒绝进一步发送数据。

1.3 提前关闭连接

关于TCP,我想我们在教科书里都读到过一句话:TCP是一种可靠的连接。而这可靠有这样一种含义,那就是操作系统接收到的来自TCP连接中的每一个字节,我都会让应用程序接收到。如果应用程序不接收怎么办? 你猜对了,RST。

看下面两段程序:

1)服务端server.c

//server.c

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];
			read(real_fd,pcContent,4096);
			
			close(real_fd); 
			exit(0);             
		} 

		close(real_fd); 
	}    

	return 0; 
}

这一段是server的最简单的代码。逻辑很简单,监听一个TCP端口然后当有客户端来连接的时候fork一个子进程来处理。注意看的是这一段fork里面的处理:

char pcContent[4096];
read(real_fd,pcContent,4096);
			
close(real_fd); 

每次只读socket的前4096个字节,然后就关闭掉连接。

2) 客户端client.c

接下来再看一下client的代码:

//client.c

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) 
	{ 
		perror("socket failed  "); 
		return -1; 
	} 

	bzero(&s_addr, sizeof(s_addr)); 
	s_addr.sin_family = AF_INET; 
	
	inet_pton(AF_INET,SER_IP,&s_addr.sin_addr); 
	s_addr.sin_port = htons(SER_PORT); 

	if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1) 
	{ 
		perror("connect fail  "); 
		return -1; 

	} 

	char pcContent[5000]={0};
	write(send_sk,pcContent,5000);

	sleep(1);

	close(send_sk);

	return 0x0;
}

这段代码更简单,就是打开一个socket然后连接一个服务器并发送5000个字节。刚才我们看服务器的代码,每次只接收4096个字节,那么就是说客户端发送的剩下的4个字节服务端的应用程序没有接收到,服务器端的socket就被关闭掉,这种情况下会发生什么状况呢,还是抓包看一看。

tcp-rst-case3

上图中,前三行就是TCP的3次握手,从第四行开始看,客户端的49660端口向服务器的9877端口发送了5000各字节的数据,然后服务器端发送了一个ACK进行了确认,紧接着服务器向客户端发送了一个RST断开了连接。和我们的预期一致。

1.4 在一个已关闭的socket上收到数据

如果某个socket已经关闭,但依然收到数据,此种情况下也会产生RST。参看如下代码:

1) 客户端client.c

//client.c

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)  
	{  
		perror("socket failed  ");  
		return -1;  
	}  

	bzero(&s_addr, sizeof(s_addr));  
	s_addr.sin_family = AF_INET;  

	inet_pton(AF_INET,SER_IP,&s_addr.sin_addr);  
	s_addr.sin_port = htons(SER_PORT);  

	if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)  
	{  
		perror("connect fail  ");  
		return -1;  
	}  

	char pcContent[4096]={0};
	write(send_sk,pcContent,4096);

	sleep(1);

	write(send_sk,pcContent,4096);
	close(send_sk);

	return 0x0;
} 

2) 服务的server.c

//server.c


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];
			read(real_fd,pcContent,4096);
			close(real_fd);  

			exit(0);              
		}  

		close(real_fd);  
	}     

	return 0;  
} 

客户端在服务端已经关闭掉socket之后,仍然在发送数据。这时服务端会产生RST。参看如下:

tcp-rst-case4

2. 总结

总结,本文讲了几种TCP连接中出现RST的情况。实际上肯定还有无数种的RST发生,我以后会慢慢收集把更多的例子加入这篇文章。

RST通常用来处理某种有意为之的异常。



[参看]

  1. 5种TCP连接中出现RST的情况

  2. 网络编程Socket之RST详解

  3. 几种TCP连接中出现RST的情况

  4. TCP协议RST:RST介绍、什么时候发送RST包

  5. Linux 下TCP连接关闭情况分析