This commit is contained in:
2026-05-24 11:42:38 +08:00
commit 30d312ac35
521 changed files with 146481 additions and 0 deletions
@@ -0,0 +1,155 @@
---
tags: [计算机网络, TCP, 状态机, 三次握手, 四次挥手]
create time: 2026-05-18 01:30
---
# TCP 段结构与状态机
## 概述
TCP(Transmission Control Protocol)是互联网最核心的可靠传输协议。理解其段结构和完整状态机,是排查线上网络问题的基本功。
## TCP 首部格式(32-bit fields)
```
┌─────────────┬─────────────┬───────────────────┐
│ Source Port │ Destination Port│ Sequence No │
│ 2 bytes │ 2 bytes │ 4 bytes │
├─────────────┼───────────────┤ │
│ Ack No │ Data Offset | Reserved│ECN│ CWR│ ACK │ PSH │ RST │ SYN │ FIN │
│ 4 bytes │ 4bits │ 3 bits │ │ │ │ │ │
├─────────────┴─────────────┴───────────────────┤
│ Window Size │
│ 2 bytes │
├───────────────────┬───────────────────────────┤
│ Checksum │ Urgent Pointer │
│ 2 bytes │ 2 bytes │
├───────────────────┴───────────────────────────┤
│ Options (variable) │
│ Max: 40 bytes │
└───────────────────────────────────────────────┘
Minimum header: 20 bytes
Maximum header: 60 bytes (with options)
```
### 关键字段详解
| 字段 | 大小 | 说明 |
|------|------|------|
| **Source/Dest Port** | 各 2B | 源端口和目的端口 |
| **Sequence Number** | 4B | 本报文第一个字节的序号(32 bit,约 4GB) |
| **Acknowledgment Number** | 4B | 期望收到的下一个字节序号(ACK=1 时有效) |
| **Data Offset** | 4 bit | 首部长度,单位 4 bytes(最小 5 = 20B,最大 F = 60B) |
| **Reserved** | 3 bit | 保留字段,必须为 0 |
| **ECN** | 2 bit | ECN-Echo + CWR:显式拥塞通知 |
| **URG** | 1 bit | 紧急指针有效 |
| **ACK** | 1 bit | 确认号有效(除首次 SYN 外几乎所有包都设 ACK=1) |
| **PSH** | 1 bit | 推送标志,告诉接收端立即提交应用层 |
| **RST** | 1 bit | 复位连接(异常关闭) |
| **SYN** | 1 bit | 同步序列号(建立连接) |
| **FIN** | 1 bit | 结束标志(优雅关闭) |
| **Window Size** | 2B | 接收窗口大小,流量控制基础 |
| **Checksum** | 2B | 覆盖首部 + 数据的 CRC |
| **Urgent Pointer** | 2B | URG=1 时偏移量,指示紧急数据位置 |
### 常用 TCP Options
| Option | 大小 | 说明 |
|--------|------|------|
| NOP | 1B | No Operation,用于填充对齐 |
| MSS | 4B | Maximum Segment Size,通常 1460(1500-MT-20IP) |
| Window Scale | 4B | 扩大窗口因子(2^x倍,最大 14 = 16384 倍) |
| SACK Perm / SACK | 可变 | Selective Acknowledgment,选择性确认 |
| Timestamps | 10B | TSval + TSecr,RTT 测量和防回绕 |
## TCP 状态机——完整图
```mermaid
stateDiagram-v2
[*] --> CLOSED
note right of CLOSED
TCP 不活跃状态
只有监听套接字在此
end note
CLOSED --> LISTEN: socket() + listen()
LISTEN --> SYN_RCVD: 收到 SYN (server only)
LISTEN --> ESTABLISHED: connect() + remote SYN-ACK (client side rare)
note right of SYN_SENT
client sent SYN
waiting for SYN-ACK
end note
SYN_RCVD --> ESTABLISHED: 3-Way Handshake Complete
SYN_SENT --> ESTABLISHED: Got SYN-ACK
ESTABLISHED --> CLOSE_WAIT: Application calls close()
ESTABLISHED --> FIN_WAIT_1: Remote sends FIN
FIN_WAIT_1 --> FIN_WAIT_2: Local ACK received
FIN_WAIT_1 --> CLOSING: Simultaneous close
FIN_WAIT_2 --> TIME_WAIT: Remote FIN received
CLOSING --> TIME_WAIT: Remote ACK received
CLOSE_WAIT --> LAST_ACK: Local application close
LAST_ACK --> CLOSED: Final ACK received
TIME_WAIT --> CLOSED: 2 * MSL timeout
note right of TIME_WAIT
Must wait 2*MSL
Ensures final ACK arrives
Prevents old duplicate packets
end note
note right of CLOSE_WAIT
Server side closing
Application must call close()
end note
```
### 所有 11 种状态一览
| # | 状态 | 触发条件 | 谁处于? |
|---|------|---------|---------|
| 1 | CLOSED | 初始/终止 | 双方初始状态 |
| 2 | LISTEN | `listen()` 系统调用 | 服务端 |
| 3 | SYN_SENT | `connect()` 发出 SYN | 客户端 |
| 4 | SYN_RCVD | 收到 SYN | 服务端 |
| 5 | ESTABLISHED | 三次握手完成 | 双方 |
| 6 | FIN_WAIT_1 | 本地调用 close() | 主动关闭方 |
| 7 | FIN_WAIT_2 | FIN_WAIT_1 的 ACK 到达 | 主动关闭方 |
| 8 | CLOSE_WAIT | 收到远程 FIN | **被动关闭方**(服务器!)|
| 9 | CLOSING | 同时关闭 | 极少见 |
| 10 | LAST_ACK | 本地发送最后一个 FIN | 被动关闭方 |
| 11 | TIME_WAIT | 发送完最后一个 ACK | 主动关闭方 |
> [!tip] 哪些状态是你最常看到的?
> - **ESTABLISHED**: 正常工作
> - **TIME_WAIT**: 高频短连接的服务端(如 nginx、API 网关)
> - **CLOSE_WAIT**: ⚠️ **应用 Bug!** 说明服务器收到了客户端的关闭请求但没调用 close()
### CLOSE_WAIT 堆积排查
```bash
# 查看 CLOSE_WAIT 数量
$ ss -tan state close-wait | wc -l
4521
# 是谁引起的?查进程
$ ss -tan state close-wait src :80
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
CLOSE-WAIT 0 0 10.0.0.1:80 203.0.113.5:45678 app=nginx(1234)
# 解决方案: 修复代码中未正确关闭的文件描述符/HTTP连接
```
## 关联笔记
- [[hhs/NETWORK/TCP三次握手与四次挥手]] — 详细解析握手/挥手过程
- [[hhs/NETWORK/TCP可靠传输机制]] — 序列号/确认号的深入讲解
- [[hhs/NETWORK/TCP拥塞控制]] — TCP 窗口的动态管理
@@ -0,0 +1,199 @@
---
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 相关的内核参数
@@ -0,0 +1,168 @@
---
tags: [计算机网络, TCP可靠传输, 序列号, 确认号, 重传]
create time: 2026-05-18 01:50
---
# 可靠传输机制
## 概述
TCP 承诺"可靠交付"——不管中间网络多么不可靠,发送方最终确保每个字节都被接收方完整、有序地收到。这是通过**序列号/确认号 + 重传 + 校验**组合实现的。
## 序列号(Sequence Number)
### 基本原理
每个 TCP 段有一个 32 bit 的序列号,代表该段的**第一个数据字节在整个流中的位置**。
```
发送方发出的数据流:
字节: 0 1 2 3 ... 99 100
├─────┼─────┼─────┼───┤ └───┤
│ Seg1│ Seg2│ Seg3│...│ │SegN│
seq=0 seq=50 seq=100 seq=99
Seq 值范围: 0 ~ 2^32 - 1 = 4,294,967,295 (约 4GB)
回绕: 到达最大值后回到初始 ISN (Initial Sequence Number)
```
### ISN(Initial Sequence Number)生成策略
```go
// Go 的 net包: ISN 使用随机化防止预测攻击
func getISN() uint32 {
// crypto/rand 或 /dev/urandom
return randomUint32()
}
```
Linux 默认 ISN = jiffies + hash(src/dst IP/port),现代内核已改为完全随机化。
## 确认号(Acknowledgment Number)
| 含义 | 说明 |
|------|------|
| ack = N | "我已经收到了 **N-1 及之前**的所有字节" |
| ack = expected next byte | 期望对方下一个收到的字节序号 |
| ACK = 0 的情况 | 仅在 SYN/FIN 阶段出现(它们不占序列号空间)|
### SACK(Selective Acknowledgment)
标准 TCP 只能确认"前面的所有字节都到了",SACK 允许接收方告诉发送方"**哪些乱序的块我也收到了**":
```
正常 ACK: SACK:
──────── ────────
收齐了 [0,49] 收到: [0,49] [100,149] [200,249]
ack=50 缺: [50,99] [150,199]
↑
只重传缺的!
```
```bash
$ sysctl net.ipv4.tcp_sack
net.ipv4.tcp_sack = 1 # Linux 默认开启
```
## 超时重传(RTO - Retransmission Timeout)
### RTO 计算(Jacobson/Karels 算法)
```
RTT_sample = T_arrival - T_sent // 本次往返时间
rtt_variance = (1 - β) * rtt_variance + β * |RTT_sample - RTT_avg|
β = 1/4
RTO = RTT_avg + 4 * rtt_variance
α = 3/4
初始 RTO: 至少 1 秒 (RFC 6298)
最大 RTO: 60 秒
```
### 快速重传(Fast Retransmit)
如果收到**3 个重复 ACK**(duplicate ACK),说明某个包很可能丢了——不等 RTO 过期立即重传:
```mermaid
flowchart LR
A["Segment 1 → recv ✅"] --> B["Segment 2 → recv ✅"]
B --> C["Segment 3 → LOST ❌"]
C --> D["DupACK for seg 2 ×3"]
D -->|"触发快速重传"| E["Re-send Segment 3 immediately ✅"]
style C fill:#FF6B6B,color:#fff
style E fill:#98FB98,color:#000
```
```
传统 RTO 重传 vs 快速重传:
─────────────────────────
传统: 丢包 → 等 RTO(≥1s) → 重传 → 再等一个 RTT → 共 ≥ 2s
快速: 丢包 → 收到3个dup ACK(~1个RTT) → 立即重传 → 共 ~1 RTT
```
## Go 中的 TCP KeepAlive
```go
import "golang.org/x/sys/unix"
// 设置 TCP keepalive (Go stdlib 1.11+)
ln, _ := net.Listen("tcp", ":8080")
conn, _ := ln.Accept()
if tc, ok := conn.(*net.TCPConn); ok {
// 每 30s 发送一次 keepalive probe
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(30 * time.Second)
// 最多重试 3 次 (内核参数 net.ipv4.tcp_keepalive_probes)
// 总超时 = 30s * 3 = 90s
}
```
```bash
# Linux 内核默认值
$ sysctl net.ipv4.tcp_keepalive_time # 首次探测前的空闲时间
net.ipv4.tcp_keepalive_time = 7200 # 2小时!
$ sysctl net.ipv4.tcp_keepalive_intvl # 两次探测间隔
net.ipv4.tcp_keepalive_intvl = 75 # 75秒
$ sysctl net.ipv4.tcp_keepalive_probes # 最多探测次数
net.ipv4.tcp_keepalive_probes = 9 # 9次
```
> [!tip] 为什么 Go http.Server 默认没有 keepalive?
> `http.Transport` 的 IdleConnTimeout 默认是 90s,但底层的底层 keepalive 由操作系统控制。高并发服务通常需要手动调小 `SetKeepAlivePeriod()` 和内核参数。
## 紧急数据(URG)
```
[普通数据] [URG flag set] [+1 urgent data byte] [普通数据]
^^^^
"请立刻处理这个字节!"
```
紧急指针指向 URG 标志后的那个字节。但由于许多应用忽略 URG 功能(Nagle 算法可能与 URG 冲突),现代实践中极少使用。
## 粘包与拆包的根因
> ⚠️ 注意:这个问题虽然在本节讨论,但它其实是应用层协议的设计问题,详见 [[hhs/NETWORK/TCP粘包与拆包]]
### 为什么会粘包?
```
发送端 write("HELLO") → TCP 缓冲 → 合并成一个段 "HELLOWORLD" → 接收端 read() 拿到一整串
发送端 write("WORLD") → 因为 Nagle 算法延迟发送,合并了上一段
```
### 为什么会拆包?
```
发送端 write("HELLO WORLD THIS IS LONG") → 超过 MSS → TCP 自动分片
接收端 read(5) → 只读到 "HELLO" → 剩下在缓冲区等待下次 read()
```
## 关联笔记
- [[hhs/NETWORK/TCP粘包与拆包]] — 应用层解决方案详解
- [[hhs/NETWORK/TCP拥塞控制]] — 重传与拥塞窗口联动
- [[hhs/NETWORK/TCP流量控制]] — 滑动窗口管理
@@ -0,0 +1,189 @@
---
tags: [计算机网络, TCP流量控制, 拥塞控制, BBR, Cubic]
create time: 2026-05-18 02:00
---
# 流量控制与拥塞控制
## 概述
TCP 有两个"刹车"机制:流量控制(防止接收方被淹没)和拥塞控制(防止网络被撑爆)。前者是**端到端**的,后者是**全网协同**的。
## 流量控制(Flow Control)
### 滑动窗口原理
```mermaid
flowchart LR
Sender["发送方"] -->|"seq 100..299"| Recv["接收方<br/>rwnd = 300"]
Recv -->|"ACK=100, window=300"| Sender
Note over Sender:"可用窗口 = min(cwnd, rwnd)"
Note over Recv:"rwnd = RecvBufSize - UnackedData"
```
| 概念 | 含义 |
|------|------|
| **rwnd** (Receive Window) | 接收方的剩余缓冲区大小,告诉发送方"我还能收多少" |
| **cwnd** (Congestion Window) | 拥塞窗口,由发送方根据网络状况自行估算 |
| **实际可用窗口** | `min(rwnd, cwnd)` — 取两者较小值 |
### 零窗口问题
当接收方缓冲区满时,会通告窗口大小为 0:
```
发送方收到 ACK with window=0 → 停止发送数据 → 进入 persist timer 探测状态
```
### Zero Window Probe (ZWP)
Linux 内核的 ZWP 定时发送一个字节的数据来探测窗口是否恢复:
```bash
$ sysctl net.ipv4.tcp_no_metrics_save
net.ipv4.tcp_no_metrics_save = 1 # 避免缓存错误的 RTT 值
```
> [!warning] 死锁场景
> 如果 ZWP 丢失且对端没有响应,连接会永久僵死。解决方案:应用层实现超时重连。
## 拥塞控制(Congestion Control)
### 四种核心算法
```mermaid
flowchart TD
subgraph "慢启动阶段 Slow Start"
A["cwnd = 1 MSS"] -->|"每个RTT翻倍(指数增长)"| B[cwnd ≥ ssthresh?]
end
B -->|"是"| C["进入拥塞避免<br/>(线性增长)"]
subgraph "拥塞避免阶段 Congestion Avoidance"
C -->|"每个RTT+1 MSS<br/>(线性增长)"| D[检测到丢包? ]
end
D -->|"是"| E["ssthresh = cwnd / 2<br/>cwnd = 1 (或 2 MSS)"]
E --> F["回到慢启动<br/>(快速恢复)"]
F --> G["快速恢复后<br/>进入拥塞避免"]
style A fill:#DDA0DD,color:#000
style C fill:#FFD700,color:#000
style E fill:#FF6B6B,color:#fff
style G fill:#98FB98,color:#000
```
### 详细对照表
| 阶段 | cwnd 变化 | 增长速度 | 适用场景 |
|------|----------|---------|---------|
| **慢启动** | 每 RTT × 2 | 指数 | 初始探测、快速恢复后 |
| **拥塞避免** | 每 RTT + 1 MSS | 线性 | 接近容量时的精细调节 |
| **快重传** | 收到 3 个 Dup ACK | 立即重传 | 轻微丢包 |
| **快恢复** | cwnd = max(cwnd/2, 2MSS) | 从减半值开始慢启动 | 伴随快重传 |
### 三种触发事件
| 事件 | 操作 | ssthresh | cwnd |
|------|------|---------|------|
| 3个重复 ACK | 快重传 + 快恢复 | cwnd / 2 | max(cwnd/2, 2×MSS) |
| RTO 超时 | 慢开始 | cwnd / 2 | 1 MSS (Reno/Cubic) / 3 segments (NewReno) |
| SACK 确认部分缺失 | SACK-based fast recovery | 同上 | 同上 |
## Linux 拥塞控制算法对比
### 可选算法列表
```bash
$ cat /proc/sys/net/ipv4/congestion_control
bbr
$ ls /lib/modules/$(uname -r)/kernel/net/ipv4/*_cc.ko*
tcp_cubic.ko tcp_dctcp.ko tcp_htcp.ko tcp_highspeed.ko tcp_hybla.ko tcp_illinois.ko tcp_lp.ko tcp_reno.ko tcp_scalable.ko tcp_vegas.ko tcp_westwood.ko tcp_yeah.ko tcp_bbr.ko
```
### 主流算法对比
```mermaid
flowchart LR
Reno["TCP Reno<br/>• cwnd/2 on drop<br/>• Classic cubic curve"]
Cubic["TCP Cubic (Linux default)<br/>• Non-linear recovery<br/>• Better for high-BDP links"]
BBR["Google BBR v2<br/>• Model bottleneck<br/>• Max throughput, low latency<br/>• No loss-based triggering"]
DCTCP["DCTCP (Datacenter)<br/>• ECN-based<br/>• Near-zero congestion in DC"]
style Reno fill:#DDA0DD,color:#000
style Cubic fill:#FFD700,color:#000
style BBR fill:#98FB98,color:#000
style DCTCP fill:#B0C4DE,color:#000
```
| 特性 | Reno | Cubic (默认) | BBR v2 | DCTCP |
|------|------|-------------|--------|-------|
| 触发方式 | 丢包/重复ACK | 丢包/重复ACK | **延迟模型** | ECN标记 |
| 高带宽利用 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 低延迟 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 数据中心 | ❌ | ✅ | ✅ (推荐) | ✅ (需ECN支持) |
| WAN/广域网 | ✅ | ✅ | ✅✅ | ❌ |
| Google 生产环境 | — | — | ✅ 全部使用 | — |
| 配置复杂度 | 零 | 调参选项多 | 最少 (只需设置目标 bw/rtt) | 需交换机支持 ECN |
### BBR v2 核心思想
BBR 不依赖丢包作为拥堵信号——它直接建立网络管道模型:
```
BBR 维护三个核心测量值:
┌─────────────┬──────────────┬─────────────┐
│ Bottleneck │ Propagation │ In-flight │
│ Bandwidth │ Delay (BDP) │ Data Limit │
│ (最高吞吐) │ (最低延迟) │ (当前水量) │
└─────────────┴──────────────┴─────────────┘
然后动态调整: send_rate ≤ bw AND in_flight ≤ bdp + buffer
```
```bash
# 切换为 BBR
$ sudo sysctl net.ipv4.tcp_congestion_control=bbr
$ echo 'net.ipv4.tcp_congestion_control=bbr' >> /etc/sysctl.conf
# 验证
$ ss --info
... cubic bbr ...
```
## Go 中的实践
### 设置 TCP 选项
```go
import "golang.org/x/net/ipv4"
// 设置 socket 级别的拥塞控制相关参数
conn, _ := net.Dial("tcp", "example.com:80")
c := ipv4.NewConn(conn.RawConn())
c.SetTrafficClass(0) // DSCP/ECN
c.SetNoDelay(false) // Nagle 算法关闭 → 小包立刻发
```
```go
// http.Transport 的连接管理
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
// 自定义 DialContext 以启用 keepalive
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second, // 替代内核默认的 7200s
}
```
## 关联笔记
- [[hhs/NETWORK/TCP段结构与状态机]] — cwnd/rwnd 在 TCP 首部中的 Window 字段
- [[hhs/NETWORK/TCP三次握手与四次挥手]] — 握手中协商的 Window Scale 选项
- [[hhs/NETWORK/TCP粘包与拆包]] — PSH 标志与流量控制的关联
@@ -0,0 +1,222 @@
---
tags: [计算机网络, TCP粘包, TCP拆包, 应用层协议]
create time: 2026-05-18 02:10
---
# TCP 粘包与拆包
## 概述
TCP 是字节流协议(Byte Stream Protocol),没有"消息边界"的概念。这导致发送方多次 write 可能合并成一个段发出(粘包),也可能一个 write 被分成多个段到达(拆包)。**这不是 Bug,这是 TCP 的设计特性**——需要在应用层解决。
> [!QUESTION] 为什么 HTTP/gRPC 不需要处理粘包?
> HTTP/1.1 用 `Content-Length` 或 `Chunked Transfer Encoding` 定义边界;gRPC/Protobuf 每个消息带长度前缀;WebSocket 有 Frame Header。它们都在 TCP 之上抽象了一层消息协议,隐式解决了这个问题。
## 为什么会产生粘包和拆包
### 粘包的成因
```
发送端操作:
write("HELLO") → 进入发送缓冲区
write("WORLD") → Nagle 算法延迟合并
read()/close() → 触发 Fin
实际网络上的 TCP 段: "HELLOWORLD" (一次性发出)
```
触发条件:
| 条件 | 说明 |
|------|------|
| **Nagle 算法** | 小数据合并后统一发送以减少小包数量 |
| **接收端读取慢** | 发送端的多个 write 堆积在发送缓冲区 |
| **SendBuffer 累积** | 多个连续 write 都未触发 flush |
### 拆包的成因
```
发送端操作:
write("A very long message that exceeds MTU...") // 超过 MSS(1460B)
实际网络上的 TCP 段:
Segment 1: [Bytes 0..1459] ← MSS = 1500 - 20(IP) - 20(TCP) = 1460
Segment 2: [Bytes 1460..end] ← 剩余部分
接收端 read(1024): ← 只读了前一部分
返回: "A very long m..." ← 不完整!
```
触发条件:
| 条件 | 说明 |
|------|------|
| **数据包 > MSS** | 超过路径 MTU 自动分片 |
| **接收端 read 大小太小** | 一次只读了一部分 |
| **网络抖动 / RTT 变化** | 数据到达时间不均匀 |
## 四种解决策略
### 方法一:固定长度
每条消息固定 N 字节,接收端按 N 字节切割:
```go
const msgLen = 1024 // 每条消息 1KB
func ReadFixedLength(conn net.Conn) ([]byte, error) {
buf := make([]byte, msgLen)
n, err := io.ReadFull(conn, buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
```
| 优点 | 缺点 |
|------|------|
| 实现简单 | 短消息浪费带宽,长消息不够用 |
| 性能最高 | — |
适用场景:**游戏协议、内部 RPC(如 Dubbo 早期版本)**
### 方法二:特殊分隔符
用特定字符(如 `\r\n\r\n`、`\0`)作为消息结束标志:
```go
func ReadDelimited(conn net.Conn) ([]byte, error) {
var buf bytes.Buffer
for {
b := make([]byte, 1)
_, err := conn.Read(b)
if err != nil {
return nil, err
}
buf.Write(b)
if bytes.HasSuffix(buf.Bytes(), []byte("\r\n\r\n")) {
return buf.Bytes()[:buf.Len()-4], nil
}
}
}
```
| 优点 | 缺点 |
|------|------|
| 简单直观 | 消息体中不能包含分隔符 |
| 类似 HTTP 协议 | — |
适用场景:**HTTP/1.1、SMTP、IMAP**
### 方法三:长度前缀 ⭐(推荐)
在消息开头加上表示长度的字段,最常见的有两种变体:
#### 方案 A:4 字节整数 + 消息体
```go
// 最常用格式: [4-byte length][payload...]
// Go encoding/binary 处理
func WriteWithLength(conn net.Conn, data []byte) error {
length := uint32(len(data))
binary.Write(conn, binary.BigEndian, length) // 大端序保证跨语言兼容
conn.Write(data)
return nil
}
func ReadWithLength(conn net.Conn) ([]byte, error) {
var length uint32
binary.Read(conn, binary.BigEndian, &length)
buf := make([]byte, length)
if length > 0 {
_, err := io.ReadFull(conn, buf)
if err != nil {
return nil, err
}
}
return buf, nil
}
```
```
内存布局:
┌─────────────┬──────────────────────────┐
│ Length(uint32) │ Payload │
│ 4 bytes │ variable bytes │
│ 100 │ [actual data of 100B] │
└─────────────┴──────────────────────────┘
```
#### 方案 B:protobuf 风格(L-Type-V)
```
┌────────┬───────┬──────────────────────────┐
│ Length │ Type │ Value │
│ VarInt │ VarInt │ variable bytes │
│ (1-10B) │(1-10B) │ │
└────────┴───────┴──────────────────────────┘
```
| 优点 | 缺点 |
|------|------|
| ✅ 最灵活,支持变长消息 | ❌ 比固定长度略复杂 |
| ✅ gRPC/Protobuf/Hessian2 都用此方式 | ❌ 需要处理超大消息 |
| ✅ 跨语言天然兼容 | — |
| ✅ 无分隔符冲突问题 | — |
### 方法四:两者结合(工业级)
```go
// 工业级协议帧结构
// ┌──────┬──────┬──────┬──────────┬──────┬──────┐
│ Magic │Ver │Type │RequestID │Length│Payload │CRC │
│ 4B │1B │1B │ 4B │4B │Var │4B │
│ 0xABCD│ 1.0 │REQ │ 0x0001 │1024 │data...│ CRC32│
│ EF header │ │ Footer │
└──────┴──────┴──────┴──────────┴──────┴────────┘
总开销: 4+1+1+4+4+4 = 18 bytes overhead
```
```go
type FrameHeader struct {
Magic uint32 // 魔数,防解析错误
Version byte // 协议版本
MessageType byte // 请求/响应/心跳等
RequestID uint32 // 关联请求响应
Length uint32 // Payload 长度
Checksum uint32 // CRC32 校验
}
```
## 各语言的内置支持
| 语言/框架 | 粘包处理 | 说明 |
|-----------|---------|------|
| **Go** stdlib `net` | ❌ 零拷贝,完全手动 | 但 `bufio.Scanner` 可帮你 |
| Go `encoding/binary` | ✅ Binary 编解码 | 配合 length-prefix |
| Java Netty | ✅ 内建 | `DelimiterBasedFrameDecoder`、`LengthFieldBasedFrameDecoder` |
| Python asyncio | ❌ 需手动 | 或用 `asyncio.Protocol` |
| C++ Boost.Asio | ❌ 需手动 | 或用自定义解包器 |
| Rust tokio | ❌ 需手动 | `tokio_util::codec` crate 支持 |
## Nagle 算法的影响
Nagle 算法的初衷是减少小包的发送数量——它规定:**如果有一个未被确认的小包,就不要发送新的小包**。
```go
// Go net.TCPConn 中关闭 Nagle
conn.(*net.TCPConn).SetNoDelay(true) // true = 禁用 Nagle
```
| 场景 | 建议 |
|------|------|
| HTTP/Web API | 关闭 (减少首包延迟) |
| 实时游戏/金融 | 关闭 (每毫秒都很重要) |
| 大数据传输 | 开启 (减少小包开销) |
| SSH | 关闭 (交互性要求高) |
## 关联笔记
- [[hhs/NETWORK/TCP段结构与状态机]] — TCP 为何是字节流而非消息流
- [[hhs/NETWORK/HTTP请求响应]] — HTTP 如何用 Content-Length 解决此问题
- [[hhs/NETWORK/WebSocket全双工通信]] — WebSocket Frame Header 自带 Length
@@ -0,0 +1,232 @@
---
tags: [计算机网络, UDP, SCTP]
create time: 2026-05-18 02:20
---
# UDP 协议与 SCTP
## 概述
UDP 是 TCP 的对立面:没有连接、不可靠、无拥塞控制——但正因如此,它在延迟敏感场景和无差错检测场景中不可替代。SCTP 则是被低估的"第三条路",结合了 TCP 和 UDP 的优势。
## UDP 首部格式
```
┌─────────────┬─────────────┐
│ Source Port │ Destination Port │ 各 2 bytes
├─────────────┼─────────────┤
│ Length │ Checksum │ 各 2 bytes
└─────────────┴─────────────┘
总长: 8 bytes(固定!)
```
| 字段 | 说明 |
|------|------|
| Source/Dest Port | 同 TCP |
| **Length** | UDP 包总长(含首部),最小 8 字节 |
| **Checksum** | 可选!(IPv4 中如果为 0 表示无校验;IPv6 中强制启用) |
### 极简首部的代价
```
TCP 20B 首部 vs UDP 8B 首部
一个 IP 包 = IP(20) + TCP(20) + Data = 40B overhead + N bytes payload
IP(20) + UDP(8) + Data = 28B overhead + N bytes payload
UDP 节省: 12 bytes / packet × 每秒数据包数 = 显著的带宽节省
```
## UDP 核心特性
| 特性 | UDP | TCP |
|------|-----|-----|
| 连接 | ❌ 无连接 | ✅ 三次握手 |
| 可靠性 | ❌ 不保证送达 | ✅ ACK + 重传 |
| 顺序保证 | ❌ 可能乱序到达 | ✅ 有序 |
| 流量控制 | ❌ | ✅ 滑动窗口 |
| 拥塞控制 | ❌ | ✅ 慢启动/快恢复等 |
| 头部开销 | 8 bytes | 20-60 bytes |
| 半双工/全双工 | N/A(无方向概念) | 全双工 |
| 流模式/报文模式 | **数据报模式**(保留边界) | 字节流模式 |
### 为什么 UDP 保留消息边界?
```go
// UDP: send("HELLO") → recv() = "HELLO"
// 一条 UDP datagram 就是一个完整消息
// TCP: write("H"); write("E"); write("L"); write("L"); write("O")
// read() 可能是 "HE", "LLO", "HELL", "LLO"...
```
## UDP 适用场景
### 典型应用
| 应用 | 为什么用 UDP? |
|------|---------------|
| **DNS** | 查询很短(通常 < 512 bytes),等待重传得不偿失 |
| **NTP** | 最新时间最重要,旧值无意义 |
| **VoIP / WebRTC** | 丢一帧比延迟卡顿更可接受 |
| **在线游戏** | 每秒几十次状态同步,延迟比可靠更重要 |
| **DHCP** | 客户端无 IP,无法使用 TCP(需要三次握手)|
| **SNMP** | 监控轮询,丢一次下轮重来即可 |
| **Redis** | 多数操作幂等或客户端自行重试 |
### DNS 用 UDP 还是 TCP?
| 情况 | 协议 | 原因 |
|------|------|------|
| 标准查询 (< 512B) | UDP port 53 | 快,RTT + 少量开销 |
| Zone Transfer (AXFR) | TCP port 53 | 传输大量记录,需可靠 |
| Response > 512B (RFC 5102) | EDNS0 + UDP | 扩展至 4096B |
| EDNS0 不支持,response 大 | TCP fallback | 设置 TC (Truncation) 位,客户端自动切 TCP |
```bash
$ dig example.com +trace # 默认走 UDP
$ dig example.com +tcp # 强制 TCP
$ host -t A example.com # 系统级 dig
```
## UDP 的可靠性增强
虽然 UDP 本身不提供可靠性,但许多基于 UDP 的应用自行实现了轻量级可靠传输:
```mermaid
flowchart LR
App["应用层"] --> U["UDP Datagram"]
subgraph "用户态实现(自加可靠性)"
U --> QUIC["QUIC<br/>内置 ACK+重传+流控"]
U --> DTLS["DTLS<br/>TLS over UDP"]
U --> RTP["RTP/RTCP<br/>媒体流+反馈"]
U --> Custom["自定义可靠协议<br/>如 uTP, KCP, Nakadi"]
end
QUIC -.->|"最终都到"| IPv4["IP"]
DTLS -.-> IPv4
RTP -.-> IPv4
Custom -.-> IPv4
```
### KCP 协议(国内广泛使用)
由俄罗斯开发者 Stan 设计,专为网游低延迟优化:
```
KCP vs TCP in game scenario:
──────────────────────────
TCP: 丢包 → RTO ≥ 1s → 卡顿
KCP: 丢包 → ARQ 立即重传 → < 100ms 恢复
FEC(前向纠错) 补包 → 无需等待确认
```
```go
import "github.com/xtaci/kcp-go"
// Go 中使用 KCP
sess, _ := kcp.Listen(":8080")
conn, _ := sess.AcceptKCP()
conn.SetNoDelay(1, 10, 2, 1) // 无Nagle, 10ms interval, 2 rounds, 1 SSThresh
conn.SetWindowSize(1024, 1024)
conn.SetMtu(1200)
```
## SCTP(Stream Control Transmission Protocol)
### 为何存在?
SCTP (RFC 4960) 试图结合 TCP 和 UDP 的优点:
| 特性 | TCP | UDP | **SCTP** |
|------|-----|-----|---------|
| 可靠性 | ✅ | ❌ | ✅ (可选) |
| 顺序保证 | ✅ 全局有序 | ❌ | ✅ **多流独立有序** |
| 多宿支持 | ❌ 单 IP | ❌ | ✅ 多 IP 冗余 |
| 消息边界 | ❌ 字节流 | ✅ 报文 | ✅ 报文 |
| 拥塞控制 | ✅ | ❌ | ✅ |
| 适用场景 | HTTP/Web | DNS/VoIP | **信令(Diameter/SIP)** |
### Multi-streaming(多流)— SCTP 的王牌特性
传统 TCP 的一个问题:**head-of-line blocking**。如果一个包丢了,所有后续消息都要等它重传完。SCTP 通过多流解决这个问题:
```
SCTP 连接拥有 3 个独立 stream:
Stream 0: [Data_0_A][Data_0_B][Data_0_C] ← 信令通道 (有序)
Stream 1: [Data_1_A] [Data_1_C] ← 聊天消息 (无序 OK)
Stream 2: [Data_2_A][Data_2_B] ← 文件传输
即使 Stream 0 的 B 丢失了 → Stream 1 和 2 继续前进!
```
```
四元组 vs 六元组:
TCP: src_ip, src_port, dst_ip, dst_port → 一个连接
SCTP: src_ip, src_port, dst_ip, dst_port → association (关联)
+ stream_id → stream within association
```
### SCTP 的使用场景
| 场景 | 说明 |
|------|------|
| **Diameter / SIP 信令** | 3GPP/ITU-T 标准强制使用 SCTP over IP |
| **SS7 over IP (SIGTRAN)** | 电信信令迁移到 IP 网络 |
| **数据库复制** | Oracle Data Guard 可配置 SCTP 连接 |
| **游戏服务器** | 多玩家流独立,互不影响 |
### Go 中的 SCTP 支持
```go
import "golang.org/x/net/sctp"
// 创建 SCTP 关联
conn, _ := sctp.DialSCTP("ip4:sctp", "&lt;local&gt;:3868", "&lt;peer&gt;:3868")
// 设置多流
conn.Set SCTPNodeAddresses(sctp.SCTP_PEER_ADDR_CHANGE, ...)
// 发送指定 stream 的消息
conn.WriteToSCTP([]byte("hello"), &sctp.SCTPAddr{
PeerAddr: net.SCTPAddr{IPAddrs: peerIPs},
Stream: 0, // 选择 stream 0
})
```
> [!tip] SCTP 的生态局限
> 虽然技术层面很优秀,但因为运营商部署迟缓和中间设备(NAT/防火墙)对 SCTP 的不兼容,其普及度远不及 TCP。但在电信行业仍是事实标准。
## UDP 编程示例
```go
package main
import (
"net"
"fmt"
)
func main() {
// 服务端
addr, _ := net.ResolveUDPAddr("udp4", ":8080")
conn, _ := net.ListenUDP("udp", addr)
defer conn.Close()
buf := make([]byte, 4096)
n, remote, _ := conn.ReadFromUDP(buf)
fmt.Printf("Received %d bytes from %v\n", n, remote)
// 客户端 — 注意每条 Write 是一个独立的 UDP 数据报
client, _ := net.DialUDP("udp", nil, addr)
client.Write([]byte("Hello UDP"))
}
```
## 关联笔记
- [[hhs/NETWORK/TCP段结构与状态机]] — UDP 缺少的连接管理功能
- [[hhs/NETWORK/DNS原理与优化]] — DNS 主要使用 UDP port 53
- [[hhs/NETWORK/HTTPS与TLS握手]] — TLS 也有 over UDP 的版本: DTLS