Files
cs-note/hhs/NETWORK/04-传输层/02-TCP三次握手与四次挥手.md
T
2026-05-24 11:42:38 +08:00

7.4 KiB
Raw Blame History

tags, create time
tags create time
计算机网络
TCP握手
TCP挥手
TIME_WAIT
2026-05-18 01:40

三次握手与四次挥手

概述

TCP 连接的建立和拆除是其可靠性机制的核心环节。理解每次交互的目的,能帮助你从协议层面判断连接为什么失败或延迟高。

三次握手(Three-Way Handshake)

完整流程

sequenceDiagram
    participant Client as 客户端<br/>ISN=c, seq=c
    participant Server as 服务端<br/>ISN=s, seq=s
    
    Note over Client: state=SYN_SENT<br/>allocate TCBSocket
    
    Client->>Server: SYN (seq=c, flags=SYN)
    Note over Server: state=LISTEN → SYN_RCVD<br/>allocate TCB<br/>window=w, MSS=m
    
    Server-->>Client: SYN-ACK (seq=s, ack=c+1, flags=SYN|ACK)<br/>window=w, MSS=m
    Note over Client: state=SYN_SENT → ESTABLISHED
    
    Client->>Server: ACK (seq=c+1, ack=s+1, flags=ACK)
    Note over Server: state=SYN_RCVD → ESTABLISHED
    Note over Client,Server: ← ESTABLISHED ✅ 连接就绪 -->
    
    Note over Client: send() buffer flushed
    Note over Server: accept()/recv() returns data

为什么必须三次?两次够吗?

假设 问题
如果只要两次(SYN + ACK) 服务器发出 SYN-ACK 后就认为对方已收到——但服务器不知道客户端是否真的收到了 SYN-ACK。如果 SYN-ACK 丢失,客户端不会发起第三次 ACK,连接永远不会进入 ESTABLISHED
如果是两次且客户端重传 SYN 服务器收到重复 SYN 会创建新的连接(没有确认上一轮的机制),导致资源泄漏
三次解决了什么 双方都确认了彼此的收发能力:客户端确认服务器收到了自己的 SYN;服务器确认客户端收到了自己的 SYN-ACK

[!question] SYN-ACK 丢失后会发生什么? 客户端超时重传 SYN(通常 3s、6s、12s... 指数退避)。服务器收到重传的 SYN 后会回复新的 SYN-ACK。整个过程由内核完成,应用层无感知。

各阶段的窗口大小

握手期间还没有实际数据传输,窗口信息用于协商参数:

阶段 携带的信息
SYN MSS (Maximum Segment Size)、Window Scale、Timestamps
SYN-ACK MSS、Window Scale、SACK Permitted
ACK 不携带新选项(可选 Window Scale 确认)
# 通过 tcpdump 观察握手
$ sudo tcpdump -nn 'tcp port 80 and (tcp[13] & 2 != 0)'
IP 192.168.1.100.54321 > 93.184.216.34.80: S 1000:1000(0) win 64240 <mss 1460,nop,wscale 7,sackOK,timestamp 123456>
                                                              ↑ SYN                ↑ Options
IP 93.184.216.34.80 > 192.168.1.100.54321: S 2000:2000(0) ack 1001 win 65535 <mss 1460,sackOK,timestamp 98765>
                                                              ↑ SYN-ACK                    ↑ ACK number = ISN+1
IP 192.168.1.100.54321 > 93.184.216.34.80: . 1001:1001(0) ack 2001 win 512
                                                              ↑ ACK

四次挥手(Four-Way Teardown)

为什么需要四次?

因为 TCP 是全双工的——每条方向的连接独立关闭。A 说"我完了"和 B 说"我也完了"是两个独立的动作。

