1、定义:
RST表示复位,RST=1表示TCP中出现严重错误(由于主机崩溃或其它原因),必须释放连接。RST=1还可用来拒绝一个非法的报文段或拒绝打开一个连接。发送RST后不会再有正常的四分组终止序列(Unix网络编程so_linger选项)。
2、有三个条件可以产生RST:
*SYN到达某端口但此端口上没有正在监听的服务器(针对TCP而言,UDP则返回端口不可达ICMP错误)。*TCP想取消一个已有连接(使用套接字SO_LINGER选项)。
*TCP接收了一个根本不存在的连接上的分节。
3、说明
RST报文段不会导致另一端产生任何响应,另一端不进行确认,收到RST的一段将终止该连接,并通知应用层连接复位。
RST会造成丢弃任何待发数据并立即发送复位报文段。正常终止连接是在所有排队数据都已经发送之后才发送FIN,正常情况下没有任何数据丢失(TCP/IP详解p187)
4、RST数据报文产生情况
其实在网络编程过程中,各种RST错误其实是比较难排查和找到原因的。因此我们需要在编程时尽可能规避不合理的操作,而导致的RST数据包,而分析那些真正有异常的RST数据包。下面我列出几种会出现RST的情况:
1 端口未打开
服务器程序端口未打开而客户端来连接。这种情况是最为常见和好理解的一种了。去telnet一个未打开的TCP的端口可能会出现这种错误。这个和操作系统的实现有关。在某些情况下,操作系统也会完全不理会这些发到未打开端口请求。
比如在下面这种情况下,主机241向主机114发送一个SYN请求,表示想要连接主机114的40000端口,但是主机114上根本没有打开40000这个端口,于是就向主机241发送了一个RST。这种情况很常见。特别是服务器程序core dump之后重启之前连续出现RST的情况会经常发生。
当然在某些操作系统的主机上,未必是这样的表现。比如向一台WINDOWS7的主机发送一个连接不存在的端口的请求,这台主机就不会回应。
2 请求超时
曾经遇到过这样一个情况:一个客户端连接服务器,connect返回-1并且error=EINPROGRESS。 直接telnet发现网络连接没有问题。ping没有出现丢包。用抓包工具查看,客户端是在收到服务器发出的SYN之后就莫名其妙的发送了RST。
比如像下面这样:
有89、27两台主机。主机89向主机27发送了一个SYN,表示希望连接8888端口,主机27回应了主机89一个SYN表示可以连接。但是主机27却很不友好,莫名其妙的发送了一个RST表示我不想连接你了。
后来经过排查发现,在主机89上的程序在建立了socket之后,用setsockopt的SO_RCVTIMEO选项设置了recv的超时时间为100ms。而我们看上面的抓包结果表示,从主机89发出SYN到接收SYN的时间多达110ms。(从15:01:27.799961到15:01:27.961886, 小数点之后的单位是微秒)。因此主机89上的程序认为接收超时,所以发送了RST拒绝进一步发送数据。
3 提前关闭
关于TCP,我想我们在教科书里都读到过一句话,'TCP是一种可靠的连接'。 而这可靠有这样一种含义,那就是操作系统接收到的来自TCP连接中的每一个字节,我都会让应用程序接收到。如果应用程序不接收怎么办?你猜对了,RST。
看两段程序:
//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;
}
char pcContent[4096];
read(real_fd,pcContent,4096);
close(real_fd);
然后再看一下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);
}
前三行就是TCP的3次握手,从第四行开始看,客户端的49660端口向服务器的9877端口发送了5000个字节的数据,然后服务器端发送了一个ACK进行了确认,紧接着服务器向客户端发送了一个RST断开了连接。和我们的预期一致。
4 在一个已关闭的socket上收到数据
如果某个socket已经关闭,但依然收到数据也会产生RST。
代码如下:
客户端:
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);
}
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。
5. GFW
防火长城(英文名称Great Firewall of China,简写为Great Firewall,缩写GFW),也称中国防火墙或中国国家防火墙,指中华人民共和国政府在其管辖因特网内部建立的多套网络审查系统的总称,包括相关行政审查系统。[1] 首要设计者为北京邮电大学原校长方滨兴,被称为“国家防火墙之父”
6. 移动链路
移动网络下,国内是有5分钟后就回收信令,也就是IM产品,如果心跳>5分钟后服务器再给客户端发消息,就会收到rst。也要查移动网络下IM 保持<5min 心跳。
7. 负载等设备
负载设备需要维护连接转发策略,长时间无流量,连接也会被清除,而且很多都不告诉两层机器,新的包过来时才通告rst。
Apple push 服务也有这个问题,而且是不可预期的偶发性连接被rst;rst 前第一个消息write 是成功的,而第二条写才会告诉你连接被重置,
曾经被它折腾没辙,因此打开每2秒一次tcp keepalive,固定5分钟tcp连接回收,而且发现连接出错时,重发之前10s内消息。
8. SO_LINGER 应用强制使用rst 关闭
该选项会直接丢弃未发送完毕的send buffer,可能造成业务错误,慎用; 当然内网服务间http client 在收到应该时主动关闭,使用改选项,会节省资源。
好像曾经测试过haproxy 某种配置下,会使用rst关闭连接,少了网络交互而且没有TIME_WAIT 问题
9. 超过超时重传次数、网络暂时不可达
10. TIME_WAIT 状态
tw_recycle = 1 时,sync timestamps 比上次小时,会被rst
11. 非正常包
连接已经关闭,seq 不正确等
12. keepalive 超时
公网服务tcp keepalive 最好别打开;移动网络下会增加网络负担,切容易掉线;非移动网络核心ISP设备也不一定都支持keepalive,曾经也发现过广州那边有个核心节点就不支持。