---
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 相关的内核参数