Files
cs-note/hhs/NETWORK/04-传输层/03-TCP可靠传输机制.md
T
2026-05-24 11:42:38 +08:00

169 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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流量控制]] — 滑动窗口管理