--- tags: [计算机网络, TCP握手, TCP挥手, TIME_WAIT] create time: 2026-05-18 01:40 --- # 三次握手与四次挥手 ## 概述 TCP 连接的建立和拆除是其可靠性机制的核心环节。理解每次交互的目的,能帮助你从协议层面判断连接为什么失败或延迟高。 ## 三次握手(Three-Way Handshake) ### 完整流程 ```mermaid sequenceDiagram participant Client as 客户端
ISN=c, seq=c participant Server as 服务端
ISN=s, seq=s Note over Client: state=SYN_SENT
allocate TCBSocket Client->>Server: SYN (seq=c, flags=SYN) Note over Server: state=LISTEN → SYN_RCVD
allocate TCB
window=w, MSS=m Server-->>Client: SYN-ACK (seq=s, ack=c+1, flags=SYN|ACK)
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 确认)| ```bash # 通过 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 ↑ SYN ↑ Options IP 93.184.216.34.80 > 192.168.1.100.54321: S 2000:2000(0) ack 1001 win 65535 ↑ 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 说"我也完了"是两个独立的动作。 ```mermaid 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 堆积的影响与应对 ```bash # 查看 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 状态 = 三次握手中途被卡住的状态 ``` ```bash # 查看 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 中的实践 ```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 处理每个连接 } ``` ## 关联笔记 - [[hhs/NETWORK/TCP段结构与状态机]] — 状态机的详细定义 - [[hhs/NETWORK/TCP可靠传输机制]] — 序列号如何工作 - [[hhs/NETWORK/TCP内核参数调优]] — TIME_WAIT 相关的内核参数