前置知识
首先来看看 TCP 的控制位和状态机,这是理解 TCP 三次握手的基础。
TCP 报文控制位
TCP 报文头部中的控制位用于控制 TCP 连接的状态,可以指示各种控制信息,如连接建立、终止、重置等。
常见的控制位有 6 个:
- SYN (Synchronize Sequence numbers): 请求建立连接 (三次握手),在连接建立时的初始数据包中设置,表明发送端希望建立连接并同步序列号,TCP 是双向通信,所以建立连接时,双方都要发一个 SYN, 虽然 SYN 报文不能携带数据,但要消耗掉一个序号
- ACK (ACKnowledgment field significant): 确认接收到的数据,ACK 标识设置后,接收端会在确认字段中填入下一个期望接收的序列号,ACK 报文如果不携带数据,则不消耗序号
- FIN (No more data from sender): 请求终止连接 (四次挥手),在数据发送完毕后的数据包中设置,通知接收 (对) 方: 发送 (己) 方已发送完所有数据,TCP 是双向通信,所以关闭连接时,双方都要发一个 FIN, 虽然 FIN 报文不携带数据,但要消耗掉一个序号
- RST (Reset the connection): 重置连接,用于异常或错误的连接终止,当接收端收到 RST 标识时,会立即终止连接,不做任何数据确认,例如 之前的文章中提到的 TIME_WAIT 问题,注意: 生产环境中出现 RST 包往往意味着潜在的问题
- PSH (Push Function): 提示接收端将接收到的数据立即交付应用层,表明数据应当被快速处理,而不需要等待更多的后续数据到达
- URG (Urgent Pointer field significant): 指示数据具有高优先级,接收端应尽快处理
- ECE (ECN-Echo): 指示通信双方在三次握手时,协商两端是否都支持显示拥塞控制
Sequence Number (序号)
TCP 是双向通信,所以单个连接中的双方都可以向对方发送数据,所以需要各自维护自己的 Seq
字段。
Seq
是动态随机生成的,这样可以避免被伪造的报文重置连接 (RST 攻击)。
TCP 提供有序的传输,所以每个数据报文段都要加上一个 Seq
序号字段:
- 当接收端收到乱序的包时,可以根据 Seq 重新排序
- 当接收端收到重复的包时,可以根据 Seq 去重
如图所示,序号增长方式 (重要):
- 数据段 1 起始 Seq 号为1,长度为 1448 (单位: 子节),那么数据段 2 的 Seq 号就等于 1 + 1448 = 1449
- 数据段 2 的长度也是 1448,所以数据段 3 的 Seq 号为 1449 + 1448 = 2897
也就是说,一个 Seq 号的大小是根据上一个数据段的 Seq 号和长度相加而来的。
所以在 TCP 数据传输过程中,任意一方发出的数据段应该是连续的: 后一个包的 Seq 号等于前一个包的 Seq + Len (三次握手和四次挥手除外)。
Len (数据段长度)
需要注意的是: Len
不包括 TCP 头部长度,所以不要认为 Len = 0
的数据包没有意义,TCP 头部本身携带的信息也很多。
ACK (确认号)
接收端告诉发送端自己已经收到了哪些数据段 (Seq
序号)。
- 发送端 发送了
Seq:1 Len:100
的数据到 接收端, 然后 接收端 回复的ACK
就是1 + 100 = 101
, 表示自己收到了 101 之前的所有数据 - 发送端 发送了
Seq:101 Len:50
的数据到 接收端, 然后 接收端 回复的ACK
就是101 + 50 = 151
, 表示自己收到了 151 之前的所有数据
比如甲发送了“Seq:x Len:y”的数据段给乙,那乙回复的确认号就是x+y,这意味着它收到了x+y之前的所有字节。
结论 (重要): 接收端回复的 ACK
号正好等于发送端的下一个 Seq
号,所以我们可以看到 10377 号包的 ACK
正好等于 10378 号包的 Seq
。
如果通信中任意一方没有发送任何数据,那么对方返回的 ACK
号也不会发生变化 (也就是三次握手时的初始值)。
TCP 状态机
下面是经典的 TCP 状态机,每个 TCP 连接从最初建立到最后断开,整个生命周期中的所有状态都囊括其中。
下面是来自维基百科的彩色版本:
三次握手示例
下面是一个典型的三次握手示例图。
- 第一次握手:客户端发起连接请求,设置请求报文控制位
SYN = 1
, 同时初始化一个随机序列号 (ISN)Seq = x
, 发送请求报文SYN
后,客户端进入SYN-SENT
状态 - 第二次握手:服务端收到客户端的连接请求后,初始化一个随机序列号 (ISN)
Seq = y
, 同时设置应答报文控制位SYN = 1
, 确认控制位ACK = x + 1
, 发送应答报文SYN-ACK
后,服务端进入SYN-RECEIVED
状态 - 第三次握手:客户端收到服务端的应答后,设置应答确认控制位
ACK = x + 1
, 发送应答报文ACK
后,客户端进入ESTABLISHED
状态,服务端收到客户端的ACK = y + 1
报文后,进入ESTABLISHED
状态,TCP 连接建立完成
在整个连接过程中,客户端和服务端的状态变化如下图所示:
从图中可以看到,对于 ESTABLISHED (连接已建立)
这个状态,客户端和服务端的感知时间是不一样的:
- 对于客户端来说,两次握手完成后,连接建立完成
- 对于服务端来说,三次握手完成后,连接建立完成
Wireshark 抓包
为了更直观的感受 TCP 的三次握手过程,这里使用 Wireshark 进行抓包:
打开 WireShark 开始抓包,然后在终端执行下列命令:
$ curl -I -H "Connection: close"
切换到 Wireshark 操作界面,使用 tcp
进行包过滤,可以看到如下的 TCP 三次握手过程,其中 192.168.3.68 是本级的局域网 IP 地址。
- 第一次握手:客户端发起连接请求时,使用的
Seq
字段为 3123802190 - 第二次握手:服务端发起应答报文时,使用的
Seq
字段为 1071295171,ACK = x + 1
, 也就是 3123802191 - 第三次握手:客户端发起应答报文时,使用的
ACK = y + 1
, 也就是 1071295172
Tips: Wireshark 默认情况下,显示的是 Seq
的相对值 (从 0 开始),如果想看到客户端和服务端的真实随机 Seq
值,可以在 Wireshark 操作界面的菜单进行如下设置:
Edit > Preferences > Protocols > TCP取消勾选: Relative Sequence numbers
证明 (粗糙版本)
讲完了 TCP 三次握手的理论基础之后,接下来可以分析并证明如下命题:
TCP 建立连接时,至少需要三次握手。
这里我们使用一个非常基础的数学证明方法:反证法,既然至少需要三次握手?那么我们的反证命题如下:
TCP 建立连接时,不需要三次握手。
具体来说,我们假设 TCP 握手次数 N 少于三次就可以建立连接,这里又可以将 N 分为三种范围区间:
下面对三种范围区间分别进行证明。
1. N < 1
当 N < 1
时,意味着双方都不发起第一次握手,此时双方甚至都不知道彼此的存在,更别谈建立连接进行通信了,所以 N < 1
不成立,此时进入下一个命题: N == 1
。
2. N == 1
当 N == 1
时,意味着双方需要一次握手,也就是 发送端 向 接收方 发起连接建立请求 (SYN
),但是请求发出后就没有下文了,所以连接还是无法建立。
因为没有收到接收端的应答报文,所以 发送端 此时无法确认两件事情:
发送 | 接收 | |
---|---|---|
发送端 | ❌ | ❌ |
接收端 | ❌ | ❌ |
- 发送端 (自己) 的发送功能是否正常 (否可以将数据正常发送到接收端)
- 接收端的接收功能是否正常 (包括是否监听了对应的端口、数据缓冲区是否正常等)
因为无法确认这两件事情,所以发送端认为连接还未建立,自然也就不会向接收方继续发送数据了。所以 N == 1
不成立,此时进入下一个命题: N == 2
。
3. N == 2
当 N == 2
时,意味着双方需要两次握手,我们在 N == 1
的基础上继续证明。
在第一次握手后,接收端 收到 发送端 的连接建立请求报文后,会回复一个连接建立应答报文 (SYN-ACK
),但是和 发送端 的报文一样,接收端 的应答发出后就没有下文了,所以连接还是无法建立。
1. 发送端 (自己) 的发送功能是否正常 (否可以将数据正常发送到接收端)
2. 接收端的接收功能是否正常 (包括是否监听了对应的端口、数据缓冲区是否正常等)
对于 N == 1
中存在的两个问题,此时已经得到了确认:
发送 | 接收 | |
---|---|---|
发送端 | ✅ | ❌ |
接收端 | ❌ | ✅ |
- 发送端的发送功能是正常的
- 接收端的接收功能是正常的
因为没有收到 发送端 的 (确认) 应答报文,所以 接收端 此时无法确认两件事情:
- 接收端 (自己) 的发送功能是否正常 (是否可以将数据正常发送到发送端)
- 发送端的接收功能是否正常
原命题证明
通过前文中的反证法,可以证明 TCP 建立连接时,至少需要三次握手。
我们沿着前文中的思路,来看看 N == 3
时,发送端 和 接收端 对应的状态变化。
在第二次握手后,发送端 收到 接收端 的连接建立应答报文后,会回复一个连接建立应答报文 (ACK
)。
1. 接收端 (自己) 的发送功能是否正常 (是否可以将数据正常发送到发送端)
2. 发送端的接收功能是否正常
对于 N == 2
中存在的两个问题,此时已经得到了确认:
发送 | 接收 | |
---|---|---|
发送端 | ✅ | ✅ |
接收端 | ✅ | ✅ |
- 接收端的发送功能是正常的
- 发送端端接收功能是正常的
证明 (正确版本)
前文中通过 (粗糙地) 反证法 + TCP 报文状态 证明了 TCP 建立连接时,至少需要三次握手,除此之外,也可以利用 TCP 序列号 (严谨地) 证明。
为了实现可靠数据传输,TCP 协议的通信双方都必须维护一个序列号 Seq
,用来标识发送出去的数据包中哪些是已经被对方收到的。
三次握手的过程等于: 通信双方相互发送初始序列号,并确认对方已经收到了初始序列号的必要过程。
- 如果只有一次握手,接收端 没有发送初始序列号
- 如果只有两次握手,只有 发送端 的初始序列号可以被确认,接收端 的初始序列号无法得到确认
除此之外,在只有两次握手的情况下,可能还会出现一种异常情况: 延迟包导致的无效连接。
如图所示,某个网络有多条路径,客户端建立连接请求的第一个数据包,正好被传输到一条延迟严重的路径,所以迟迟没有到达服务器。
客户端发送超时后,认为第一个数据包丢失了,于是重新发起请求,第二个请求被传输到正常的路径,所以很快就完成了连接。
对于客户端来说,本次通信过程似乎已经结束了,但是此时它的第一个数据包,延迟到达了服务器。因为服务器并不知道这是一个旧的无效请求,所以按照正常情况回复应答。
如果 TCP 只有两次握手,服务器上此时就建立了一个无效的 (过期的重复) 连接。
但是在 TCP 三次握手的机制下,客户端收到服务器的回复后,发现这个 (已经超时的) 连接不是它想要的,所以就应答一个 RST
数据包,服务器收到 RST
数据后,同时关闭连接。
重点: 三次握手,到底握的是什么?发送方和接收方的初始化 Seq 值 (ISN, Initial Sequence Number), 通过这个值就可以区分当前的本次连接和历史旧连接。
结论
理论上讲 3 次以上,不论握手多少次,都无法确认一个 TCP 连接是 “完全可靠” 的。
但通过 3 次握手,至少可以确认连接是 “基本可用” 的,再增加握手次数,也只不过是提高 “连接可用” 这个结论的可信程度而已。
TCP 3 次握手后,发送端 和 接受端状态变化如下:
- 发送端确认了: 自己发送、接收正常,对方发送、接收正常
- 接收端确认了: 自己发送、接收正常,对方发送、接收正常
发送 | 接收 | |
---|---|---|
发送端 | ✅ | ✅ |
接收端 | ✅ | ✅ |
综上所述,TCP 三次握手也是软件工程中的一个经典的 “Trade-Off” 案例。
扩展阅读
FQA
第一次握手时可以携带应用数据吗?
不行,因为此时连接还没有建立。
是否可以由接收方将应用数据缓存起来,等到三次握手完成,连接建立之后,再由接收方交给上层应用呢?
也不行,这样会放大 TCP SYN Flood
攻击,如果攻击者伪造了大量的携带数据报文,那么接收方就需要大量的内存来临时存储应用数据,最终导致内存耗尽。
第二次握手时可以携带应用数据吗?
第二次握手是接收方向发送方发送数据,虽然可以携带数据,但是没有任何实质意义。
第三次握手时可以携带应用数据吗?
可以。
发送第三次握手之前,发送方此时已经进入 ESTABLISHED
状态,所以只要第三次握手的报文到达接收方,那么接收方的状态也会进入 ESTABLISHED
状态,连接就算建立完成了。
此时接收方将发送方在第三次握手时携带的应用数据,转交给上层应用即可。
那这样就不会引发 TCP SYN Flood
攻击吗?
作为攻击者来说,也是需要考虑攻击成本的,如果在第三次握手携带应用数据,就会建立起正常的 TCP 连接,攻击者同样需要资源来存储建立的连接,对于攻击者来说,这是本末倒置的。如果攻击者是远程操纵 “肉鸡” 进行攻击的话,直接在连接建立完成后,让 “肉鸡” 发送海量应用请求就可以了,没有必要在第三次握手时携带应用数据。
TCP 连接失败抓包
TCP 握手失败一般分两种类型,要么被拒绝,要么丢包。因此在 Wireshark 中可以用两个过滤表达式定位出大多数失败的握手。
1. 表达式 1
(tcp.flags.reset == 1) && (tcp.Seq == 1)
从表面上看,它只是过滤出 Seq 号为1,且含有 Reset 标志的包,似乎与握手无关。
但在 (默认) 启用 Relative Sequence Numbers 的情况下,这往往表示握手请求被对方拒绝了,结果如下图所示。
接下来只需右键选中过滤出的包,再点击 Follow TCP Stream 就可以把失败的全过程显示出来,如下图所示。
此次握手失败的原因是服务器没有在监听 80 端口,所以拒绝了客户端的握手请求。
2. 表达式 2
(tcp.flags.syn == 1) && (tcp.analysis.retransmission)
这个表达式可以过滤出重传的握手请求。
一个握手请求之所以要重传,往往是因为对方没收到,或者对方回复的确认包丢失了。这个重传特征正好用来过滤,结果如下图所示。
接下来只需右键选中过滤出的包,再点击 Follow TCP Stream 就可以把失败的全过程显示出来,如下图所示。
此次握手失败的原因是丢包,所以服务器收不到握手请求。
转自: 为什么 TCP 建立连接需要三次握手 - 蛮荆 https://dbwu.tech/posts/network/why-tcp-does-needs-three-way-handshake/