TCP 异常处理

TCP RST

参考:

TCP RST 引起的常见报错:

  • Connection reset
  • Connection reset by peer

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

前面说到出现“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 by peer”,该错误和“Connection reset”是有区别的:

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

“Connection reset by peer”如下图所示:

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

出错了重试;

客户端和服务器统一使用 TCP 长连接;

客户端和服务器统一使用 TCP 短连接。

首先是出错了重试:这种方案可以简单防止“Connection reset”错误,然后如果服务不是“幂等”的则不能使用该方法;比如提交订单操作就不是幂等的,如果使用重试则可能造成重复提单。

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

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

正常情况 tcp 四层握手关闭连接,rst 基本都是异常情况,整理如下:

  1. 使用 ping 可以看到丢包情况
  2. GFW
  3. 对方端口未打开,发生在连接建立。 如果对方 sync_backlog 满了的话,sync 简单被丢弃,表现为超时,而不会 rst
  4. close Socket 时 recv buffer 不为空。 例如,客户端发了两个请求,服务器只从 buffer 读取第一个请求处理完就关闭连接,tcp 层认为数据没有正确提交到应用,使用 rst 关闭连接。
  5. 移动链路。 移动网络下,国内是有 5 分钟后就回收信令,也就是 IM 产品,如果心跳>5 分钟后服务器再给客户端发消息,就会收到 rst。也要查移动网络下 IM 保持<5min 心跳。
  6. 负载等设备。 负载设备需要维护连接转发策略,长时间无流量,连接也会被清除,而且很多都不告诉两层机器,新的包过来时才通告 rst。 Apple push 服务也有这个问题,而且是不可预期的偶发性连接被 rst;rst 前第一个消息 write 是成功的,而第二条写才会告诉你连接被重置, 曾经被它折腾没辙,因此打开每 2 秒一次 tcp keepalive,固定 5 分钟 tcp 连接回收,而且发现连接出错时,重发之前 10s 内消息。
  7. SO_LINGER 应用强制使用 rst 关闭 该选项会直接丢弃未发送完毕的 send buffer,可能造成业务错误,慎用; 当然内网服务间 http client 在收到应该时主动关闭,使用改选项,会节省资源。 好像曾经测试过 haproxy 某种配置下,会使用 rst 关闭连接,少了网络交互而且没有 TIME_WAIT 问题
  8. 超过超时重传次数、网络暂时不可达
  9. TIME_WAIT 状态。 tw_recycle = 1 时,sync timestamps 比上次小时,会被 rst
  10. 设置 connect_timeout。 应用设置了连接超时,sync 未完成时超时了,会发送 rst 终止连接。
  11. 非正常包。 连接已经关闭,seq 不正确等
  12. keepalive 超时。 公网服务 tcp keepalive 最好别打开;移动网络下会增加网络负担,切容易掉线;非移动网络核心 ISP 设备也不一定都支持 keepalive,曾经也发现过广州那边有个核心节点就不支持。
  13. 数据错误,不是按照既定序列号发送数据 11.在一个已关闭的 socket 上接收数据 12.服务器关闭或异常终止了连接,由于网络问题,客户端没有收到服务器的关闭请求,这称为 TCP 半打开连接。就算重启服务器,也没有连接信息。如果客户端向提其写入数据,对方就会回应一个 RST 报文段。

三次握手与四次挥手异常处理

参考:

大家好,我是小林。

之前收到个读者的问题,对于 TCP 三次握手和四次挥手的一些疑问:

  • 第一次握手,如果客户端发送的 SYN 一直都传不到被服务器,那么客户端是一直重发 SYN 到永久吗?客户端停止重发 SYN 的时机是什么?
  • 第三次握手,如果服务器永远不会收到 ACK,服务器就永远都留在 Syn-Recv 状态了吗?退出此状态的时机是什么?
  • 第三次挥手,如果客户端永远收不到 FIN,ACK,客户端永远停留在 Fin-Wait-2 状态了吗?退出此状态时机是什么时候呢?
  • 第四次挥手,如果服务器永远收不到 ACK,服务器永远停留在 Last-Ack 状态了吗?退出此状态的时机是什么呢?
  • 如果客户端 在 2SML 内依旧没收到 FIN,ACK,会关闭链接吗?服务器那边怎么办呢,是怎么关闭链接的呢?

可以看到,这些问题都是关于 TCP 是如何处理这些异常场景的,我们在学 TCP 连接建立和断开的时候,总是以为这些过程能如期完成。

可惜理想很丰满,现实很骨感,事实预料呀

TCP 当然不傻,对以上这些异常场景都是有做处理的。

当时也用做实验的方式带大家看 TCP 是如何处理这些异常场景的。

如果新读者还不知道小林的图解网络 PDF,可以到我公众号后台回复「图解」获取就行。

不过,当时这些知识分散到了多个章节,这次就针对读者问的这一系列问题,来详细说说 TCP 是怎么处理这些异常的?

这些异常场景共分为两大类,第一类是 TCP 三次握手期间的异常,第二类是 TCP 四次挥手期间的异常。

TCP 三次握手期间的异常

我们先来看看 TCP 三次握手是怎样的。

第一次握手丢失了,会发生什么?

当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。

在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发超时重传机制。

不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。

当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?

在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。

通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍

当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。

所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。

第二次握手丢失了,会发生什么?

当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。

第二次握手的 SYN-ACK 报文其实有两个目的 :

  • 第二次握手里的 ACK, 是对第一次握手的确认报文;
  • 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;

所以,如果第二次握手丢了,就会发送比较有意思的事情,具体会怎么样呢?

因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文

然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。

那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文

在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

因此,当第二次握手丢失了,客户端和服务端都会重传:

  • 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定。;
  • 服务端会重传 SYN-AKC 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries   内核参数决定。

第三次握手丢失了,会发生什么?

客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。

因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。

注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文

TCP 四次挥手期间的异常

我们再来看看 TCP 四次挥手的过程。

第一次挥手丢失了,会发生什么?

当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。

正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2 状态。

如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。

当客户端重传 FIN 报文的次数超过 tcp_orphan_retries   后,就不再发送 FIN 报文,直接进入到 close 状态。

第二次挥手丢失了,会发生什么?

当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。

在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。

这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT2 状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。

对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2 状态不可以持续太久,而  tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。

这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭。

第三次挥手丢失了,会发生什么?

当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。

此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。

如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。

第四次挥手丢失了,会发生什么?

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。

在 Linux 系统,TIME_WAIT 状态会持续 60 秒后才会进入关闭状态。

然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。


是吧,TCP 聪明着很!


最后修改 April 11, 2024: wireshark, tcp (8b88d894)