为什么是三次握手而不是两次/四次?
这是 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 攻击
全连接队列满:新连接被丢弃 → 客户端连接超时实际案例:三次握手失败
场景一:服务端端口未监听
$ 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(复位),直接拒绝连接。
场景二:网络不通
$ 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?它是如何解决半连接队列耗尽问题的?
