Files

169 lines
5.3 KiB
Markdown
Raw Permalink Normal View History

2026-05-24 11:42:38 +08:00
---
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流量控制]] — 滑动窗口管理