TCP出现RST的几种情况


再谈TCP握手

导致 “Connection reset” 的原因是服务器端因为某种原因关闭了Connection,而客户端依然在读写数据,此时服务器会返回复位标志“RST”,然后此时客户端就会提示 “java.net.SocketException: Connection reset” 。

可能有同学对复位标志 “RST” 还不太了解,这里简单解释一下:

TCP建立连接时需要三次握手,在释放连接需要四次挥手;

例如三次握手的过程如下:

  • 第一次握手:客户端发送syn包( syn=j )到服务器,并进入 SYN_SENT 状态,等待服务器确认;

  • 第二次握手:服务器收到syn包,并会确认客户的SYN( ack=j+1 ),同时自己也发送一个SYN包( syn=k ),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;

  • 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包ACK ( ack=k+1 ),此包发送完毕,客户端和服务器进入 ESTABLISHED( TCP 连接成功 )状态,完成三次握手。

可以看到握手时会在客户端和服务器之间传递一些 TCP 头信息,比如 ACK 标志、SYN 标志以及挥手时的 FIN 标志等。

除了以上这些常见的标志头信息,还有另外一些标志头信息,比如推标志 PSH 、复位标志 RST 等。其中复位标志 RST 的作用就是 “复位相应的TCP连接” 。

TCP连接和释放时还有许多细节,比如半连接状态、半关闭状态等。详情请参考这方面的巨著《TCP/IP详解》和《UNIX网络编程》。


RST 报文从何产生

前面说到出现 “Connection reset” 的原因是服务器关闭了 Connection [ 调用了Socket.close() 方法]。

大家可能有疑问了:服务器关闭了Connection为什么会返回 “RST” 而不是返回 “FIN” 标志。

原因在于 Socket.close() 方法的语义和 TCP 的 “FIN” 标志语义不一样:

发送 TCP 的 “FIN” 标志表示我不再发送数据了,而 Socket.close() 表示我不在发送也不接受数据了。

问题就出在 “我不接受数据” 上,如果此时客户端还往服务器发送数据,服务器内核接收到数据,但是发现此时 Socket 已经 close 了,则会返回 “RST” 标志给客户端。

当然,此时客户端就会提示:”Connection reset”。

详细说明可以参考oracle的有关文档:http://docs.oracle.com/javase/1.5.0/docs/guide/net/articles/connection_release.html。

另一个可能导致的 “Connection reset” 的原因是服务器设置了 Socket.setLinger (true, 0) 。但我检查过线上的 tomcat 配置,是没有使用该设置的,而且线上的服务器都使用了 nginx 进行反向代理,所以并不是该原因导致的。关于该原因上面的 oracle 文档也谈到了并给出了解释。


Connection reset 与 Connection reset by peer

区别之处

此外啰嗦一下,另外还有一种比较常见的错误 “Connection reset by peer”,该错误和 “Connection reset” 是有区别的:

  • 服务器返回了 “RST” 时,如果此时客户端正在从 Socket 套接字的输出流中读数据则会提示 Connection reset ;

例如:

A向B发起连接,但B之上并未监听相应的端口,这时B操作系统上的 TCP 处理程序会发 RST 包。

  • 服务器返回了 “RST” 时,如果此时客户端正在往 Socket 套接字的输入流中写数据则会提示 Connection reset by peer 。

例如:

AB正常建立连接了,正在通讯时,A向B发送了FIN包要求关连接,B发送ACK后,网断了,A通过若干原因放弃了这个连接(例如进程重启)。

等网络恢复之后,B又开始发数据包(客户端并不知道,服务器已经忘记三次握手了),A收到后表示压力很大,不知道这野连接哪来的,就发了个RST包强制把连接关了,B收到后会出现 connect reset by peer 错误。

需要注意的是,服务端有两种情况不会发送RST:

  1. 服务器关机:

    会断开 TCP 连接,会发送 FIN 数据报

  2. 服务器主机崩溃的状态

    如果,客户端和服务器已经建立了连接的时候,此时服务器崩溃(达到这一标准可以把服务器的网线拔掉,这个时候,服务器就不能发送 FIN 数据报了,和关机不一样的)

    此时如果客户端向服务器发送数据的时候,因为服务器已经不存在了,那么客户端就不能接受到服务器给客户端的 ack 信息,这个时候,客户端建立的是 TCP 连接,就会重发数据报,发送多少次之后就会返回超时,也就是 ETIMEOUT 。

  • ETIMEOUT:当connect调用的时候会进行三次握手,如果客户端没有收到服务器对SYN的ACK数据报,就会返回ETIMEOUT(客户端在返回这个错误之前会重发SYN数据报)

