剖析TCP三握四挥
TCP 协议是 面向连接 的协议,通信前需要先在双端建立 逻辑信道 ,即使是断开连接也不能说断就断,还需双方同意。所以下面我们详细说明 TCP 的连接管理。
在TCP的连接建立过程中一般需要处理下面三个问题
- 要使每一方能够确知对方的存在。
- 要允许双方协商一些参数(如最大报文段长度,最大窗口大小,服务质量等)。
- 能够对传输实体资源(如缓存大小等)进行分配
TCP建立连接最常见的方式就是通过三次握手(three-way handshake) ,连接释放最常见的方式则是 四次挥手(four-way handshake) ,下面我们先介绍这两种最常见的连接管理机制。
三次握手
- 客户端会随机初始化序号(client_ISN),将此序号置于 TCP 首部的
序号
字段中,同时把SYN
标志位置为 1 ,表示SYN
报文。接着把第一个SYN
报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据 ,之后客户端处于 SYN-SENT 状态。 - 服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的序号
字段中,其次把 TCP 首部的确认应答号
字段填入 client_isn + 1 , 接着把SYN
和ACK
标志位置为 1 ,同时写入rwnd
,以告知对方自己的窗口大小。最后把该报文发给客户端,该报文也不包含应用层数据 ,之后服务端处于 SYN-RCVD 状态。 - 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为 1 ,其次确认应答号
字段填入 server_isn + 1 ,最后把报文发送给服务端。这次报文可以携带客户到服务器的数据 ,之后客户端处于 ESTABLISHED 状态。 - 服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。之后双方便可进行通信。
SYN
和SYN+ACK
段不携带数据,所以不消耗序列,ACK
段可携带数据,若携带数据,则消耗序列 ,而 TFO 可以在前两次交换数据,参考此处三次握手之前的连接都称之为 半连接 。
ISN 为什么要随机?
为了防止历史报文被下一个相同四元组的连接接收(主要方面);
四元组唯一标识一个 TCP 连接,当一个 TCP 连接在经历四次挥手关闭时,假如有一个数据包延迟特别大,而这个连接在关闭后又马上以相同的四元组建立起来,那么先前这个连接的 TCP 数据包到达的时候,大概率系列号还落在接收窗内,那么这个数据包就可能会被错误接收。因此 RFC0793 指出 ISN 应该每 4μs 自增 1,从而防止同一个连接的不同实例的数据包混淆。
ISN = M + F(localhost, localport, remotehost, remoteport)
M
是一个计时器,这个计时器每隔 4 微秒加 1。
F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。若四元组相同,则随机数相同。示意图点这里为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;
TCP 系列号欺骗如下图所示:
假设 A 是服务器,B 是拥有特殊权限的客户端,C 是攻击者。
- 第一条消息 C 冒充 B 来向服务器 A 请求建立连接,此时 C 发出的数据包的 IP 地址会填写成 B 的;
第二条消息假设 A 没有其他手段来验证 B,而仅仅根据 IP 地址判断 C 发过来的建立连接的请求是 B 发过来的,因此向 B 发送
SYN+ACK
,此时假设 B 被 C 进行了 DOS 攻击或者处于其他异常状态而不能响应第二条消息(如果 B 处于正常状态会响应一个RST
包来重启 TCP 连接);第三条消息假如 C 能正确的猜测出 A 在第二条消息中的 ISN,就可以冒充 B 和 A 完成三次握手的过程,让 A 误以为和 B 建立了连接。接下来 C 就可以冒充 B 给 A 发送一些危险数据或者指令而实现攻击。
另外提一句,一些朋友做实验用 wireshark 抓包时会发现 SYN 报文的 ISN 为 0,如下:
打开条目后就可以看到,这其实是相对序列号,wireshark 为了方便而帮我们生成的:
为什么是三次握手?不是两次、四次?
-
三次握手才可以阻止重复历史连接的初始化(主要原因)
我们考虑一个场景,客户端先发送了 SYN(seq = 90) 报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100) 报文。
客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:
- 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
- 那么此时服务端就会回一个
SYN + ACK
报文给客户端; - 客户端收到后可以根据自身的上下文,判断这是一个历史连接,那么客户端就会发送
RST
报文给服务端,表示中止这一次连接。 - 收到新 SYN 报文后,才重新建立起新连接。
而两次握手则会直接建立历史连接,服务器可能直接发送数据,造成资源的浪费:
-
三次握手才可以同步双方的初始序列号
本质而言,TCP 握手握的是通信双方数据原点的序列号 ,通过同步双方初始序列号来实现 双向可靠传输 。
1
2
3// A --SYN=9 --> B
// A <-SYN=4 ACK=10 -- B
// A -- ACK=5 --> B两次握手只能实现 A 方和 B 方就 A 方的同步,也就是说,两次握手只能保证 A 到 B 的数据传输是可靠的,反向则无法保证。这就相当于,A -> B是 TCP,而B -> A是 UDP(类比不是很准确)。如果要保证 B 到 A 的数据传输也可靠,则还需要第三次握手。
-
防止资源浪费
在 SYN 泛洪攻击下,两次握手就建立了连接,而后服务器便向对端发送数据,然而对方压根不会回应,白白造成资源的浪费。
不使用「两次握手」和「四次握手」的原因:
- 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
四次挥手
四次握手是在半关闭场景下进行的断连接操作,还有另一种断开操作只需要三次挥手。
- 客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态。 - 服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSED_WAIT
状态。 - 客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态。 - 等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。 - 客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态 - 服务器收到了
ACK
应答报文后,就进入了CLOSED
状态,至此服务端已经完成连接的关闭。 - 客户端在经过
2MSL
一段时间后,自动进入CLOSED
状态,至此客户端也完成连接的关闭。主动关闭连接的,才有 TIME_WAIT 状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
半关连接 :TCP 的半关连接是指 TCP 连接只有一方发送了
FIN
,另一方没有发出FIN
包,仍然可以在一个方向上正常发送数据 。这种场景并不常见,一般来说调用 shutdown() 接口时候就会进入半关闭状态,调用常规的 close() 一般是期待完整的双向关闭这个 TCP 连接。shutdown() 接口相当指示程序,本端已经没有数据待发送,所以发送一个FIN
到对端,但是仍然可以从对端接收数据,直到对端发送一个FIN
指示关闭连接为止,详见后文。同时关闭参考此处
断开连接为什么需要四次挥手?
因为 TCP 是 全双工协议 ,双方都可以接收和发送数据,所以断开连接时,也需要服务端和客服端都确定对方将不再发送数据。
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送 ,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
四次挥手可以变成三次吗?
可以。实际上,三次挥手比四次挥手更为常见。 以下情形会转为三次挥手:
-
当接收端收到
FIN
报文时没有需要发送的数据,且开启了延迟确认,则会将ACK
(第二次)和FIN
(第三次)合并发送。 -
当接收端收到
FIN
报文时只有少于 MMS 的少量数据,且开启了延迟确认,则会将ACK
、FIN
报文和数据合并发送。 -
当发送方调用 close() 时,表明自己不再接收和发送数据,之后接收方如果还发送数据,发送方内核会发送 RST 报文强制释放连接,所以此方式比较粗暴,勉强算得上三次挥手。
FIN 报文一定得调用关闭连接的函数,才会发送吗?
不一定。如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手。
为什么需要设置 TIME_WAIT ?
-
防止历史连接中的数据,被后面相同四元组的连接错误的接收;
当序列号发生回绕时,就没法直接通过序列号来判断新老报文了。这时,一个历史报文可能刚好落在在同一四元组的新连接下的窗口中,此时窗口就会接收历史报文,从而引起数据错乱。于是,最直接的办法就是 等待网络中的所有包消失 ,所以需要设置
TIME_WAIT
。可以通过 时间戳 的方式判断新老报文。详细内容会在其他文章中讲解。
-
保证被动关闭连接的一方,能被正确的关闭;
假设客户端收到服务器的 FIN 后不等待 TIME_WAIT 而直接关闭连接,那么如果最后客户回复的 ACK 丢失,则服务器会超时重发 FIN,此时由于客户端的客户状态已经丢失(因为连接已关闭),所以它将直接回复 RST,从而被服务器解释为异常终止。
为什么 TIME_WAIT 等待的时间是 2MSL?
因为 2MSL 可以保证网络上所有的历史报文全部消失 ,参见下图:
假如现在 A 发送 ACK 后,最坏情况下,这个 ACK 在 1MSL 时到达 B;此时 B 在收到这个 ACK 的前一刹那,一直在重传 FIN,这个 FIN 最坏会在 1MSL 时间内消失。因此从 A 发送 ACK 的那一刹那开始,等待 2MSL 可以保证 A 发送的最后一个 ACK,和 B 发送的最后一个 FIN 都在网络中消失。附带也能够保证上条问题第二点。详细参见:知乎文章 。
MSL
应大于 IP 协议TTL
换算的时间,RFC793 建议MSL
设置为 2 分钟,Linux 遵循伯克利习惯设置为 30 s。
握手和挥手的异常处理
参考本系列另一篇文章:三握四挥的异常处理