[整理] Socket通信的异常以及处理
一、TCP 握手的异常情况
1. 客户端第一个「SYN」包丢了。
如果客户端第一个「SYN」包丢了,也就是服务端根本就不知道客户端曾经发过包,那么处理流程主要在客户端。
而在 TCP 协议中,某端的一组「请求-应答」中,在一定时间范围内,只要没有收到应答的「ACK」包,无论是请求包对方没有收到,还是对方的应答包自己没有收到,均认为是丢包了,都会触发超时重传机制。
所以此时会进入重传「SYN」包。根据《TCP/IP详解卷Ⅰ:协议》中的描述,此时会尝试三次,间隔时间分别是 5.8s、24s、48s,三次时间大约是 76s 左右,而大多数伯克利系统将建立一个新连接的最长时间,限制为 75s。
也就是说三次握手第一个「SYN」包丢了,会重传,总的尝试时间是 75s。
2. 服务端收到「SYN」并回复的「SYN,ACK」包丢了。
此时服务端已经收到了数据包并回复,如果这个回复的「SYN,ACK」包丢了,站在客户端的角度,会认为是最开始的那个「SYN」丢了,那么就继续重传,就是我们前面说的「错误 1」 的流程。
而对服务端而言,如果发送的「SYN,ACK」包丢了,在超时时间内没有收到客户端发来的「ACK」包,也会触发重传,此时服务端处于 SYN_RCVD 状态,会依次等待 3s、6s、12s 后,重新发送「SYN,ACK」包。
而这个「SYN,ACK」包的重传次数,不同的操作系统下有不同的配置,例如在 Linux 下可以通过 tcp_synack_retries 进行配置,默认值为 5。如果这个重试次数内,仍未收到「ACK」应答包,那么服务端会自动关闭这个连接。
同时由于客户端在没有收到「SYN,ACK」时,也会进行重传,当客户端重传的「SYN」被收到后,服务端会立即重新发送「SYN,ACK」包。
3. 客户端最后一次回复「SYN,ACK」的「ACK」包丢了。
如果最后一个「ACK」包丢了,服务端因为收不到「ACK」会走重传机制,而客户端此时进入 ESTABLISHED 状态。
多数情况下,客户端进入 ESTABLISHED 状态后,则认为连接已建立,会立即发送数据。但是服务端因为没有收到最后一个「ACK」包,依然处于 SYN-RCVD 状态。
那么这里的关键,就在于服务端在处于 SYN-RCVD 状态下,收到客户端的数据包后如何处理?
这也是比较有争议的地方,有些资料里会写到当服务端处于 SYN-RCVD 状态下,收到客户端的数据包后,会直接回复 RTS 包响应,表示服务端错误,并进入 CLOSE 状态。
但是这样的设定有些过于严格,试想一下,服务端还在通过三次握手阶段确定对方是否真实存在,此时对方的数据已经发来了,那肯定是存在的。所以当服务端处于 SYN-RCVD 状态下时,接收到客户端真实发送来的数据包时,会认为连接已建立,并进入 ESTABLISHED 状态。
那么实际情况为什么会这样呢?
当客户端在 ESTABLISHED 状态下,开始发送数据包时,会携带上一个「ACK」的确认序号,所以哪怕客户端响应的「ACK」包丢了,服务端在收到这个数据包时,能够通过包内 ACK 的确认序号,正常进入 ESTABLISHED 状态。
4. 客户端故意不发最后一次「SYN」包。
前面一直在说正常的异常逻辑,双方都还算友善,按规矩做事,出现异常主要也是因为网络等客观问题,接下来说一个恶意的情况。
如果客户端是恶意的,在发送「SYN」包后,并收到「SYN,ACK」后就不回复了,那么服务端此时处于一种半连接的状态,虽然服务端会通过 tcp_synack_retries 配置重试的次数,不会无限等待下去,但是这也是有一个时间周期的。
如果短时间内存在大量的这种恶意连接,对服务端来说压力就会很大,这就是所谓的 SYN FLOOD 攻击。这就属于安全攻防的范畴了。
二、TCP 四次挥手的异常情况
1. 断开连接的 FIN 包丢了。
我们前面一直强调过,如果一个包发出去,在一定时间内,只要没有收到对端的「ACK」回复,均认为这个包丢了,会触发超时重传机制。而不会关心到底是自己发的包丢了,还是对方的「ACK」丢了。
所以在这里,如果客户端率先发的「FIN」包丢了,或者没有收到对端的「ACK」回复,则会触发超时重传,直到触发重传的次数,直接关闭连接。
对于服务端而言,如果客户端发来的「FIN」没有收到,就没有任何感知。会在一段时间后,也关闭连接。
2. 服务端第一次回复的 ACK 丢了。
此时因为客户端没有收到「ACK」应答,会尝试重传之前的「FIN」请求,服务端收到后,又会立即再重传「ACK」。
而此时服务端已经进入 CLOSED-WAIT 状态,开始做断开连接前的准备工作。当准备好之后,会回复「FIN,ACK」,注意这个消息是携带了之前「ACK」的响应序号的。
只要这个消息没丢,客户端可以凭借「FIN,ACK」包中的响应序号,直接从 FIN-WAIT-1 状态,进入 TIME-WAIT 状态,开始长达 2MSL 的等待。
附记: 2MSL( Maximum Segment Life) 指两倍的最大存活时间;
- 客户端没有收到自己的「ACK」应答,会超时重传「FIN」;
服务端收到重传的「FIN」,也会再发「ACK」应答; - 客户端收到自己的「ACK」应答,也不会再发任何消息,包括ACK;
无论是1还是2,A都需要等待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:
去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL) = 2MSL
3. 服务端发送的 FIN,ACK 丢了。
服务端在超时后会重传,此时客户端有两种情况,要么处于 FIN-WAIT-2 状态(之前的 ACK 也丢了),会一直等待;要么处于 TIME-WAIT 状态,会等待 2MSL 时间。
也就是说,在一小段时间内客户端还在,客户端在收到服务端发来的「FIN,ACK」包后,也会回复一个「ACK」应答,并做好自己的状态切换。
4. 客户端最后回复的 ACK 丢了。
客户端在回复「ACK」后,会进入 TIME-WAIT 状态,开始长达 2MSL 的等待,服务端因为没有收到「ACK」的回复,会重试一段时间,直到服务端重试超时后主动断开。
或者等待新的客户端接入后,收到服务端重试的「FIN」消息后,回复「RST」消息,在收到「RST」消息后,复位服务端的状态。
5. 客户端收到 ACK 后,服务端跑路了。
客户端在收到「ACK」后,进入了 FIN-WAIT-2 状态,等待服务端发来的「FIN」包,而如果服务端跑路了,这个包永远都等不到。
在 TCP 协议中,是没有对这个状态的处理机制的。但是协议不管,系统来凑,操作系统会接管这个状态,例如在 Linux 下,就可以通过 tcp_fin_timeout 参数,来对这个状态设定一个超时时间。
需要注意的是,当超过 tcp_fin_timeout 的限制后,状态并不是切换到 TIME_WAIT,而是直接进入 CLOSED 状态。
6. 客户端收到 ACK 后,客户端自己跑路了。
客户端收到「ACK」后直接跑路,服务端后续在发送的「FIN,ACK」就没有接收端,也就不会得到回复,会不断的走 TCP 的超时重试的机制,此时服务端处于 LAST-ACK 状态。
那就要分 2 种情况分析:
- 在超过一定时间后,服务端主动断开。
- 收到「RST」后,主动断开连接。
「RST」消息是一种重置消息,表示当前错误了,应该回到初始的状态。如果客户端跑路后有新的客户端接入,会在此发送「SYN」以期望建立连接,此时这个「SYN」将被忽略,并直接回复「FIN,ACK」消息,新客户端在收到「FIN」消息后是不会认的,并且会回复一个「RST」消息。