4、TCP 三次握手:SYN、SYN-ACK、ACK 的抓包验证与握手延迟分析

三次握手,说白了就是 TCP 连接建立的「敲门仪式」。你想想看,两个人要通话,总得先确认对方在听吧?TCP 也一样,客户端和服务端得互相确认「我准备好了,你呢?」。这个过程,就是通过 SYN、SYN-ACK、ACK 这三个报文完成的。

我在项目里见过不少新手,觉得三次握手就是背背状态机。但真正遇到线上问题的时候,你会发现——握手延迟往往是应用卡顿的元凶。今天我们就用 Wireshark 抓包,把这三个报文一个一个揪出来看。

4.1 三次握手的标准流程

先画个图,把流程理清楚。别急着抓包,脑子里得有画面。

核心要点:三次握手的本质是「一发、一回、一确认」。少了任何一步,连接都建不起来。
TCP 三次握手流程图 客户端 (Client) 服务端 (Server) CLOSED → SYN_SENT LISTEN → SYN_RCVD ① SYN (seq=x) ② SYN-ACK (seq=y, ack=x+1) ③ ACK (seq=x+1, ack=y+1) ESTABLISHED ESTABLISHED ✅ 连接建立,开始传输数据 💡 关键点:seq 和 ack 是握手过程的「对账凭证」。 客户端发送 seq=x,服务端回复 seq=y, ack=x+1,客户端再回复 ack=y+1。

4.2 抓包验证:把报文抓出来看看

光看理论不过瘾。我们直接上 Wireshark,抓一个真实的 HTTP 请求看看。

4.2.1 抓包准备

我个人习惯用 tcp.port == 80 作为过滤条件,这样只显示 HTTP 相关的 TCP 流。你也可以用 tcp.flags.syn == 1 直接过滤 SYN 包。

小技巧:在 Wireshark 里输入 tcp.stream eq 0 可以只看第一个 TCP 流,避免被其他连接干扰。

4.2.2 抓包结果分析

下面是我从一次真实抓包中提取的三次握手报文。注意看 seq 和 ack 的变化。

序号 时间 源 → 目的 Flags Seq Ack
1 0.000000 192.168.1.100 → 93.184.216.34 SYN 0 0
2 0.045123 93.184.216.34 → 192.168.1.100 SYN-ACK 0 1
3 0.045210 192.168.1.100 → 93.184.216.34 ACK 1 1

看到了吗?

  • 第一次握手:客户端发 SYN,seq=0。这是相对序列号,Wireshark 默认从 0 开始显示。
  • 第二次握手:服务端回 SYN-ACK,seq=0,ack=1。注意 ack 是客户端的 seq+1。
  • 第三次握手:客户端发 ACK,seq=1,ack=1。服务端的 seq+1 变成了 ack。
注意:Wireshark 默认显示的是「相对序列号」。如果你想看真实的原始序列号,右键 → Protocol Preferences → Relative Sequence Numbers,取消勾选即可。我在排查一个诡异的丢包问题时,就靠这个发现了客户端和服务端序列号不匹配的 bug。

4.3 握手延迟分析:慢在哪里?

三次握手本身很快,通常也就几十毫秒。但一旦出现延迟,问题就来了。我遇到过最夸张的一次,握手花了 3 秒多——用户直接反馈「网页打不开」。

4.3.1 延迟的三大来源

说白了,握手延迟就三个地方:

  1. 网络 RTT(往返时间):这是物理限制。客户端到服务端距离越远,RTT 越大。比如从北京到纽约,RTT 大约 200ms,三次握手至少 1.5 个 RTT,也就是 300ms。
  2. 服务端处理延迟:服务端收到 SYN 后,需要分配传输控制块(TCB)。如果服务器负载高,这个分配过程可能变慢。
  3. 客户端处理延迟:收到 SYN-ACK 后,客户端要回复 ACK。如果客户端 CPU 忙或者 socket 队列满了,也会卡住。

4.3.2 用 Wireshark 计算握手延迟

Wireshark 里有个好东西叫「TCP 握手 RTT」。你选中 SYN 包,在 Statistics → TCP Stream Graph → Round Trip Time Graph 里就能看到。

我一般手动算:

  • 从 SYN 发出到收到 SYN-ACK:这是半个 RTT(服务端处理时间 + 网络传输时间)。
  • 从 SYN-ACK 发出到收到 ACK:这是另外半个 RTT。
  • 两者相加就是完整的握手 RTT。

拿上面的抓包数据来说:

  • SYN → SYN-ACK:0.045123 - 0.000000 = 45.123ms
  • SYN-ACK → ACK:0.045210 - 0.045123 = 0.087ms
  • 总握手时间:45.123 + 0.087 ≈ 45.21ms

嗯,这里要注意:第二次握手到第三次握手的时间极短(0.087ms),说明客户端处理很快。但第一次到第二次花了 45ms,说明网络 RTT 占了主导。

实战经验:如果 SYN → SYN-ACK 的时间远大于 SYN-ACK → ACK 的时间,问题大概率出在服务端或网络路径上。我曾经排查过一个案例,SYN → SYN-ACK 花了 800ms,最后发现是中间防火墙的 SYN Cookie 校验导致延迟。

4.4 避坑指南:三次握手的常见问题

做网络排障这么多年,我总结了几条血泪教训:

4.4.1 SYN 超时重传

客户端发 SYN 后,如果没收到 SYN-ACK,会等一段时间(默认 1 秒)后重传。如果重传 3 次还没回应,连接就超时了。

我曾经遇到过一台服务器,SYN 包发出去石沉大海。抓包一看,服务端根本没收到 SYN。最后发现是交换机端口配置了错误的 VLAN,直接把包丢了。

4.4.2 SYN Flood 攻击

攻击者伪造大量 SYN 包,但不完成第三次握手。服务端会一直保留半连接,直到资源耗尽。

应对方法:

  • 开启 SYN Cookie(Linux 默认开启)
  • 限制半连接队列大小(net.ipv4.tcp_max_syn_backlog)
  • 使用防火墙限速

4.4.3 握手延迟的优化思路

如果你的应用对握手延迟敏感,可以考虑:

  • TCP Fast Open(TFO):在 SYN 里携带数据,省掉一次 RTT。但需要客户端和服务端都支持。
  • 连接复用:用 HTTP Keep-Alive 或连接池,避免频繁握手。
  • 部署 CDN:把服务端搬到离用户近的地方,减少 RTT。
我的建议:别一上来就搞优化。先抓包看看握手延迟到底是多少,是网络问题还是服务端问题。数据说话,别靠猜。

4.5 小结

三次握手,说白了就是 SYN、SYN-ACK、ACK 三个报文的来回。抓包验证时,重点看 seq 和 ack 的变化,以及每个阶段的时间差。握手延迟主要来自网络 RTT 和服务端处理,用 Wireshark 的 RTT 图可以快速定位瓶颈。

嗯,这一章就到这里。记住:抓包是手段,不是目的。真正重要的是通过抓包理解 TCP 的行为,然后找到优化方向。