深入理解 TCP 三次握手
| 技术TCP 三次握手是建立可靠连接的基础。本文从握手过程、序列号机制、安全性等角度,深入分析为什么 TCP 需要三次握手而不是两次,以及围绕这个过程的各种边界情况和攻击手段。
三次握手过程
客户端 服务端
| |
| ① SYN (seq=x) |
| ──────────────────────────────────→|
| |
| ② SYN+ACK (seq=y, ack=x+1) |
|←────────────────────────────────── |
| |
| ③ ACK (seq=x+1, ack=y+1) |
| ──────────────────────────────────→|
| |
| 连接建立完成 |
三步各自的作用:
- SYN — 客户端告诉服务端:“我要建立连接,我的初始序列号是 x”
- SYN+ACK — 服务端回应:“收到,我同意,我的初始序列号是 y,确认你的 x+1”
- ACK — 客户端确认:“收到你的序列号 y,确认 y+1”
为什么不能两次握手?
核心答题点只有一个:
两次握手下,服务端无法确认客户端是否真的收到了自己的回复(SYN+ACK),就单方面认为连接已建立。
这导致一个具体问题:
网络中滞留的历史 SYN 报文到达服务端时,服务端会误建连接、白白分配资源,而客户端根本不会用这个连接。
两次握手导致错误建连的例子
假设客户端 C 向服务端 S 建立连接:
① C → S:SYN (seq=100) — 第一次连接请求。这个报文因为网络拥塞,滞留在网络中,迟迟没到达 S。
② C 超时重传 → S:SYN (seq=200) — C 没收到回复,重新发一个 SYN。这次网络正常,报文很快到达 S。
③ S → C:SYN+ACK (seq=300, ack=201) — S 回应第二次请求。如果是两次握手,S 此时已经认为连接建立,分配了资源。但没关系,这次是正常的,C 也确实想连接。
④ C 和 S 正常通信,完成数据传输后关闭连接。 双方断开,连接释放,一切正常。
⑤ 之前滞留的旧报文终于到达 S:SYN (seq=100)。S 收到这个 SYN,它不知道这是个历史残留的废包——它看起来完全像一个新的合法连接请求。
⑥ S → C:SYN+ACK (seq=500, ack=101) — S 回应这个旧请求。如果是两次握手,S 此刻又认为连接已建立,再次分配内存、端口等资源,等待 C 发数据。
⑦ C 收到这个 SYN+ACK,一脸懵逼 — C 根本没发过 seq=100 的新请求(那是很久以前的),直接丢弃或发 RST。但 S 已经白白建立了连接、占用了资源,而且不会有任何数据过来。
结果:服务端被一个早已失效的历史报文骗了,白白浪费资源。如果这种滞留报文很多,服务端资源会被大量无效连接耗尽。
三次握手如何解决?
回到第 ⑥ 步,S 发出 SYN+ACK 后,不会立即建立连接,而是等待第三次 ACK。C 收到这个莫名其妙的 SYN+ACK 后不会回 ACK(因为自己没有待建立的连接),S 迟迟收不到第三次确认,连接就不会建立,资源也不会被浪费。
常见答题误区
| 偏离重点的说法 | 应该强调的 |
|---|---|
| “为了确认双方的收发能力” | 这是附带作用,不是根本原因 |
| “为了同步序列号” | 序列号两次也能同步,不是核心矛盾 |
| “TCP 规范就是这么定的” | 等于没回答 |
正确的重点就一个:防止历史失效连接请求导致服务端错误建立连接、浪费资源。
第三步 ACK 滞留或丢失怎么办?
情况一:ACK 延迟但最终到达
S 在 SYN-RECEIVED 状态下有重传计时器,没收到 ACK 就重发 SYN+ACK(通常重传 5 次,间隔指数退避:1s → 2s → 4s → 8s → 16s)。C 每收到一次 SYN+ACK 都会重新回 ACK。
如果那个滞留的 ACK 在重传期间到达了,S 一样正常接受,连接照常建立。
情况二:ACK 丢失,C 先发数据
C 发送 ACK 后就进入 ESTABLISHED 状态。如果它紧接着发数据,这个数据包里天然带着 ACK 标志位和正确的确认号,S 收到后等价于收到了第三次握手的 ACK,连接直接建立。
情况三:ACK 丢失,重传也全失败
S 重传 SYN+ACK 达到上限后放弃,发 RST,连接失败。C 发数据时收到 RST,应用层报错 Connection reset。
实际中,因为情况二的存在(C 发数据时自带 ACK),第三步 ACK 丢失很少真正导致连接失败。
序列号(seq)机制
seq 是 TCP 可靠传输的基石,核心作用有三个:
1. 保证数据有序
TCP 是字节流协议,数据在网络中可能走不同路径、乱序到达。接收方靠 seq 把乱序的报文按正确顺序重组。
发送:seg1(seq=1, 100字节) → seg2(seq=101, 100字节) → seg3(seq=201, 100字节)
到达顺序可能是:seg3 → seg1 → seg2
接收方按 seq 排序:seg1 → seg2 → seg3 → 交给应用层
2. 保证数据可靠(确认 + 重传的基础)
ACK 机制依赖 seq 来工作。接收方通过 ack 号告诉发送方"我已收到 seq=N 之前的所有数据":
A → B:seq=1, 100字节
B → A:ack=101("101之前的我都收到了,下一个给我101")
A → B:seq=101, 100字节(丢了)
A 超时没收到 ack=201 → 重传 seq=101 的报文
没有 seq,接收方无法告诉发送方"哪些收到了、哪些没收到",重传机制就无法运作。
3. 去重
网络中同一个报文可能被重复投递。接收方靠 seq 识别重复数据并直接丢弃:
A → B:seq=101, 100字节
A 超时重传:seq=101, 100字节(同样的)
B 先后收到两个 seq=101 的报文 → 第二个直接丢弃
一句话总结:seq 让接收方能排序、确认、去重。没有它,TCP 和 UDP 没区别。
服务端如何验证第三步 ACK 的合法性?
服务端检查 ACK 报文中的确认号是否等于自己在第二步发出的 seq+1:
① C → S:SYN (seq=x)
② S → C:SYN+ACK (seq=y, ack=x+1) ← S 自己生成了 y
③ C → S:ACK (seq=x+1, ack=y+1) ← S 检查:ack 是不是 y+1?
S 在 SYN-RECEIVED 状态下维护一个半连接记录(存在 SYN 队列中),里面记着:
| 记录字段 | 值 |
|---|---|
| 客户端 IP:Port | 来自第 ① 步 |
| 客户端初始 seq | x(来自第 ① 步) |
| 自己的初始 seq | y(自己在第 ② 步生成的) |
收到第 ③ 步的包后,S 做以下校验:
- 源 IP:Port 匹配半连接记录?
- ack == y + 1?(确认对方知道我的序列号)
- seq == x + 1?(确认是同一个客户端的后续包)
全部通过 → 从半连接队列移到全连接队列(accept queue),连接建立。
一句话概括:服务端靠"对暗号"验证——我发给你的 seq=y,你必须回 ack=y+1。y 是我随机生成的,只有真正收到我回复的人才知道。
初始序列号为什么是随机的?
ISN(Initial Sequence Number)不从 0 开始,原因有二:
安全性:防止攻击者猜测 seq 伪造报文。如果 y 可预测,攻击者不需要真正收到 SYN+ACK,直接猜出 y 值伪造 ack=y+1 发给 S 就能骗过验证。这就是早期的 TCP 序列号预测攻击。
防止历史连接干扰:如果两台机器之间短时间内建了多次连接,随机 ISN 可以避免旧连接的残留报文被新连接误认为有效数据。
现代操作系统(如 Linux)的 ISN 生成基于时钟 + 源/目的 IP 端口的 hash 算法,让序列号基本不可预测。
SYN Cookie:无状态的防御
在高并发场景下,S 甚至可以不在内存中保存半连接记录,而是把验证信息编码进 y 本身:
y = hash(源IP, 源端口, 目的IP, 目的端口, 时间戳, 密钥)
收到第 ③ 步 ACK 时,S 不查表,用同样的公式重新算一遍,看 ack-1 是否等于重算的结果。对得上就合法。
这就是 SYN Cookie —— 不存状态,靠计算验证,专门防 SYN Flood 攻击。SYN Flood 攻击通过大量伪造源 IP 的 SYN 包,耗尽服务端的半连接队列。SYN Cookie 让服务端无需维护半连接状态,从根本上化解了这种攻击。
总结
| 问题 | 答案 |
|---|---|
| 三次握手的过程 | SYN → SYN+ACK → ACK |
| 为什么不能两次 | 防止历史失效 SYN 导致服务端错误建连、浪费资源 |
| 第三步 ACK 丢失 | 服务端重传 SYN+ACK;客户端发数据时自带 ACK 也能补救 |
| seq 的作用 | 排序、确认重传、去重——可靠传输的基石 |
| 服务端怎么验证 ACK | 检查 ack == 自己的 seq+1,即"对暗号" |
| ISN 为什么随机 | 防止序列号预测攻击 + 防止历史连接干扰 |
| SYN Cookie | 无状态防御 SYN Flood,把验证信息编码进序列号本身 |