TCP 报文结构回顾

在分析握手和挥手之前,先回顾 TCP 头部的关键字段:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     |       |C|E|U|A|P|R|S|F|                               |
| Hdr |  Rsvd |W|C|R|C|S|S|Y|I|            Window             |
| Len |       |R|E|G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

其中 SYN、ACK、FIN、RST 是控制位,决定了报文的类型。

三次握手详解

第一次握手:SYN

客户端 ──── SYN=1, seq=x ────> 服务器
  • 客户端发送 SYN 报文,SYN 标志位置 1
  • 序列号 seq=x(随机初始值,称为 ISN)
  • 客户端进入 SYN_SENT 状态

第二次握手:SYN+ACK

客户端 <──── SYN=1, ACK=1, seq=y, ack=x+1 ──── 服务器
  • 服务器收到 SYN,回复 SYN+ACK
  • SYN=1 表示这是服务器的连接请求
  • ACK=1 表示确认号有效
  • seq=y(服务器的 ISN)
  • ack=x+1 表示确认收到了客户端的 seq=x
  • 服务器进入 SYN_RCVD 状态

第三次握手:ACK

客户端 ──── ACK=1, seq=x+1, ack=y+1 ────> 服务器
  • 客户端发送 ACK 确认
  • seq=x+1(接上一次的序列号)
  • ack=y+1(确认服务器的 seq=y)
  • 双方进入 ESTABLISHED 状态

为什么是三次?

核心原因:防止历史连接的初始化

如果只有两次握手,服务器无法确认客户端是否收到了自己的 SYN+ACK。三次握手的本质是双方各确认一次收发能力:

握手确认内容
第 1 次服务器确认:客户端能发
第 2 次客户端确认:服务器能收能发
第 3 次服务器确认:客户端能收

三次握手同时也是最小的确认次数。

四次挥手详解

为什么挥手是四次?

TCP 是全双工协议,每个方向的关闭需要独立进行。发送 FIN 只表示"我不会再发数据了",但对方可能还有数据要发。

挥手过程

主动关闭方                        被动关闭方
    │                                │
    │──── FIN=1, seq=u ────────>     │   ① 主动方发送 FIN
    │                                │
    │     <──── ACK=1, ack=u+1 ─────│   ② 被动方确认 FIN
    │                                │
    │     (被动方继续发送剩余数据)     │   ③ 半关闭状态
    │                                │
    │     <──── FIN=1, seq=w ────────│   ④ 被动方发送 FIN
    │                                │
    │──── ACK=1, ack=w+1 ────────>   │   ⑤ 主动方确认 FIN
    │                                │
    │  TIME_WAIT (2MSL)              │   ⑥ 等待确认到达
    │                                │

状态变迁

主动关闭方: ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
被动关闭方: ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED

为什么需要 TIME_WAIT?

TIME_WAIT 持续 2MSL(Maximum Segment Lifetime),有两个作用:

  1. 确保最后的 ACK 能到达:如果最后的 ACK 丢失,被动方会重发 FIN,主动方需要在 TIME_WAIT 状态重新发送 ACK
  2. 防止旧连接的报文干扰新连接:等待足够时间让网络中残留的报文消亡

抓包实战

使用 tcpdump 抓取握手过程:

# 抓取 80 端口的 TCP 握手
tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack|tcp-fin) != 0' -nn -c 20 port 80

# 更详细的抓包
tcpdump -i eth0 -nn -S port 80

使用 Wireshark 过滤器:

tcp.flags.syn == 1 && tcp.flags.ack == 0  # 过滤 SYN
tcp.flags.fin == 1                          # 过滤 FIN
tcp.flags.rst == 1                          # 过滤 RST

常见问题排查

1. SYN_RCVD 状态堆积

# 查看 SYN_RCVD 连接数
ss -ant | grep SYN-RCVD | wc -l

# 可能是 SYN Flood 攻击,启用 SYN Cookie
sysctl -w net.ipv4.tcp_syncookies=1

2. TIME_WAIT 过多

# 查看 TIME_WAIT 数量
ss -ant | grep TIME-WAIT | wc -l

# 启用 TIME_WAIT 复用
sysctl -w net.ipv4.tcp_tw_reuse=1

3. 连接超时排查

# 查看重传统计
netstat -s | grep -i retrans

# 查看超时参数
sysctl net.ipv4.tcp_syn_retries
sysctl net.ipv4.tcp_fin_timeout

总结

TCP 的握手和挥手是可靠的基石。理解报文级别的交互,对于排查网络问题、优化性能至关重要。