前面谈到了导致 “Connection reset” 的原因,而具体的解决方案有如下几种:

  1. 出错了重试;
  2. 客户端和服务器统一使用TCP长连接;
  3. 客户端和服务器统一使用TCP短连接。
  • 首先是出错了重试:这种方案可以简单防止 “Connection reset” 错误,然后如果服务不是 “幂等” 的则不能使用该方法;比如提交订单操作就不是幂等的,如果使用重试则可能造成重复提单。

  • 然后是客户端和服务器统一使用 TCP 长连接:客户端使用 TCP 长连接很容易配置(直接设置HttpClient就好),而服务器配置长连接就比较麻烦了,就拿tomcat来说,需要设置 tomcat 的 maxKeepAliveRequestsconnectionTimeout 等参数。另外如果使用了 nginx 进行反向代理或负载均衡,此时也需要配置 nginx 以支持长连接(nginx默认是对客户端使用长连接,对服务器使用短连接,详见 keepalived 相关指令)。

  • 使用长连接可以避免每次建立 TCP 连接的三次握手而节约一定的时间,但是我这边由于是内网,客户端和服务器的 3 次握手很快,大约只需1ms。ping一下大约0.93ms(一次往返);三次握手也是一次往返(第三次握手不用返回)。根据80/20原理,1ms可以忽略不计;又考虑到长连接的扩展性不如短连接好、修改nginx和tomcat的配置代价很大(所有后台服务都需要修改);所以这里并没有使用长连接。

小结

  1. Connection reset,远程主机没有监听这个端口、连接,可以是:
    1. 服务端已关闭,客户端仍旧请求,服务端返回Rst;
    2. 服务端未监听该端口,客户端请求,服务端返回Rst;
  2. Connection reset by peer,是远程主机强迫关闭了一个现有的连接,可以是:
    1. 客户端断网重连,服务端返回Rst;
    2. 服务端进程崩溃后重启,向先前的客户端返回Rst,并等待下次重新与客户端建连;

常见异常情况

正常情况 TCP 都是通过四次挥手关闭连接,RST 基本都是异常情况,整理如下:

  1. 使用 ping 可以看到丢包情况

  2. GFW

  3. 对方端口未打开,发生在连接建立

      如果对方 sync_backlog 满了的话,sync直接被丢弃,表现为超时,而不会 Rst

  4. close Socket 时 recv buffer 不为空

  例如,客户端发了两个请求,服务器只从buffer 读取第一个请求处理完就关闭连接,tcp层认为数据没有正确提交到应用,使用rst关闭连接。

  1. 移动链路

    移动网络下,国内是有5分钟后就回收信令,也就是IM产品,如果心跳 > 5分钟后服务器再给客户端发消息,就会收到rst。也要查移动网络下IM 保持 < 5min 心跳。

  2. 负载等设备

    负载设备需要维护连接转发策略,长时间无流量,连接也会被清除,而且很多都不告诉两层机器,新的包过来时才通告rst。

   Apple push 服务也有这个问题,而且是不可预期的偶发性连接被rst;rst 前第一个消息write 是成功的,而第二条写才会告诉你连接被重置,

  曾经被它折腾没辙,因此打开每2秒一次tcp keepalive,固定5分钟 tcp 连接回收,而且发现连接出错时,重发之前10s内消息。

  1. SO_LINGER 应用强制使用rst 关闭

    该选项会直接丢弃未发送完毕的send buffer,可能造成业务错误,慎用; 当然内网服务间http client 在收到应该时主动关闭,使用改选项,会节省资源。

    好像曾经测试过haproxy 某种配置下,会使用rst关闭连接,少了网络交互而且没有TIME_WAIT 问题

  2. 超过超时重传次数、网络暂时不可达

  3. TIME_WAIT 状态

  tw_recycle = 1 时,sync timestamps 比上次小时,会被 Rst

  1. 设置 connect_timeout

    应用设置了连接超时,sync 未完成时超时了,会发送rst终止连接。

  2. 非正常包

    连接已经关闭,seq 不正确等

  3. keepalive 超时

    公网服务tcp keepalive 最好别打开;移动网络下会增加网络负担,切容易掉线;非移动网络核心ISP设备也不一定都支持keepalive,曾经也发现过广州那边有个核心节点就不支持。

  4. 数据错误,不是按照既定序列号发送数据

  5. 在一个已关闭的 socket 上接收数据

  6. 服务器关闭或异常终止了连接,由于网络问题,客户端没有收到服务器的关闭请求,这称为TCP半打开连接。就算重启服务器,也没有连接信息。如果客户端向提其写入数据,对方就会回应一个RST报文段。


参考文档

setsockopt :SO_LINGER 选项设置(转)

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


文章作者: NesteaLin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 NesteaLin !
  目录