一、问题设定

这一讲是整门课最像「设计课」的一讲。我们不是在学一个既成的协议,而是在从零把 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

为什么值得这么改?

  1. 协议更简单,只有一种控制报文
  2. 这个「重复 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. 序号空间必须变大(1 位显然不够)
  2. 发送方要缓存所有未确认的分组(以备重传)
  3. ⭐ **丢包时怎么办?**重传全部,还是只重传丢的那个?——这就是 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 倍。

两条工程结论:

  1. 降低 RTT 比增加带宽更能改善协议效率——这正是 CDN(第 6 讲)和 LEO 星座(Starlink)的核心价值主张。
  2. 高 BDP 链路(“长肥管道”, Long Fat Network)对协议提出了特殊要求:大窗口意味着大缓冲区、大序号空间,以及丢包时的巨大重传代价。TCP 的窗口缩放选项(第 14 讲)和 SACK(第 13 讲)都是为这类链路准备的。

九、随堂自测

  1. 按顺序说出 rdt1.0 → rdt3.0 每一步加入的信道缺陷和被逼出的机制
  2. rdt2.0 的致命缺陷是什么?rdt2.1 如何解决?
  3. 为什么停等协议只需要 1 位序号?如果窗口是 4,需要几位?
  4. 接收方收到一个重复的分组,为什么必须仍然发 ACK?
  5. rdt2.2 去掉 NAK 用什么代替?这个思想在 TCP 里变成了什么机制?
  6. 定时器设置得太短会导致什么?会导致协议错误吗?
  7. 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 长距离链路上做大文件传输,如果内核参数没调好,实际速度可能只有百分之几。