一、TCP 的五个特征
| 特征 | 含义 |
|---|---|
| 面向连接 | 传输数据前必须握手(第 14 讲) |
| 可靠、按序的字节流 | ⭐ 没有报文边界(第 9 讲的坑 1) |
| 全双工 | 同一条连接上双向同时传输 |
| 点对点 | 一个发送方,一个接收方(没有多播) |
| 流水线 + 拥塞控制 + 流量控制 | 窗口机制(第 12、14、16 讲) |
二、报文段结构
0 16 31
┌─────────────────────┬─────────────────────────────┐
│ 源端口号 (16) │ 目的端口号 (16) │
├─────────────────────┴─────────────────────────────┤
│ 序号 (32) ⭐ │
├───────────────────────────────────────────────────┤
│ 确认号 (32) ⭐ │
├──────┬────────┬───────────┬───────────────────────┤
│头长(4)│保留(4) │CEUAPRSF(8)│ 接收窗口 (16) ⭐ │
├──────┴────────┴───────────┼───────────────────────┤
│ 校验和 (16) │ 紧急数据指针 (16) │
├───────────────────────────┴───────────────────────┤
│ 选项(长度可变,0–40 字节) │
├───────────────────────────────────────────────────┤
│ 数据 │
└───────────────────────────────────────────────────┘
字段详解
| 字段 | 位数 | 说明 |
|---|---|---|
| 源/目的端口 | 16×2 | 多路复用/分解(第 10 讲) |
| 序号 | 32 | ⭐ 本报文段第一个字节在字节流中的编号 |
| 确认号 | 32 | ⭐ 期望收到的下一个字节的编号 |
| 头部长度 | 4 | 以 4 字节为单位。最小 5(20 字节),最大 15(60 字节) |
| 标志位 | 8 | 见下 |
| 接收窗口 | 16 | 接收方还能接收多少字节 → 流量控制(第 14 讲) |
| 校验和 | 16 | 反码校验和,含伪首部(第 10 讲) |
| 紧急指针 | 16 | 配合 URG,实际上已废弃 |
| 选项 | 0–320 位 | MSS、窗口缩放、SACK、时间戳 |
标志位(从高到低)
| 标志 | 全称 | 作用 |
|---|---|---|
| C | CWR | 拥塞窗口已减小(ECN,第 16 讲) |
| E | ECE | ECN 回显(第 16 讲) |
| U | URG | 紧急数据(废弃) |
| A | ACK | ⭐ 确认号字段有效 |
| P | PSH | 请立即交付给应用 |
| R | RST | ⭐ 重置连接(拒绝/异常) |
| S | SYN | ⭐ 建立连接 |
| F | FIN | ⭐ 关闭连接 |
常用选项
| 选项 | 作用 | 讲次 |
|---|---|---|
| MSS | 最大报文段大小,握手时协商 | 本讲 |
| Window Scale | 把 16 位窗口扩展到最大 1 GB | 第 14 讲 |
| SACK Permitted / SACK | 选择性确认 | 本讲 |
| Timestamps | 更精确的 RTT 测量 + 防序号回绕 | 本讲 |
三、序号与确认号 ⭐
这是 TCP 最容易搞错的部分,也是必考点。
3.1 序号是按字节编号的
⚠️ 不是按报文段编号。
数据流: [ byte 0 ][ byte 1 ][ byte 2 ] ... [ byte 999999 ]
└───── 500 字节一段 ─────┘
报文段 1: 序号 = 0
报文段 2: 序号 = 500
报文段 3: 序号 = 1000
一个报文段的序号 = 它携带的第一个字节在整个字节流中的编号。
📌 初始序号(ISN)不是 0,而是随机的。原因有二:① 避免上一个同四元组连接的残留报文被误认(第 12 讲提到的分组滞留问题);② 防止攻击者猜测序号进行盲注入(第 4 讲的安全威胁)。
3.2 确认号是「期望的下一个字节」
确认号 = 已按序正确收到的最后一个字节的编号 + 1
它是累积确认(GBN 风格):ACK = 1000 意味着「0 到 999 我全收到了,请从 1000 开始发」。
3.3 完整的 Telnet 场景(KR 经典例题)
用户在 Telnet 客户端敲下字母 C。假设客户端 ISN = 42,服务器 ISN = 79。
主机 A(客户端) 主机 B(服务器)
│ │
│ Seq=42, ACK=79, data='C' │
├────────────────────────────────────▶│
│ │ 服务器回显 'C'
│ Seq=79, ACK=43, data='C' │
│◀────────────────────────────────────┤
│ │
│ Seq=43, ACK=80 (纯确认,无数据) │
├────────────────────────────────────▶│
逐条解释:
- A 发 1 个字节,序号 42。同时确认「我期望 B 的第 79 字节」。
- B 收到字节 42,因此确认号 = 43。同时它自己发出的这个字节序号是 79。
- A 收到 B 的字节 79,确认号 = 80。这个报文段没有数据,序号仍标为 43。
⭐ 注意「捎带确认」(piggybacking):确认信息搭在数据报文段上一起发,不用单独发一个包。这是 TCP 全双工带来的效率优化。
3.4 乱序报文段怎么办
TCP 规范没有强制。实现的选择:
- 丢弃(GBN 风格)—— 简单
- ⭐ 缓存(SR 风格)—— 所有现代实现都这样做
但确认号仍然是累积的——即使缓存了后面的数据,也只能确认到第一个空洞之前。
收到: [0-499] [1000-1499] ← 500-999 丢了
缓存 1000-1499,但 ACK = 500 ← 只能确认到空洞前
这个「重复 ACK」正是快速重传的信号(第六节)。
四、RTT 估计与超时
4.1 为什么难
超时时间(Timeout Interval)该设多久?
- 太短 → 不必要的重传,浪费带宽,加剧拥塞
- 太长 → 丢包后恢复太慢,吞吐量下降
而 RTT 是不断变化的(排队时延随负载波动,第 3 讲)。
4.2 SampleRTT 与 EstimatedRTT
SampleRTT:从发出一个报文段到收到其确认所经历的时间。
⚠️ 只对未重传的报文段测量(原因见 Karn 算法)。
单次采样噪声很大,因此用**指数加权移动平均(EWMA)**平滑:
EstimatedRTT = (1 − α) × EstimatedRTT + α × SampleRTT
推荐值 α = 0.125(即 1/8)
⭐ EWMA 的性质:过去的样本权重按指数衰减,近期样本影响更大。展开来看:
EstimatedRTT = α·S_n + α(1−α)·S_{n−1} + α(1−α)²·S_{n−2} + ...
4.3 DevRTT:偏差估计
只有均值不够——还要知道 RTT 波动有多大。
DevRTT = (1 − β) × DevRTT + β × |SampleRTT − EstimatedRTT|
推荐值 β = 0.25
4.4 超时间隔
TimeoutInterval = EstimatedRTT + 4 × DevRTT
└── 均值 ──┘ └─ 安全余量 ─┘
📌 **为什么是 4 倍?**没有严格的理论推导,是工程经验值。含义是:RTT 波动越大,留的余量越大。在稳定链路上超时贴近 RTT,在抖动剧烈的链路上自动放宽。
初始值:TimeoutInterval = 1 秒(RFC 6298)。
4.5 指数退避
⭐ 每次超时后,TimeoutInterval 加倍。
1s → 2s → 4s → 8s → ... (上限通常 60s 或 120s)
**为什么?**超时往往意味着网络拥塞。**如果拥塞时还按原速率重传,只会让拥塞更严重。**指数退避是一种保守的拥塞响应——第 15 讲会看到这个思想的完整版本。
(收到新的 ACK 后,超时间隔恢复由公式计算。)
4.6 Karn 算法
问题:一个报文段被重传后收到 ACK,这个 ACK 是对原始发送的确认,还是对重传的确认?
情形 A: 情形 B:
发送 ──┐ 发送 ──╳(丢失)
│ 慢
[超时] │ [超时]
重传 ──┤ 重传 ──┐
◀───┘ ACK ◀───┘ ACK
若按重传时刻算 → RTT 严重低估
若按原发时刻算 → RTT 严重高估
无法区分,因此任何一种算法都会引入系统性误差。
Karn 算法的解法(两条):
- ⭐ 不对重传过的报文段测量 SampleRTT
- 超时后用指数退避得到新的超时值(因为无法更新估计,只能保守放大)
📌 现代补充:TCP Timestamps 选项在每个报文段里带上发送时间戳,接收方原样回显,从而可以对重传的报文段也精确测量 RTT——它绕过了 Karn 算法要解决的整个问题。这是「加一个字段就消除一类难题」的漂亮例子。
五、TCP 的可靠传输
5.1 简化的发送方逻辑
NextSeqNum = InitialSeqNum
SendBase = InitialSeqNum
loop forever:
事件:收到上层数据
创建报文段(序号 = NextSeqNum)
if 定时器未运行: 启动定时器
发送报文段
NextSeqNum += 数据长度
事件:定时器超时
⭐ 只重传 SendBase 那一个报文段(最老的未确认)
重启定时器(间隔加倍)
事件:收到 ACK,确认号为 y
if y > SendBase:
SendBase = y ← 窗口滑动
if 还有未确认报文段: 重启定时器
else: 停止定时器
else:
⭐ 重复 ACK 计数 +1
if 计数 == 3: 快速重传
⚠️ 注意超时只重传一个报文段——这是 TCP 与 GBN 的重要区别。TCP 不会「回退 N 步」重传整个窗口。
5.2 三种典型场景
场景 1:ACK 丢失
A: Seq=92, 8 字节 ────────▶ B
◀──────╳ ACK=100(丢失)
[超时] 重传 Seq=92 ────────▶
◀───────────────── ACK=100
场景 2:过早超时,累积确认救场 ⭐
A: Seq=92, 8 字节 ─────────▶
A: Seq=100, 20 字节 ───────▶
[Seq=92 超时] 重传 ────────▶
◀──────────────── ACK=100
◀──────────────── ACK=120
⭐ 收到 ACK=120 后,SendBase=120,不再重传 100
场景 3:累积确认掩盖 ACK 丢失
A: Seq=92, 8 字节 ─────────▶
◀──────╳ ACK=100(丢失)
A: Seq=100, 20 字节 ───────▶
◀───────────────── ACK=120
⭐ ACK=120 一次确认了两个报文段,第一个 ACK 丢失完全无害
场景 3 展示了累积确认的价值——这也是第 12 讲说 GBN 对 ACK 丢失鲁棒的具体体现。
六、快速重传(Fast Retransmit)
6.1 动机
超时时间通常远大于 RTT(EstimatedRTT + 4·DevRTT,再加上可能的退避)。等超时才重传,一个丢包就要浪费几百毫秒。
能不能更早发现丢包?
6.2 机制
如果发送方收到对同一数据的 3 个重复 ACK(即总共 4 个相同的 ACK)
→ 认为 SendBase 处的报文段已丢失
→ ⭐ 立即重传,不等超时
为什么重复 ACK 意味着丢包?
发送: seg1 seg2 seg3 seg4 seg5
✓ ╳ ✓ ✓ ✓
接收方收到 seg1 → ACK=期望 seg2
收到 seg3(乱序)→ 缓存,仍 ACK=期望 seg2 ← 重复 1
收到 seg4 → 缓存,仍 ACK=期望 seg2 ← 重复 2
收到 seg5 → 缓存,仍 ACK=期望 seg2 ← 重复 3
⭐ 触发快速重传
后面的报文段能到达,说明网络通着,那么中间那个多半是丢了而不是慢了。
6.3 为什么是「三个」而不是一个 ⭐
因为 IP 网络允许分组乱序到达。(第 17 讲会看到路由变化、多路径负载均衡都会造成乱序。)
- 1 个重复 ACK:极可能只是轻微乱序 → 若立刻重传,会产生大量不必要的重传
- 3 个重复 ACK:说明后面已经有 3 个报文段先到了,乱序到这个程度的概率很低
📌 3 是一个工程折中:足够小以快速反应,足够大以避免误判。这个数字来自 1990 年代的实测经验,一直沿用至今。
七、SACK:选择确认
7.1 累积确认的局限
发送 1,2,3,4,5,6,7,8
丢失 2 和 6
接收方只能 ACK=2(期望 2)
发送方完全不知道 3,4,5,7,8 已经到了
→ 保守的实现会把它们全部重传(GBN 行为)
**一个 RTT 只能修复一个空洞。**在丢包率高或窗口大的链路上,这非常慢。
7.2 SACK(RFC 2018)
在 TCP 选项中携带已收到的不连续块:
ACK = 2
SACK: [3,6), [7,9) ← 意思是「3-5 和 7-8 我收到了」
发送方立即知道:只需重传 2 和 6
- 最多可携带 3–4 个 SACK 块(受 40 字节选项空间限制)
- 握手时通过 SACK-Permitted 选项协商启用
- ⭐ 今天几乎所有 TCP 实现默认启用
📌 有了 SACK,TCP 才真正具备了选择重传的能力——回到第 12 讲的结论:TCP = GBN 的确认框架 + SR 的重传行为。
八、例题(Worked Example)
题目:主机 A 向主机 B 发送数据,初始序号 ISN = 3000,MSS = 1000 字节。A 连续发出 5 个满载报文段。第 2 个报文段(序号 4000)在传输中丢失,其余全部到达。B 支持缓存乱序报文段且启用了 SACK。
(a) 写出 5 个报文段各自的序号。 (b) 写出 B 对每个到达的报文段所发出的确认号。 (c) A 何时触发快速重传? (d) 若启用 SACK,B 的第 4 个 ACK 会携带什么信息? (e) 若某次测得 SampleRTT = 120 ms,此前 EstimatedRTT = 100 ms,DevRTT = 15 ms,计算更新后的超时间隔。
解答:
(a)
seg1: Seq=3000 (3000–3999)
seg2: Seq=4000 (4000–4999) ← 丢失
seg3: Seq=5000 (5000–5999)
seg4: Seq=6000 (6000–6999)
seg5: Seq=7000 (7000–7999)
(b)
收到 seg1 → ACK=4000 (期望 4000)
收到 seg3 → ACK=4000 重复 #1 (缓存 5000–5999)
收到 seg4 → ACK=4000 重复 #2
收到 seg5 → ACK=4000 重复 #3 ⭐
(c) 收到第 3 个重复 ACK(即由 seg5 引发的那个,总共第 4 个 ACK=4000)时触发快速重传,立即重发 Seq=4000 的报文段,无需等待超时。
(d)
ACK = 4000
SACK: [5000, 8000)
(三个块连续,可以合并成一个块。)A 由此明确知道:只有 4000–4999 需要重传,5000–7999 已安全到达。
(e)
EstimatedRTT = 0.875 × 100 + 0.125 × 120 = 87.5 + 15 = 102.5 ms
DevRTT = 0.75 × 15 + 0.25 × |120 − 102.5|
= 11.25 + 0.25 × 17.5 = 11.25 + 4.375 = 15.625 ms
TimeoutInterval = 102.5 + 4 × 15.625 = 102.5 + 62.5 = 165 ms
⚠️ 注意计算 DevRTT 时用的是更新后的 EstimatedRTT(RFC 6298 的顺序实际上是先算 DevRTT 再算 EstimatedRTT;本课程按 KR 教材的顺序,考试时按讲义为准,但要在答案中写清你用的是哪个 EstimatedRTT)。
九、随堂自测
- TCP 序号是按报文段编号还是按字节编号?初始序号为什么随机?
Seq=1500, ACK=3200的报文段,分别在说什么?- 什么是捎带确认?
- 写出 EstimatedRTT、DevRTT 和 TimeoutInterval 的公式。α 和 β 的推荐值是多少?
- Karn 算法要解决什么问题?TCP Timestamps 选项如何绕过它?
- TCP 超时后重传几个报文段?这与 GBN 有何不同?
- 快速重传为什么需要 3 个重复 ACK 而不是 1 个?
- SACK 解决了累积确认的什么局限?
十、本讲要点回顾
- TCP:面向连接、可靠有序的字节流、全双工、点对点。
- ⭐ 序号 = 本段第一个字节的编号;确认号 = 期望的下一个字节编号(累积确认)。
- 初始序号随机化,防残留报文误认与盲注入。
- EstimatedRTT = 0.875·Est + 0.125·Sample;DevRTT = 0.75·Dev + 0.25·|Sample−Est|;Timeout = Est + 4·Dev。
- Karn 算法:不对重传过的段测 RTT,改用指数退避。Timestamps 选项使这一限制不再必要。
- TCP 超时只重传最老的那一个报文段。
- 三个重复 ACK → 快速重传,因为 IP 允许乱序,1 个不足以判定丢失。
- SACK 让发送方一次性得知所有空洞,把 TCP 从「每 RTT 修一个洞」解放出来。
十一、自测答案
1. **按字节编号。**一个报文段的序号是它所携带的第一个字节在整个字节流中的编号。初始序号随机化有两个原因:① 防止上一个同四元组连接残留在网络中的报文段被新连接误认为有效数据;② 防止攻击者猜出序号进行盲注入或连接重置攻击。
2. Seq=1500:「本报文段携带的第一个数据字节,是我这个方向字节流中的第 1500 号字节」。ACK=3200:「你那个方向的字节我已经按序收到了 3199 号为止,请从 3200 开始发」。两者描述的是两个相反方向的流。
3. 捎带确认(piggybacking)是指把确认信息放在反方向的数据报文段的 ACK 字段里一起发送,而不是单独发一个纯 ACK 包。它利用了 TCP 的全双工特性,节省了一个报文的开销。
4.
EstimatedRTT = (1−α)·EstimatedRTT + α·SampleRTT α = 0.125
DevRTT = (1−β)·DevRTT + β·|SampleRTT − EstimatedRTT| β = 0.25
TimeoutInterval = EstimatedRTT + 4·DevRTT
5. 问题是重传歧义:一个报文段被重传后收到 ACK,无法判断这个 ACK 对应的是原始发送还是重传,因而任何 RTT 测量都会有系统性误差(低估或高估)。Karn 算法的解法是干脆不对重传过的报文段采样,并在超时时用指数退避保守放大超时值。Timestamps 选项在每个报文段中携带发送时刻并由接收方原样回显,ACK 中的时间戳直接指明它对应的是哪一次发送,歧义因此消失,重传段的 RTT 也能精确测量。
6. 只重传一个——SendBase 处最老的那个未确认报文段。GBN 会重传整个窗口内所有已发送未确认的分组。TCP 的选择大大减少了重传浪费(并且在 SACK 配合下能进一步精确)。
7. 因为 IP 网络不保证按序交付:路由变化、等价多路径负载均衡都可能造成报文段乱序到达。轻微乱序会产生 1–2 个重复 ACK,若据此立即重传,会导致大量不必要的重传(既浪费带宽,又在拥塞时火上浇油)。3 个重复 ACK 意味着已有 3 个后续报文段先行到达,这种程度的乱序概率很低,判定为丢包更可靠。3 是快速性与准确性之间的工程折中。
8. 累积确认只能告诉发送方「第一个空洞在哪里」,无法表达「空洞之后哪些块已经收到」。因此当窗口内有多个丢包时,发送方每个 RTT 只能修复一个空洞,恢复极慢,且保守实现会重传大量已成功到达的数据。SACK 在选项中列出已收到的不连续块,发送方一次就能知道所有需要重传的部分,把恢复时间从 O(空洞数)×RTT 降到约 1 个 RTT。