TCP 的「三握四挥」
TCP 是「有状态」的传输协议,相比较于 UDP, TCP 在真正的数据传输之前,需要通讯双方进行「三次握手」建立连接之后才能进行数据传输;数据传输完成,需要经过「四次挥手」连接才完成释放;有没有思考过为什么 TCP 要建立连接?为什么“握手”是三次、而“挥手”而是四次?
1. 为什么需要建立连接
无论是 TCP/IP 四层分层模型,还是 OSI 七层分层模型,TCP 都是处于「传输层」的协议,「网络层」的 IP 是不可靠的,其并不能保证数据包一定能够成功被接收方所接收;数据包在网络传输过程中,有可能丢失;在 TCP 是如何实现可靠传输的 这篇文章中,我们知道TCP 通过引入序号机制、ACK 应答机制、滑动窗口和拥塞控制能够做到在错综复杂的网络中实现「可靠」传输;通过「建立连接」的过程,能够明确以下几种场景:
- 明确通信双方所在的网络环境是可以实现通信的,并不存在通信双方网络不通等情况;
- 明确通信双方的「起始序号」,TCP 通过序号机制来保证不重复接收数据、不丢失数据、有序地接收数据交付给到「应用层」;
- 明确通信双方初始「窗口」大小,避免传输一开始就大量的传输数据,超出接收方的处理速度,这些超出的数据报文会被丢弃,造成资源浪费;
可以看到,建立连接的过程,本质上还是在确认实现「可靠」传输的基本要素:起始序号、滑动窗口大小
2. 建立连接的过程
- 【第一次握手】客户端「随机生成」起始序号 X,确认窗口大小为 A,发送 SYN 控制报文给服务端,客户端进入
SYNC_SENT
阶段,等待服务端返回 ACK 报文; - 【第二次握手】服务端成功收到 SYN 控制报文以后,ACK 序号为 X+1,同时「随机生成」起始序号为 Y,确认窗口大小为 B,发送 ACK 控制报文给到客户端,服务端进入
SYNC_RCVD
阶段,等待客户端返回 ACK 报文; - 【第三次握手】客户端成功收到 ACK 控制报文以后,发送 ACK 控制报文,ACK 序号为 Y+1, 对于客户端来说连接已成功建立,进入
ESTABLISHED
阶段,已经可以开始数据传输了; - 服务端成功收到 ACK 控制报文以后,对于服务端来说连接也已经成功建立,进入
ESTABLISHED
阶段,可以开始数据传输了。
其中,起始序号是基于时钟生成的「时间值」+ 「TCP 四元组」经过 Hash 算法计算生成的,每一个时刻的起始序号都不一样~
2.1 为什么是三次「握手」?
首先,为什么建立连接的过程是三次,不是两次或者四次、五次?
我们上面提到建立连接的过程,实际上是确认通信双方的「起始序号」、「窗口大小」、「网络环境可用」
A. 网络环境可用
对于通信双方来说,最低三次「握手」才能够确认双方都能够接收到消息:
- 对于客户端来说,发出去的 SYN 控制报文,只有成功接收到服务端的 ACK 控制报文,才能确认本身能够成功发送报文、能够成功接收报文,即「接发」能力正常;
- 对于服务端来说,只有收到来自客户端的 ACK 报文,才能确认本身能够成功发送报文、能够成功接收报文,即「接发」能力正常;
能确认双方「接发」能力正常的最低发送次数为三次,两次只能确认一方的「接发」能力正常;当然四次、五次也可以,只是「没有必要」。
B. 确认「起始序号」、「窗口大小」
起始序号和窗口大小本质上是一样的,属于数据传输的起始数据;
无论是对于客户端来说、还是对服务端来说,都只有接收到了来自服务端的 ACK 控制报文,才能确认自己发送的「起始序号」成功被对方感知。
2.2 「握手」过程出现异常
A. 第一次握手异常
我们知道 TCP 有「超时重传」机制,在客户端发送完 SYN 控制报文想要建立连接时,如果因为「网络原因」,在规定的时间内没有收到相应的 ACK 报文,会触发重传机制,重新发送 SYN 控制报文;这种情况下,发送了两个 SYN 控制报文;假设为 SYN 1 、SYN 2,可能会有以下几种情况:
只有 SYN 1 或者 SYN 2 到达服务端
如果只有 SYN 1/ SYN 2 成功抵达服务端,则服务端只会处理这个最新的 SYN 报文,正常进行三次握手建立连接SYN 1 和 SYN 2 都到达服务端
服务端会为每个 SYN 报文创建一个新的连接记录半连接队列,如果两个 SYN 报文都到达了服务端,因为这两个 SYN 来自同一源 IP 地址和端口,且目标相同的 IP 地址和端口,所以它们实际上是试图建立同一个连接;服务端能够根据半连接队列识别到两个 SYN 是重复的,会从中选择一个 SYN 报文进行处理。SYN 1 最终也到达服务端
如果最初的 SYN 1在网络中滞留了一段时间后终于到达服务端,而此时服务端已经通过SYN 2与客户端建立了连接,服务端能够根据全连接队列识别到这是一个重复的SYN,并将其视为无效,并且返回一个RST 报文给客户端,指示该 SYN 不再有效。
上面提到了半连接队列和全连接队列,这两个是什么东西?
半连接队列
- 当客户端发起第一次握手请求时(即 SYN 报文),服务端会将此次握手的相关信息存储到一个队列中,这个队列就是「半连接队列」
- 相关信息中就包含了 TCP 四元组: 目的 IP 地址、源 IP 地址、目的端口、源端口
全连接队列
- 当服务端收到来自客户端的第三次握手(即 ACK 报文),服务端会将此次握手的相关信息存储到一个队列中,这个队列就是「全连接队列」
SYN 攻击
这两个队列是维护在服务端中,并且队列都存在最大数量上限,服务端不可能支撑无限多的 TCP 连接,从而引申出 SYN 攻击:攻击方只发 SYN 报文,不回应 ACK,对于服务端来说,收到 SYN 报文之后,会将其加入到半连接队列中,并且会返回携带 SYN 的 ACK 报文给客户端,服务端无法分辨是否为攻击,只能傻傻等待;攻击方只要大量的发送 SYN 报文,服务端的半连接队列很容易就被堆满,对于正常的 SYN 报文,服务端半连接队列已经无法容纳了~
那怎么来处理这种攻击呢?留给你思考思考hhh
B. 第二次握手异常
对于成功到达服务端的 SYN 报文,服务端会将其记录在半连接队列中,随后返回携带 SYN 的 ACK 报文给到客户端,如果这个报文在网络中丢失了:
对于客户端来说,因为没有收到 ACK 报文,在超过 RTO 时间之后,会触发「超时重传」,重发 SYN 报文,如下图:
对于服务端来说,因为没有收到来自客户端的 ACK 报文,在超过 RTO 时间之后,同样会触发「超时重传」,在重传超过「一定次数」之后,服务端会认为握手失败,放弃此次连接请求;如下图:
C. 第三次握手异常
客户端在回复完 ACK 报文之后,就进入 ESTABLISHED
阶段了,认为已经成功建立起连接,开始发送数据了,假如回复的 ACK 报文丢失在网络中,会造成服务端并不知道自己的第二次握手是否成功抵达;超过「RTO」时间还是没有收到 ACK 报文就会「超时重传」。
服务端超时重传
服务端在超过 RTO 时间之后还是没有收到 ACK 包,就会触发「超时重传」。
客户端发送数据
客户端发送第三次握手的 ACK 报文之后,就会进入 ESTABLISHED
阶段,开始发送数据,如果后续的数据报文能够成功被服务端接收,那服务端就不会继续依赖第三次握手的 ACK 报文,自动切换状态到 ESTABLISHED
阶段
3. 释放连接的过程
上面梳理完了建立连接过程中的「三握」,接下来对释放连接的「四挥」进行梳理;
释放连接是可以由通讯双方任意一方发起,即可以是客户端,也可以是服务端发起,下面我以客户端主动发起释放为例子进行梳理:
- 【第一次挥手】客户端主动发起释放,发送 FIN 报文,从
ESTABLISHED
阶段进入FIN_WAIT_1
阶段,等待 ACK 报文;此时客户端仅接收数据,不再发送数据; - 【第二次挥手】服务端接收到来自客户端的 FIN 报文之后,回复 ACK 报文,从
ESTABLISHED
阶段进入CLOSE_WAIT
阶段,此时服务端仍可以继续发送和接收数据; - 【第三次挥手】服务端发送 FIN 报文,从
CLOSE_WAIT
阶段进入LAST_ACK
阶段,等待 ACK 报文;此时服务端仅接收数据,不再发送数据; - 【第四次挥手】客户端接收到来自服务端的 FIN 报文之后,回复 ACK 报文,从
FIN_WAIT_2
阶段进入TIME_WAIT
阶段,此时客户端已经不再接收和发送数据;等待 2MSL 之后则进入CLOSE
阶段;服务端接收到 ACK 报文后进入CLOSE
阶段。
3.1 为什么是四次「挥手」?
释放连接可以由任意一方发起,因为 TCP 是全双工通信,客户端可以发送数据,服务端也可以发送数据;客户端发送 FIN 报文,仅仅意味着服务端知道客户端不再继续发送数据了,不意味着服务端不可以继续发送数据了;所以通信双方必定存在通知对方不再发送数据的信号(即 FIN 报文),以及确保对方已经明确收到 FIN 报文的 ACK 报文,即最少 4 个控制报文。(对应四次挥手)
3.2 「挥手」过程出现异常
相应的,网络环境从不保证每一次报文都能顺利到达目的地,每一次「挥手」都有可能无法送达
A. 第一次挥手异常
对于客户端来说,超过 RTO 时间之后,还是没有收到 ACK 报文,则会触发「超时重传」,重传超过一定次数之后,会直接进入 CLOSE
阶段,相当于实在无法通知服务端要释放连接了;
对于服务端来说,并没有收到来自客户端的 FIN 报文,认为还是正常的连接,有可能继续进行发送数据:此时客户端收到来自服务端的数据报文,识别到连接已经断开,会回复 RST 控制报文,通知服务端释放资源。
B. 第二次挥手异常
即客户端收不到来自服务端的 ACK 报文,超过 RTO 时间之后还是收不到 ACK 报文,客户端会触发「超时重传」;重传超过一定次数之后,客户端还是收不到 ACK 报文,会直接进入 CLOSE
阶段;跟第一次挥手异常类似。
C. 第三次挥手异常
即服务端的 FIN 报文无法成功送达客户端,客户端无法响应 ACK 报文,超过 RTO 时间之后还是收不到 ACK 报文,服务端会触发「超时重传」,如果重传一直失败:
对于客户端来说,在收到 ACK 报文之后,就进入 FIN_WAIT_2
阶段,等待服务端的 FIN 报文,如果超时「一定时间」还是没有收到来自服务端的 FIN 报文,客户端就不再等待了,直接进入 CLOSE
阶段,主动释放连接;
对于服务端来说:在重试超过「一定次数」之后,还是没能收到 ACK,因为已经是第三次握手,也会进入 CLOSE
阶段,释放连接;
D. 第四次挥手异常
客户端在成功收到来自服务端的 FIN 报文时,则从 FIN_WAIT_2
阶段进入到 TIME_WAIT
阶段,在等待 2MSL 之后就会进入 CLOSE
阶段;假设第四次挥手失败,对于服务端来说,没有收到 ACK 报文,会触发「超时重传」机制;重传一定次数之后,客户端还是收不到 ACK 报文,就会直接进入CLOSE
阶段;
E. 异常小结
可以看到四次挥手,发送 FIN 报文端如果没有收到相应的 ACK 报文,都会触发重传机制,「尽可能」让自己的 FIN 报文抵达,如果经过重试之后还是无法到达,也不会「死等」,会进入到 CLOSE
阶段,因为每一个 TCP 连接都是需要服务端/客户端消耗内核资源的(CPU、内存等)
3.3 为什么要等待 2MSL
我们可以看到,客户端在成功接收到第三次挥手之后会进入 TIME_WAIT
阶段,在等待2MSL之后才会真正的进入CLOSE
阶段,为什么是 2MSL 呢? 1MSL 可以吗?
MSL,即 Maximum Segment Lifetime
,最大报文生存时间,它是指报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
A. 避免历史报文出现在新链接中
如果不等待 2MSL 才进入 CLOSE
阶段,有可能因为网络原因在网络中滞留的报文延迟到达了客户端,而此时又刚好存在使用相同「TCP 四元组」建立起的连接,就会接收到这个「历史报文」,造成错乱。
B. 「尽量」让服务端也正常关闭
如果第四次挥手在网络中丢失,在超过 RTO 之后,服务端会「超时重传」,如果不等待 2MSL 就进入 CLOSE
阶段,那即使客户端收到重传的「第三次挥手」,也无法正常进行第四次挥手,服务端没有办法走「正常」的关闭流程。
C. 1 MSL 可以吗?
实际上,重要的是客户端在进行第四次挥手之后,不要立即进入 CLOSE
阶段,给服务端一次机会;等待 1 MSL 理论上已经能够覆盖绝大部分因网络超时导致的异常,2 MSL 则是为了能够更加保险;
D. 3、4、5 MSL可以吗?
等待时间太长也存在弊端,处于 TIME_WAIT
阶段的客户端,此时无法接受/发送数据,但是内核的资源仍然占用着,也无法发起新的 TCP 连接,CPU、内存、端口都被占用着;如果短时间存在大量连接进入到 TIME_WAIT
阶段,上述影响则会更加明显,所以合理的等待时间也是非常重要的~
4. 最后
写这篇文章,主要还是想要对 TCP 建立连接、释放连接的过程有一个清晰的认识,文中那么多阶段,我想在过一段时间之后肯定会忘记,重要的是梳理这个过程时产生的一些思考~ 继续加油