Skip to content

为什么是三次握手而不是两次/四次?

这是 TCP 协议中最经典的面试问题之一。

很多候选人能背出「三次握手建立连接」,但问到「为什么是三次」,就答不上来了。

今天我们来彻底解决这个问题。

先说结论

三次握手是确认双方「发送能力」和「接收能力」的最小次数。

不是两次(不够),不是四次(浪费)。

从一次通话说起

假设你想打电话给远方的朋友:

场景 A(不握手):你直接对着电话喊「喂喂喂」,但你不知道朋友在不在,也不知道他能不能听到。

场景 B(一次握手):你喊「喂」,朋友回「哎」。现在你知道朋友能听到你,但你不知道你能不能听到朋友。

场景 C(两次握手):你喊「喂」,朋友回「哎,能听到」。现在你知道双方都能听到——这看起来够了吧?

等等,你喊「喂」的时候,朋友不知道你是谁,也不知道你的声音大不大。朋友回「哎」的时候,你也不知道朋友准备好了没有。

场景 D(三次握手)

  • 你喊「喂,我是张三」(第一次)
  • 朋友回「哎,张三你好,我是李四,我准备好了,你准备好了吗」(第二次)
  • 你说「准备好了,开始聊吧」(第三次)

现在双方确认了:

  • 对方能听到我
  • 我能听到对方
  • 双方都知道对方的身份
  • 双方都准备好了

这才是一个可靠的通话开始。

三次握手的本质:确认双方的收发能力

TCP 通信需要确认四种能力:

┌─────────────────────────────────────────────────────────────┐
│                      确认四种能力                            │
├─────────────────────────────────────────────────────────────┤
│  客户端能发送 ──────────────────────> 服务端能接收          │
│  服务端能发送 ──────────────────────> 客户端能接收          │
│  客户端能接收 <────────────────────── 服务端能发送           │
│  服务端能接收 <────────────────────── 客户端能发送           │
└─────────────────────────────────────────────────────────────┘

第一次握手:确认服务端能接收 + 客户端能发送

客户端 ──── SYN ────> 服务端

客户端:我的发送能力 OK(我能发出去)
服务端:客户端想连接我,我的接收能力 OK(我能收到)

第二次握手:确认客户端能接收 + 服务端能发送

客户端 <── SYN+ACK ── 服务端

客户端:服务端同意连接,收到确认,我的接收能力 OK(我能收到)
服务端:客户端确认收到,客户端的发送+接收都 OK

第三次握手:确认服务端能接收 + 客户端能接收

客户端 ──── ACK ────> 服务端

服务端:收到确认,服务端的发送能力 OK,客户端的接收能力 OK

为什么第三次还需要?

因为服务端在第二次握手时,只是「确认了自己能收到客户端的数据」,但还不确定「自己的数据能不能到达客户端」。

第三次握手就是让服务端确认:客户端真的能收到我的数据。

为什么不是两次?

两次握手最多只能确认两种能力:

方案一:只确认「服务端能接收 + 客户端能发送」
客户端 ──── SYN ────> 服务端
客户端 <─── ACK ──── 服务端

问题:客户端不知道自己能不能收到服务端的数据!
      服务端不知道自己能不能发送数据给客户端!

方案二:只确认「客户端能接收 + 服务端能发送」
服务端 <─── SYN ──── 客户端
服务端 ──── ACK ────> 客户端

问题:服务端不知道客户端能不能收到!
      客户端不知道自己的发送是否成功!

两次握手的经典问题:历史连接(Stale SYN)

场景:
1. 客户端发送 SYN A(旧的请求,延迟了很久)
2. 网络拥堵,SYN A 迟迟未到
3. 客户端超时,重发 SYN B(新的请求)
4. SYN B 先到达服务端,连接建立
5. SYN A 终于到达服务端

如果只有两次握手:
服务端收到 SYN A,以为是新的连接请求,建立连接
但客户端已经不需要这个连接了

服务端:连接已建立(但客户端已经不认了)
客户端:什么?我没有发起这个连接!

三次握手可以解决这个问题:服务端收到 SYN A 后,回复 SYN+ACK,但客户端会用序号确认这不是自己想要的连接。

为什么不是四次?

