一、问题设定
这一讲是整门课最像「设计课」的一讲。我们不是在学一个既成的协议,而是在从零把 TCP 的核心逻辑推导出来。
┌──────────────┐ ┌──────────────┐
│ 发送进程 │ │ 接收进程 │
└──────┬───────┘ └──────▲───────┘
rdt_send()│ │deliver_data()
┌──────▼───────┐ ┌───────┴──────┐
│ 可靠数据传输 │ │ 可靠数据传输 │
│ 协议(发送方) │ │ 协议(接收方) │
└──────┬───────┘ └───────▲──────┘
udt_send()│ │rdt_rcv()
▼ │
═══════════════ 不可靠信道 ═══════════════
四个接口:
| 接口 | 谁调用 | 含义 |
|---|---|---|
rdt_send() |
上层应用 | 请把这些数据可靠地送过去 |
udt_send() |
协议 | 把分组丢给不可靠信道 |
rdt_rcv() |
信道 | 分组到了 |
deliver_data() |
协议 | 把数据交给上层应用 |
方法论:我们逐步给信道加入缺陷,每加一种,就看它逼出什么机制。
二、rdt1.0:完全可靠的信道
假设:信道不出错、不丢包。
发送方状态机(只有一个状态):
┌─────────────────┐
│ 等待上层调用 │◄──┐
└────────┬────────┘ │
│ rdt_send(data)
│ make_pkt(data)
│ udt_send(packet)
└───────────────┘
接收方:
┌─────────────────┐
│ 等待下层调用 │◄──┐
└────────┬────────┘ │
│ rdt_rcv(packet)
│ extract(packet, data)
│ deliver_data(data)
└───────────────┘
**什么都不用做。**这是基线。
三、rdt2.0:信道会出现比特差错
新缺陷:分组内容可能被翻转,但不会丢失。
逼出三样机制:
| 机制 | 作用 |
|---|---|
| 校验和 | 检测差错(第 10 讲的反码校验和) |
| 确认(ACK) | 接收方说「收到了,是好的」 |
| 否定确认(NAK) | 接收方说「收到了,但坏了,重发」 |
这一整套统称 ARQ(Automatic Repeat reQuest,自动重传请求)协议。
发送方状态机
┌──────────────────────┐
┌───▶│ 等待上层调用 │
│ └──────────┬───────────┘
│ │ rdt_send(data)
│ │ sndpkt = make_pkt(data, checksum)
│ │ udt_send(sndpkt)
│ ▼
│ ┌──────────────────────┐
│ │ ⭐ 等待 ACK 或 NAK │──┐ rdt_rcv(rcvpkt) && isNAK(rcvpkt)
│ └──────────┬───────────┘ │ udt_send(sndpkt) ← 重发
│ │ └──────┘(自环)
└───────────────┘
rdt_rcv(rcvpkt) && isACK(rcvpkt)
⭐ 注意「等待 ACK 或 NAK」状态的性质:在这个状态下,发送方不接受上层的新数据。
这就是停等协议(stop-and-wait):发一个,等一个,再发下一个。
rdt2.0 的致命缺陷
问题:如果 ACK 或 NAK 本身被损坏了呢?
发送方收到一个读不懂的响应,它不知道:
- 接收方是否收到了数据?
- 该重传还是该继续?
能重传吗?可以,但接收方会收到重复的分组,而且它无法分辨这是新数据还是重复——这就产生了数据重复问题。
四、rdt2.1:加入序号
解决办法:给分组编号。
发送方在分组里放一个序号
接收方检查序号:
与期望的一致 → 新数据,交付,发 ACK
与上一个相同 → ⭐ 重复分组,丢弃,但仍然发 ACK
⚠️ 重复分组必须仍然发 ACK——因为对方之所以重传,正是因为它没收到(或没看懂)上一个 ACK。不回 ACK 会导致死循环。
关键问题:序号要几位?
⭐ 答案:1 位就够了(0 和 1 交替)。
**为什么?**因为停等协议下,任何时刻信道中最多只有一个未确认的分组。发送方只需要区分「这是我刚发的那个」和「这是上一个的重复」两种情况——一位足矣。
📌 这个协议因此得名「交替比特协议」(Alternating-Bit Protocol)。
状态数翻倍:发送方现在有 4 个状态(等待上层-序号0、等待ACK-序号0、等待上层-序号1、等待ACK-序号1)。
rdt2.1 发送方(简化表示)
[等上层, seq=0] --rdt_send--> [等ACK, seq=0]
▲ │
│ │ 收到 ACK 且未损坏
│ ▼
[等ACK, seq=1] <--rdt_send-- [等上层, seq=1]
│ ▲
└──────────────────────────────┘
收到 ACK 且未损坏
在 [等ACK] 状态下:收到损坏的包 或 收到 NAK → 重发当前分组
五、rdt2.2:去掉 NAK
观察:NAK 其实是多余的。
做法:接收方不发 NAK,而是对最后一个正确收到的分组重复发 ACK(带上序号)。
接收方收到坏的分组 seq=1
→ 不发 NAK
→ 重发 ACK(0) ← 意思是「我还停在 0,1 我没收到」
发送方收到重复的 ACK(0)
→ 明白 seq=1 出了问题
→ 重传 seq=1
为什么值得这么改?
- 协议更简单,只有一种控制报文
- ⭐ 这个「重复 ACK 意味着出问题」的思想,直接演化成了 TCP 的快速重传(第 13 讲)
🎯 rdt2.2 是 TCP 确认机制的直接祖先。TCP 的累积确认和三次重复 ACK 触发快速重传,思想都在这里。
六、rdt3.0:信道还会丢包
新缺陷:分组(包括 ACK)可能彻底消失。
现在发送方面临一个真正困难的问题:等了很久没有 ACK,是数据丢了,还是 ACK 丢了,还是只是慢?
⭐ 关键洞察:发送方无法区分这三种情况,也不需要区分。
解决办法:倒计时定时器 + 超时重传。
发送一个分组时 → 启动定时器
收到 ACK → 停止定时器
定时器超时 → 重传该分组,重启定时器
为什么这样是正确的
| 实际情况 | 超时重传的后果 |
|---|---|
| 数据丢了 | ✅ 重传,正确恢复 |
| ACK 丢了 | 接收方收到重复分组 → 靠序号识别并丢弃,重发 ACK ✅ |
| 只是慢了(过早超时) | 产生重复分组 → 同样靠序号处理 ✅ |
📌 **序号机制让「不必要的重传」变得无害。**这是一个极其重要的设计思想:与其精确判断,不如让错误判断的后果可容忍。
rdt3.0 的四种运行场景
场景 1:正常
发送方 接收方
pkt0 ─────────────▶
◀───────────── ack0
pkt1 ─────────────▶
◀───────────── ack1
场景 2:分组丢失
pkt0 ─────────────▶
◀───────────── ack0
pkt1 ────────╳ (丢了)
[超时]
pkt1 ─────────────▶
◀───────────── ack1
场景 3:ACK 丢失
pkt1 ─────────────▶ (收到了,交付)
╳─────────── ack1(丢了)
[超时]
pkt1 ─────────────▶ ⭐ 重复!接收方靠序号丢弃,重发 ack1
◀───────────── ack1
场景 4:过早超时
pkt1 ─────────────▶
[超时太早]
pkt1 ─────────────▶ ⭐ 重复,被丢弃
◀───────────── ack1(第一次的)
◀───────────── ack1(第二次的) ← 发送方要能忽略多余的 ACK
⚠️ 场景 4 说明了一件事:定时器设太短不会导致错误,但会导致不必要的重传,浪费带宽。如何设置超时时间因此成为一个关键问题——第 13 讲的 RTT 估计就是在回答它。
**至此,rdt3.0 是一个正确的协议。**它就是 TCP 可靠性的骨架。
七、停等协议的性能:为什么必须改
rdt3.0 正确,但慢得不能用。
7.1 利用率公式
定义发送方利用率 = 发送方实际在发送数据的时间占比。
一个完整周期 = L/R(传输一个分组)+ RTT + L/R(ACK 传输,通常忽略)
≈ RTT + L/R
利用率 U = (L/R) / (RTT + L/R)
7.2 经典数值(必须记住这个例子)
链路速率 R = 1 Gbps
往返时延 RTT = 30 ms
分组长度 L = 8000 bit(1000 字节)
传输时延 L/R = 8000 / 10⁹ = 8 μs = 0.008 ms
U = 0.008 / (30 + 0.008) = 0.008 / 30.008 ≈ 0.00027
⭐ 利用率 0.027%。
实际吞吐量 = 8000 bit / 30.008 ms ≈ 267 kbps。
🚨 在一条 1 Gbps 的链路上,只跑出了 267 kbps。 你花钱买的带宽,99.97% 被浪费在等待上。
7.3 时间轴的直观图
发送方 ┃█┃ ┃█┃
└┬┘ └┬┘
8μs 8μs
│◀──────── 30 ms 全在等 ──────▶│
7.4 根源:回到带宽时延积
回顾第 3 讲:
BDP = R × RTT = 1 Gbps × 30 ms = 3×10⁷ bit = 3.75 MB
管道能装 3.75 MB,而停等协议在任一时刻只往里放 1000 字节。
🎯 结论:要跑满链路,必须允许多个分组同时在途。 这就是流水线(pipelining),也是下一讲的全部内容。
7.5 需要多大的窗口
所需窗口 W ≥ BDP / L = 3×10⁷ / 8000 = 3750 个分组
发送方必须能在收到第一个 ACK 之前发出 3750 个分组。
⚠️ 这立刻带来三个新问题(第 12 讲逐个解决):
- 序号空间必须变大(1 位显然不够)
- 发送方要缓存所有未确认的分组(以备重传)
- ⭐ **丢包时怎么办?**重传全部,还是只重传丢的那个?——这就是 GBN vs SR 的分歧点
八、例题(Worked Example)
题目:某卫星链路 R = 100 Mbps,单向传播时延 250 ms(GEO 卫星),分组长 1500 字节。
(a) 计算停等协议的利用率和有效吞吐量。 (b) 要达到 90% 的利用率,窗口需要多大(以分组为单位)? (c) 若改用 LEO 卫星(单向传播时延 20 ms),重算 (b)。 (d) 从 (b)(c) 的对比中,你能得出什么工程结论?
解答:
(a)
RTT = 2 × 250 ms = 500 ms
L/R = 1500 × 8 / 10⁸ = 1.2×10⁻⁴ s = 0.12 ms
U = 0.12 / (500 + 0.12) ≈ 0.00024 = 0.024%
有效吞吐量 = 12000 bit / 0.50012 s ≈ 24 kbps
100 Mbps 的链路跑出 24 kbps —— 比拨号上网还慢。
(b) 要达到利用率 U,窗口 W 满足:
U = W·(L/R) / (RTT + L/R)
0.9 = W × 0.12 / 500.12
W = 0.9 × 500.12 / 0.12 ≈ 3751 个分组
(c) LEO:RTT = 40 ms
W = 0.9 × 40.12 / 0.12 ≈ 301 个分组
(d) **窗口需求与 RTT 成正比。**RTT 降低 12.5 倍,所需窗口也降低约 12.5 倍。
两条工程结论:
- 降低 RTT 比增加带宽更能改善协议效率——这正是 CDN(第 6 讲)和 LEO 星座(Starlink)的核心价值主张。
- 高 BDP 链路(“长肥管道”, Long Fat Network)对协议提出了特殊要求:大窗口意味着大缓冲区、大序号空间,以及丢包时的巨大重传代价。TCP 的窗口缩放选项(第 14 讲)和 SACK(第 13 讲)都是为这类链路准备的。
九、随堂自测
- 按顺序说出 rdt1.0 → rdt3.0 每一步加入的信道缺陷和被逼出的机制。
- rdt2.0 的致命缺陷是什么?rdt2.1 如何解决?
- 为什么停等协议只需要 1 位序号?如果窗口是 4,需要几位?
- 接收方收到一个重复的分组,为什么必须仍然发 ACK?
- rdt2.2 去掉 NAK 用什么代替?这个思想在 TCP 里变成了什么机制?
- 定时器设置得太短会导致什么?会导致协议错误吗?
- R = 10 Gbps,RTT = 100 ms,分组 1500 字节。停等协议的利用率是多少?要跑满链路需要多大窗口?
十、本讲要点回顾
演进表(背下来):
| 版本 | 信道缺陷 | 新增机制 |
|---|---|---|
| rdt1.0 | 无 | 无 |
| rdt2.0 | 比特差错 | 校验和、ACK、NAK |
| rdt2.1 | + ACK/NAK 也会损坏 | 序号(1 位) |
| rdt2.2 | 同上 | 去掉 NAK,用重复 ACK |
| rdt3.0 | + 丢包 | 定时器 + 超时重传 |
- 序号让不必要的重传变得无害——与其精确判断,不如让误判可容忍。
- 停等协议的利用率 = (L/R) / (RTT + L/R),在高 BDP 链路上低到不可接受。
- 要跑满链路,在途数据量必须达到 BDP → 必须流水线。
- 流水线带来三个新问题:序号空间、发送缓存、丢包恢复策略。
十一、自测答案
1. rdt1.0:无缺陷,无机制。rdt2.0:比特差错 → 校验和 + ACK/NAK。rdt2.1:ACK/NAK 自身也可能损坏 → 序号(解决重复问题)。rdt2.2:同样的缺陷,但优化为用重复 ACK 代替 NAK。rdt3.0:分组丢失 → 定时器 + 超时重传。
2. 致命缺陷是:若 ACK/NAK 本身损坏,发送方无法判断接收方是否收到了数据。若选择重传,会产生接收方无法识别的重复分组。rdt2.1 通过在分组中加入序号解决——接收方看到序号与上次相同,就知道这是重复,丢弃数据但仍回 ACK。
3. 因为停等协议下信道中最多只有一个未确认的分组,接收方只需区分「新的」与「上一个的重复」两种情况,1 位足够。窗口为 4 时(GBN),序号空间必须 > 窗口,至少 5 个序号,需要 3 位(提供 8 个序号)。(第 12 讲会精确讨论 GBN 与 SR 各自的序号空间约束。)
4. 因为对方重传的原因,正是它没有收到(或没看懂)上一次的 ACK。如果接收方对重复分组保持沉默,发送方会一直超时重传,形成死锁式的无限重传。回 ACK 才能让发送方推进。
5. 用对最后一个正确接收的分组重复发送 ACK(带序号) 来代替 NAK。在 TCP 中,这个思想演化为累积确认 + 三次重复 ACK 触发快速重传(第 13 讲)——收到重复 ACK 意味着后续分组已经到达但中间有洞。
6. 会导致不必要的重传,浪费带宽,在拥塞时还会加剧拥塞(第 15 讲)。但不会导致协议错误——因为序号机制保证了重复分组会被接收方正确识别并丢弃。这就是为什么 TCP 可以采用「宁可保守也不激进」的超时估计策略(第 13 讲)。
7.
L/R = 12000 bit / 10¹⁰ bps = 1.2 μs = 0.0012 ms
U = 0.0012 / (100 + 0.0012) ≈ 1.2×10⁻⁵ = 0.0012%
有效吞吐量 ≈ 12000 / 0.1 s = 120 kbps
BDP = 10¹⁰ × 0.1 = 10⁹ bit = 125 MB
所需窗口 = 10⁹ / 12000 ≈ 83,334 个分组
📌 8 万多个在途分组——这远远超出 TCP 16 位接收窗口字段(最大 64 KB)的表达能力,必须启用窗口缩放选项。这就是为什么在 10 Gbps 长距离链路上做大文件传输,如果内核参数没调好,实际速度可能只有百分之几。