TCP 三次握手与四次挥手:用 Wireshark 逐帧分析

抓一次真实的 HTTP 连接,从 TCP 首部字段、序列号变化到 TIME_WAIT,把建立和关闭连接的每一帧都看明白。

DATE
LEN
1,053 字
TTL
3 min

三次握手几乎是每本网络教材的第一张图,但真正在 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):

HDRTCP Header (RFC 9293)
012345678910111213141516171819202122232425262728293031
Source Port16
Destination Port16
Sequence Number32
Acknowledgment Number32
Data Offset4
Rsrvd4
CWR
ECE
URG
ACK
PSH
RST
SYN
FIN
Window16
Checksum16
Urgent Pointer16
Options (可变长,最多 40 字节)
Data

握手阶段最需要关注的是:

  • Sequence Number:本报文段第一个数据字节的序号;
  • Acknowledgment Number:期望收到对方的下一个字节序号(ACK 标志置位时才有效);
  • Flags:SYN、ACK、FIN、RST 这几个标志位决定了报文的“身份”;
  • Options:MSS、Window Scale、SACK Permitted、Timestamps 等只在 SYN 报文里协商的选项。

三次握手

SEQTCP 三次握手
Client Server SYN seq=x SYN_SENT SYN, ACK seq=y ack=x+1 SYN_RCVD ACK seq=x+1 ack=y+1 双方进入 ESTABLISHED

在 Wireshark 里对应的三帧大致是这样(开启了相对序列号):

No.方向FlagsSeqAck说明
1C → S[SYN]0—携带 MSS=1460、WS、SACK_PERM、TSval
2S → C[SYN, ACK]01服务端也选择自己的初始序列号
3C → S[ACK]11握手完成,不携带数据

为什么 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 步确认,少一步都不完整。

四次挥手

SEQTCP 四次挥手(客户端主动关闭)
Client Server FIN, ACK seq=u FIN_WAIT_1 ACK ack=u+1 CLOSE_WAIT FIN_WAIT_2 FIN, ACK seq=v LAST_ACK ACK ack=v+1 TIME_WAIT(等待 2MSL)

TCP 是全双工的,每个方向需要单独关闭,所以是四次:

  1. 客户端发 FIN:我没有数据要发了;
  2. 服务端回 ACK:知道了(但我可能还有数据要发);
  3. 服务端发 FIN:我也发完了;
  4. 客户端回 ACK:知道了。

实际抓包时经常只看到三个报文

如果服务端收到 FIN 时恰好也没有数据要发,它会把第 2、3 步合并成一个 [FIN, ACK],于是“四次挥手”在抓包里表现为三帧。这不是异常。

TIME_WAIT 为什么要等 2MSL?

主动关闭的一方要在 TIME_WAIT 停留 2 倍的 MSL(报文最大生存时间),原因有两个:

  1. 保证最后一个 ACK 能到达:如果它丢了,对端会重传 FIN,自己必须还“在场”才能再回一个 ACK;
  2. 让旧连接的报文在网络中自然消亡:避免它们被同一四元组的新连接误收。

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 里会有很直观的图。

↑↓ 选择 · Enter 打开 · Esc 关闭