169 lines
5.3 KiB
Markdown
169 lines
5.3 KiB
Markdown
|
|
---
|
|||
|
|
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流量控制]] — 滑动窗口管理
|