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