抓包的时候看到那三行 SYN → SYN, ACK → ACK,很多人第一反应是"能通就行,为什么非得三次"。

我刚学网络那会儿也这么想,直到有次线上服务出现大量 CLOSE_WAIT 连接堆积,才逼着自己把这个问题从头推了一遍。今天把"为什么是三次"讲透,顺便把面试会追问的延伸点一起收拾了。

先说结论

三次握手要解决的核心问题,不是"确认对面能收到",而是双方都确认自己和对方的收发能力正常,并且对初始序列号达成一致。

具体来说,这三次完成了四件事:

  1. 客户端确认:我的发送 OK、你的接收 OK
  2. 服务端确认:我的发送 OK、你的接收 OK
  3. 双方交换初始序列号(ISN)
  4. 双方确认这个连接是"新鲜"的,不是历史遗留的失效请求

第 4 条是最容易被忽略、也是最能体现"为什么必须是三次"的一条。

为什么不能是两次

两次握手只能让服务端确认"客户端发的包我能收到",但服务端无法确认"客户端能收到我发的包"。这还只是能力层面的问题,更严重的是历史失效连接请求。

看这个场景:

客户端发了一个 SYN(seq=100),路上卡了很久。客户端超时后重发了新的 SYN(seq=200),这次连接建好、传完数据、正常关闭。这时候,那个卡住的老 SYN(seq=100)终于抵达服务端。

如果是两次握手,服务端收到老 SYN,会直接认为连接已建立,开始分配资源、等待客户端发数据。但客户端根本不知道这个连接存在——它早就关了。这份资源就一直空等,成了"半开连接"。攻击者拿这个做文章,就是 SYN 洪泛。

三次握手下,服务端收到老 SYN 后回 SYN-ACK,客户端一比对序列号(100 和自己期望的对不上),直接回 RST,服务端资源释放。一次往返解决。

TCP 的三次握手在 RFC 793 里定死,背后有 1983 年 Tomlinson 那篇讨论"不可靠信道上建立可靠连接"的经典讨论:三次是最小可行次数,两次不足以判定连接的新鲜性。

为什么不是四次

因为第二步能合并。

看起来应该是四次:

客户端 → SYN        我要连你
服务端 → ACK        我收到了
服务端 → SYN        我也要连你
客户端 → ACK        我收到了

但服务端要发的 ACK(确认你的 SYN)和 SYN(同步我的序列号),方向相同、时机相同,完全可以塞进同一个包里——这就是 SYN-ACK。

所以:三次是理论最小值,四次是没必要的重复。

一句话记:握手要交换两个方向各一次"我准备好了",而中间那次可以双向合并。

序列号为什么必须同步

握手还有个隐藏任务:交换初始序列号。

TCP 是字节流协议,每个字节都有序号。双方必须知道对方的起始序号,才能:

  • 判断哪些数据是新数据、哪些是重传
  • 做流量控制和丢包检测
  • 防止"旧连接的残包"被当成本次连接的数据

如果 ISN 不在握手阶段对齐,后面所有序号都对不上,会话直接崩。而 ISN 的选取本身也不该是简单递增——现代实现会用时间戳 + 随机数生成,防止被猜测后注入数据。

几个实战延伸

半连接队列 vs 全连接队列

这两个队列经常被混着说,但排查问题时必须分清:

  • 半连接队列(syn queue):收到 SYN、已回 SYN-ACK、还没收到 ACK 的连接
  • 全连接队列(accept queue):三次握手已完成、等应用 accept() 取走的连接

线上"连接超时但服务进程活着"的典型原因,往往不是被攻击,而是全连接队列满了——应用 accept() 太慢,队列积压。这两个队列要分开看,排查方向完全不同。

# 全连接队列溢出统计(ListenOverflows 非 0 就是)
ss -lnt
nstat -az TcpExtListenOverflows TcpExtListenDrops

SYN 洪泛怎么防

# 开启 syncookies:不回队列,把连接状态编码进序列号
sysctl -w net.ipv4.tcp_syncookies=1

# 半连接队列上限
sysctl net.ipv4.tcp_max_syn_backlog

# SYN-ACK 重试次数(越少越快释放)
sysctl net.ipv4.tcp_synack_retries

tcp_syncookies 的原理值得一提:它不保存连接状态,而是把状态信息编码进返回的序列号里,客户端带着它回来时再解码校验。代价是丢失部分 TCP 选项(比如窗口扩大),但在被打的时候是救命手段。

抓包怎么看

sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'

正常握手是 S → S. → .(三个包)。如果看到大量只有 S 没有回应,要么是被打了,要么对端服务挂了。如果看到 S → R.,说明对端端口没开。

一句话总结

三次握手不是"规定",是在不可靠网络里建立可靠连接的最小共识次数——两次不够(防不住历史失效连接),四次多余(ACK 和 SYN 可以合并)。

下次面试被问"为什么三次",别只答"确认双方收发能力",把历史失效连接这个点抛出来,再补一句"所以 ISN 的随机性也是安全设计",面试官的眼睛会亮一下。

下一篇讲四次挥手,以及为什么 TIME_WAIT 要等 2MSL——那个才是真·面试重灾区。

更多推荐