5.3 KiB
5.3 KiB
tags, create time
| tags | 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 的 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]
↑
只重传缺的!
$ 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 过期立即重传:
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
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
}
# 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流量控制 — 滑动窗口管理