TCP 三次握手与四次挥手:用 Wireshark 逐帧分析
抓一次真实的 HTTP 连接,从 TCP 首部字段、序列号变化到 TIME_WAIT,把建立和关闭连接的每一帧都看明白。
- DATE
- LEN
- 1,053 字
- TTL
- 3 min
- LAYER
- L4 · 传输层
三次握手几乎是每本网络教材的第一张图,但真正在 Wireshark 里对上每一个字段的人并不多。这篇笔记的目标是:抓一次真实连接,把建立和关闭的每一帧都解释清楚。
实验准备
开两个终端,一个抓包,一个发请求:
# 终端 1:只抓与目标主机 80 端口相关的 TCP 报文,保存为 pcap
sudo tcpdump -i any -nn -w handshake.pcap 'tcp port 80 and host example.com'
# 终端 2:发起一次 HTTP 请求
curl -s http://example.com/ -o /dev/null
Ctrl+C 停止抓包,然后用 Wireshark 打开 handshake.pcap。
技巧
如果抓到了很多无关流量,在 Wireshark 里右键任意一个报文 → Conversation Filter → TCP,就只剩这一条连接了(等价于过滤器 tcp.stream eq N)。
先认识 TCP 首部
所有分析都建立在首部字段上(RFC 9293):
握手阶段最需要关注的是:
- Sequence Number:本报文段第一个数据字节的序号;
- Acknowledgment Number:期望收到对方的下一个字节序号(ACK 标志置位时才有效);
- Flags:SYN、ACK、FIN、RST 这几个标志位决定了报文的“身份”;
- Options:MSS、Window Scale、SACK Permitted、Timestamps 等只在 SYN 报文里协商的选项。
三次握手
在 Wireshark 里对应的三帧大致是这样(开启了相对序列号):
| No. | 方向 | Flags | Seq | Ack | 说明 |
|---|---|---|---|---|---|
| 1 | C → S | [SYN] | 0 | — | 携带 MSS=1460、WS、SACK_PERM、TSval |
| 2 | S → C | [SYN, ACK] | 0 | 1 | 服务端也选择自己的初始序列号 |
| 3 | C → S | [ACK] | 1 | 1 | 握手完成,不携带数据 |
为什么 ack = x + 1?
SYN 标志本身要占用一个序列号,尽管它不携带任何数据。所以服务端确认“我收到了你序号为 x 的 SYN,下一个想要的是 x+1”。FIN 也一样会占用一个序列号,这一点在挥手阶段还会看到。
相对序列号
真实的初始序列号(ISN)是随机的 32 位数,Wireshark 默认显示相对序列号(从 0 开始)以便阅读。想看原始值,可以看 tcp.seq_raw 字段,或在 Preferences → Protocols → TCP 里关掉 Relative sequence numbers。
为什么是三次,不是两次?
RFC 793 给出的主要理由是防止历史连接的初始化报文造成混乱:如果一个早已失效的旧 SYN 在网络中滞留后到达服务端,两次握手会让服务端直接建立一个无人使用的连接;而三次握手中,客户端收到不认识的 SYN/ACK 会回 RST,把这次错误的建立扼杀掉。
另一个角度是:双方都需要确认对方收到了自己的 ISN。客户端的 ISN 由第 2 步确认,服务端的 ISN 由第 3 步确认,少一步都不完整。
四次挥手
TCP 是全双工的,每个方向需要单独关闭,所以是四次:
- 客户端发 FIN:我没有数据要发了;
- 服务端回 ACK:知道了(但我可能还有数据要发);
- 服务端发 FIN:我也发完了;
- 客户端回 ACK:知道了。
实际抓包时经常只看到三个报文
如果服务端收到 FIN 时恰好也没有数据要发,它会把第 2、3 步合并成一个 [FIN, ACK],于是“四次挥手”在抓包里表现为三帧。这不是异常。
TIME_WAIT 为什么要等 2MSL?
主动关闭的一方要在 TIME_WAIT 停留 2 倍的 MSL(报文最大生存时间),原因有两个:
- 保证最后一个 ACK 能到达:如果它丢了,对端会重传 FIN,自己必须还“在场”才能再回一个 ACK;
- 让旧连接的报文在网络中自然消亡:避免它们被同一四元组的新连接误收。
Linux 中 TIME_WAIT 的时长被硬编码为 60 秒(TCP_TIMEWAIT_LEN)。
常用过滤器
tcp.flags.syn == 1 && tcp.flags.ack == 0 # 只看 SYN(新连接发起)
tcp.flags.syn == 1 && tcp.flags.ack == 1 # 只看 SYN/ACK
tcp.flags.fin == 1 # 连接关闭
tcp.flags.reset == 1 # 连接被重置
tcp.analysis.retransmission # 重传
tcp.stream eq 0 # 第 0 条 TCP 流
SYN Flood
如果看到大量 SYN 却没有后续 ACK,且源地址五花八门,很可能是 SYN Flood。服务端为每个半连接维护状态,队列被占满后就无法接受新连接。Linux 的 SYN Cookie(net.ipv4.tcp_syncookies)就是为此设计的。
小结
- SYN 和 FIN 各占用一个序列号,这是
ack = seq + 1的来源; - 三次握手是为了同步双方 ISN、并防止历史连接干扰;
- 四次挥手源于全双工,第 2、3 步可能合并;
- TIME_WAIT 是“善后”,不是 bug。
下一篇打算看看 TCP 的重传与拥塞控制,在 Wireshark 的 Statistics → TCP Stream Graphs 里会有很直观的图。