200 lines
7.4 KiB
Markdown
200 lines
7.4 KiB
Markdown
|
|
---
|
|||
|
|
tags: [计算机网络, TCP握手, TCP挥手, TIME_WAIT]
|
|||
|
|
create time: 2026-05-18 01:40
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 三次握手与四次挥手
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
TCP 连接的建立和拆除是其可靠性机制的核心环节。理解每次交互的目的,能帮助你从协议层面判断连接为什么失败或延迟高。
|
|||
|
|
|
|||
|
|
## 三次握手(Three-Way Handshake)
|
|||
|
|
|
|||
|
|
### 完整流程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 确认)|
|
|||
|
|
|
|||
|
|
```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 <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 说"我也完了"是两个独立的动作。
|
|||
|
|
|
|||
|
|
```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 相关的内核参数
|