四次握手可以确认所有能力,但第三次和第四次存在冗余:

第一次:客户端发送 SYN                    → 确认服务端能收 + 客户端能发
第二次:服务端发送 SYN+ACK                → 确认客户端能收 + 服务端能发 + 能发
第三次:客户端发送 ACK                     → 确认服务端能收 + 客户端能收
第四次:?(已经确认完了,再发一次是多余的)

第三次握手本身已经完成了所有确认

  • 服务端在第二次握手时确认了「客户端能收我的数据」
  • 第三次握手让服务端确认「客户端真的收到了我的数据」

所以第四次是多余的。

三次握手与序号

初始序号(ISN)

每次建立连接时,双方都要选择一个初始序号(Initial Sequence Number, ISN)

为什么需要 ISN?
1. 区分新旧连接(防止旧数据被误认为是新数据)
2. 可靠传输的基础(确认号基于序号)
3. 安全性(防止攻击者伪造 TCP 连接)

序号怎么选?

不能从 0 开始,因为:

  • 同一个连接可能断开后快速重新建立
  • 如果旧连接的数据延迟到达,可能被新连接错误接收

正确做法是随机选择

旧连接 ISN: 1000
新连接 ISN: 1000000  (随机数,完全不同)

旧连接的数据(序号 1000-1100)不会和新连接的数据混淆

握手过程中的序号

客户端                                      服务端
   │                                            │
   │  ─── SYN seq=x ────────────────────────> │
   │                                            │
   │  <── SYN+ACK seq=y, ack=x+1 ─────────── │
   │                                            │
   │  ─── ACK seq=x+1, ack=y+1 ────────────> │
   │                                            │
   │  双方都已确认对方的 ISN,连接建立           │
  • 第一次握手:客户端发送自己的 ISN(x)
  • 第二次握手:服务端发送自己的 ISN(y),并确认收到 x(ack=x+1)
  • 第三次握手:客户端确认收到 y(ack=y+1)

三次握手与半队列

半连接队列(SYN Queue)

收到第一次握手后,连接进入半连接队列:

┌────────────────────────────────────────────────────────────┐
│ 服务端                                                    │
│                                                          │
│  收到 SYN ──> 进入半连接队列 ──> 发送 SYN+ACK            │
│                                                          │
│  等待最后的 ACK(三次握手完成)                            │
│                                                          │
└────────────────────────────────────────────────────────────┘

全连接队列(Accept Queue)

三次握手完成后,连接进入全连接队列,等待应用层 accept:

┌────────────────────────────────────────────────────────────┐
│ 服务端                                                    │
│                                                          │
│  三次握手完成 ──> 进入全连接队列 ──> 应用层 accept()       │
│                                                          │
│  accept() 从队列取出连接,交给应用程序处理                 │
│                                                          │
└────────────────────────────────────────────────────────────┘

队列满的影响

半连接队列满:新的 SYN 被丢弃或延迟 → SYN Flood 攻击
全连接队列满:新连接被丢弃 → 客户端连接超时

实际案例:三次握手失败

场景一:服务端端口未监听

bash
$ netstat -an | grep 8080
# 没有输出,说明没有监听

$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
telnet: Unable to connect to remote host: Connection refused

服务端回复 RST(复位),直接拒绝连接。

场景二:网络不通

bash
$ ping 192.168.1.100
PING 192.168.1.100: 56 data bytes
Request timeout for icmp_seq 0

网络层 ICMP 不可达,三次握手根本不会发生。

场景三:防火墙拦截

客户端 ──── SYN ────> 防火墙 ────> 服务端

                   SYN 被丢弃

防火墙只开放了部分端口,外网访问时 SYN 被丢弃。

面试追问方向

  • 为什么三次握手是确认双方收发能力的最小次数?
  • 两次握手有什么问题?什么是历史连接问题?
  • 四次握手为什么是多余的?
  • TCP 为什么要随机选择初始序号?
  • 什么是半连接队列和全连接队列?
  • SYN Flood 攻击的原理是什么?如何防御?
  • 如果第三次握手丢失了,会发生什么?
  • 什么是 SYN Cookie?它是如何解决半连接队列耗尽问题的?

基于 VitePress 构建