sequenceDiagram
    participant A as 主动关闭方 (Client)
    participant B as 被动关闭方 (Server)
    
    Note over A: Application calls close()
    
    A->>B: FIN (seq=x)                       ──→ Step 1: FIN
    Note over B: state=ESTABLISHED → CLOSE_WAIT
    Note over B: "Client is done, but I may still have data!"
    
    B->>A: ACK (ack=x+1)                     ──→ Step 2: ACK (确认FIN)
    Note over B: Application calls close() after sending remaining data
    
    B->>A: FIN (seq=y)                       ──→ Step 3: FIN
    Note over A: state=FIN_WAIT_2 → FIN_WAIT_1
    Note over A: "OK, I'm done too."
    
    A->>B: ACK (ack=y+1)                     ──→ Step 4: ACK
    
    Note over A: → TIME_WAIT (wait 2*MSL)
    Note over B: → CLOSED ✅
    
    Note over A: After 2*MSL timeout → CLOSED ✅

关键时机说明

Time  A (主动方)                          B (被动方)
──── ───────────────────────────────────────────────────────────
t=0  close() → send(FIN)                  ESTABLISHED
     → FIN_WAIT_1                         ↓ recv(FIN)
t=1                                  → CLOSE_WAIT (等应用调用close)
     ← ACK(FIN)                           send(...) remaining data
     FIN_WAIT_2                           ↓ close()
t=2                                          → send(FIN)
     ← FIN                                → LAST_ACK
t=3  ACK(FIN) → TIME_WAIT                  → CLOSED
     (等待 2*MSL ≈ 120s)
t=120 TIME_WAIT → CLOSED ✅

TIME_WAIT 的深度解析

为什么要等 2 * MSL?

原因 说明
确保最后一个 ACK 到达 如果 ACK 丢失,对方会重发 FIN — TIME_WAIT 状态下可以重新回复 ACK
让旧连接的报文在网络中自然消亡 避免旧连接的数据包误入新连接(同一四元组)

MSL(Maximum Segment Lifetime)是多少?

  • RFC 标准未定义具体值
  • Linux / Windows / macOS 通常设为 60 秒
  • 所以 2 * MSL ≈ 120 秒

TIME_WAIT 堆积的影响与应对

# 查看 TIME_WAIT 数量
$ ss -tan state time-wait | wc -l
15234

# TIME_WAIT 的源端口分布
$ ss -tan state time-wait src :80 | awk '{print $5}' | cut -d: -f2 | sort | uniq -c | sort -rn | head

# 解决方案
方案 说明 风险
增加可用端口数 /proc/sys/net/ipv4/ip_local_port_range 调整为 1024 65535 安全扫描器可能利用短连接
启用 TIME_WAIT 复用 net.ipv4.tcp_tw_reuse = 1 只能复用在 outbound 连接上(RFC 兼容性)
快速回收 net.ipv4.tcp_tw_recycle = 0(已从内核移除) ❌ 不安全,会破坏 NAT 场景
使用 keepalive 缩短生命周期 降低 keepalive 超时时间 效果有限
架构改造 gRPC 长连接代替 HTTP 短连接 最佳方案

[!warning] tcp_tw_recycle 已被移除 该选项在 IPv4 NAT 场景下有缺陷(不同内部主机映射到同一公网 IP 时,TS 字段冲突)。Linux 4.12 起已从内核彻底移除。

半开连接(Half-Open Connection)

SYN_RECV 状态 = 三次握手中途被卡住的状态
# 查看 SYN_RECV 数量
$ ss -tan state syn-recv
State   Recv-Q Send-Q Local Port   Peer Port
SYN-RECV   0      0       :80         203.0.113.5:12345

# 大量 SYN_RECV = SYN Flood 攻击!

防御措施:

  • tcp_syncookies = 1(开启 SYN Cookie)
  • 限制每秒 SYN 速率
  • CDN/WAF 前置过滤

Go 中的实践

// 客户端设置 connect timeout
conn, err := net.DialTimeout("tcp", "example.com:80", 5*time.Second)

// 服务端设置 accept timeout (Go 1.19+)
ln, _ := net.Listen("tcp", ":8080")
for {
    c, err := ln.Accept()          // 阻塞等待
    if err != nil {
        // 处理 Accept 超时
        break
    }
    go handleConn(c)               // goroutine 处理每个连接
}

关联笔记