💻 语言:Python 3.9+ 或 C++17(只能用 UDP socket)


为什么这个作业最重要

理解 TCP 和实现 TCP 是两回事。 你可以完美复述滑动窗口、快速重传、AIMD——直到你自己写一个,才会发现那些教材上一笔带过的地方藏着多少真正的困难。


提供的框架

课程提供以下文件(不可修改):

lab3/
├── channel.py          # ⭐ 可配置的不可靠信道模拟器
├── test_harness.py     # 自动测试与评分脚本
├── reference_traces/   # 参考的正确行为轨迹
└── YOUR_CODE/
    ├── sender.py       # ⭐ 你要实现
    └── receiver.py     # ⭐ 你要实现

信道模拟器

python3 channel.py --loss 0.05 --reorder 0.02 --corrupt 0.01 \
                   --delay-mean 50 --delay-jitter 20 --bandwidth 10
参数 含义
--loss 分组丢失概率
--reorder ⭐ 乱序概率(会打乱到达顺序)
--corrupt ⭐ 比特翻转概率(考验你的校验和)
--delay-mean / --delay-jitter 单向时延均值与抖动(ms)
--bandwidth 带宽限制(Mbps)
--duplicate ⭐ 分组重复概率

⚠️ 信道对上下行独立生效——你的 ACK 也会丢、也会乱序。


报文格式(必须严格遵守)

 0        8       16       24       32
┌────────┬────────┬────────┬────────┐
│              Seq (32)              │  ⭐ 字节序号(不是分组序号)
├────────────────────────────────────┤
│              Ack (32)              │
├────────┬────────┬─────────────────┤
│ Flags  │ 保留    │   Window (16)   │  Flags: SYN=1 ACK=2 FIN=4 SACK=8
├────────┴────────┼─────────────────┤
│  Checksum (16)  │  Length (16)     │  ⭐ 16 位反码校验和(第 10 讲)
├─────────────────┴─────────────────┤
│           Payload (0–1024)         │
└────────────────────────────────────┘

⚠️ 所有多字节字段使用网络字节序(大端)(第 9 讲的坑 3)。

import struct
HEADER_FMT = '>IIBBHHH'      # ⭐ '>' 表示大端
HEADER_LEN = struct.calcsize(HEADER_FMT)

Part 1:停等协议 rdt3.0(20 分)

实现第 11 讲的 rdt3.0

✅ 16 位反码校验和(检测 --corrupt)
✅ 1 位交替序号
✅ 固定超时(先用 500 ms)
✅ 超时重传
✅ 对重复分组回 ACK 但不重复交付

通过标准

python3 test_harness.py --part 1 --loss 0.2 --corrupt 0.05
# ⭐ 接收到的文件必须与发送的文件【逐字节相同】(用 sha256 校验)

📌 **这一步只考正确性,不考性能。**在 20% 丢包下它会慢得令人发指——这正是你要亲身体会的(第 11 讲的利用率公式)。

必须记录的数据:在 --loss 0、RTT=100ms、文件 1 MB 时的传输时间与吞吐量。与第 11 讲的公式 U = (L/R)/(RTT+L/R) 对比。


Part 2:Go-Back-N(25 分)

✅ 发送窗口 N(命令行可配,默认 16)
✅ ⭐ 累积确认
✅ ⭐ 单个定时器(针对最老的未确认分组)
✅ ⭐ 超时重传窗口内全部已发送未确认分组
✅ 接收方【丢弃】乱序分组,重发上一个 ACK
✅ 序号空间 ≥ N+1(第 12 讲)

必须记录:在丢包率 0%、1%、5%、10% 下的吞吐量与总发送分组数

总发送分组数是关键指标——它直接体现 GBN 的重传浪费。


Part 3:选择重传(25 分)⭐

✅ ⭐ 逐分组确认
✅ ⭐ 每个未确认分组一个定时器(可用一个定时器堆实现)
✅ ⭐ 超时只重传那一个
✅ ⭐ 接收方【缓存】乱序分组,补齐后批量交付
✅ ⭐ 接收窗口左侧的分组仍要回 ACK(第 12 讲的易错点)
✅ 序号空间 ≥ 2N

⚠️ 本 Part 最容易出错的三处

① 忘记对【窗口左侧】的重复分组回 ACK
   → 发送方永远等不到 ACK → 死锁

② 接收窗口滑动时没有把【所有连续已缓存】的分组一次交付
   → 数据顺序错乱

③ 定时器管理:用 N 个 threading.Timer 会在窗口大时开销爆炸
   ⭐ 正确做法:用一个 heapq 维护 (超时时刻, seq),主循环里检查堆顶

必须记录:与 Part 2 在相同参数下的对比表。

丢包率 GBN 吞吐量 SR 吞吐量 GBN 发包数 SR 发包数
0%
1%
5%
10%

报告中必须解释这张表符合还是不符合第 12 讲的理论预测。


Part 4:自适应超时(15 分)

实现第 13 讲的 RTT 估计:

EstimatedRTT = 0.875 × EstimatedRTT + 0.125 × SampleRTT
DevRTT       = 0.75 × DevRTT + 0.25 × |SampleRTT − EstimatedRTT|
Timeout      = EstimatedRTT + 4 × DevRTT

✅ ⭐ Karn 算法:【不对重传过的分组采样 RTT】
✅ ⭐ 超时后指数退避(Timeout 加倍,上限 60 s)
✅ 初始 Timeout = 1 s

必须提交一张图:横轴时间,纵轴同时画出 SampleRTTEstimatedRTTTimeout 三条曲线。

在报告中解释:当信道时延突然增大时(用 --delay-mean 中途变化),三条曲线各自如何反应?为什么 Timeout 的反应比 EstimatedRTT 更"迟钝"?


Part 5:拥塞控制(25 分)⭐

实现第 16 讲的 TCP Reno:

✅ ⭐ 慢启动:cwnd 从 1 MSS 开始,每收到一个 ACK 加 1 MSS
✅ ⭐ 达到 ssthresh 后转入拥塞避免:每 RTT 加 1 MSS
✅ ⭐ 超时:ssthresh = cwnd/2, cwnd = 1,回到慢启动
✅ ⭐ 3 个重复 ACK:ssthresh = cwnd/2, cwnd = ssthresh + 3,进入快速恢复
✅ ⭐ 实际发送窗口 = min(cwnd, rwnd, 发送缓冲区)

必须提交一张 cwnd 演化图:横轴时间(或 RTT 轮次),纵轴 cwnd。

图上必须标注出

① 慢启动阶段(指数增长的那一段)
② 转入拥塞避免的转折点
③ 每一次丢包事件
④ ⭐ 区分【超时】(cwnd 掉到 1)和【3 个重复 ACK】(cwnd 掉到约一半)

📌 **这张图是本作业的标志性成果。**它应该长得和第 16 讲那张锯齿图一模一样——如果不像,说明你的实现有问题。

加分项(+10)

任选其一实现并对比:

⭐ SACK 选项:让发送方一次得知所有空洞,与纯 SR 对比恢复速度
⭐ CUBIC:cwnd 是距上次丢包时间的三次函数,与 Reno 在高 BDP 下对比
⭐ 快速恢复的窗口膨胀/收缩细节(RFC 6582 NewReno)

Part 6:性能实验报告(10 分)

必须包含以下实验(不是描述,是数据):

实验 1:窗口大小 vs 吞吐量

固定 RTT = 100 ms、带宽 10 Mbps、无丢包,测量窗口 = 1, 2, 4, 8, 16, 32, 64 时的吞吐量。

在同一张图上画出理论值 U = min(1, W·(L/R)/(RTT+L/R)) 和实测值,解释差异。

实验 2:丢包率 vs 吞吐量

固定窗口,测量丢包率 = 0.1%, 0.5%, 1%, 2%, 5% 时的吞吐量。

验证第 16 讲的公式:吞吐量是否近似正比于 1/√L?用对数坐标画图,看斜率是否接近 −0.5。

实验 3:BDP 验证

固定带宽,改变 RTT(20/50/100/200 ms),测量"跑满链路所需的最小窗口"。

验证它是否正比于 RTT(即正比于 BDP)。


⚠️ 一票否决项

❌ 传输结果与原文件不一致 → 该 Part 直接 0 分
   (可靠传输协议的第一要务是【正确】,性能是第二位的)
❌ 在无丢包信道上都无法完成传输 → 全作业 0 分
❌ 使用了 TCP socket 而非 UDP → 全作业 0 分

调试建议 ⭐

这是本课程最容易卡住的作业。以下建议来自往届学生的真实困难。

1. 分阶段验证,不要一次写完

① 先在【零丢包、零乱序】的信道上跑通停等 → 验证报文格式正确
② 加 --loss 0.1 → 验证超时重传
③ 加 --corrupt 0.05 → 验证校验和
④ 加 --reorder 0.1 → ⭐ 这一步会暴露大部分序号 bug
⑤ 加 --duplicate 0.05 → 验证重复检测

2. 打日志,但要能关掉

def log(event, **kw):
    if VERBOSE:
        print(f"[{time.time()-T0:8.3f}] {event:12s} " +
              " ".join(f"{k}={v}" for k, v in kw.items()), file=sys.stderr)

log("SEND", seq=seq, len=n, cwnd=round(cwnd,1))
log("RECV_ACK", ack=ack, dup=dup_count)
log("TIMEOUT", seq=send_base, new_rto=round(rto,3))

⚠️ 日志一定要输出到 stderr,否则会污染数据流。

3. 把日志直接变成图

# 边跑边写 CSV,实验结束直接画图
csv_writer.writerow([t, cwnd, ssthresh, rtt_sample, est_rtt, timeout])

📌 **不要等实现完了再想怎么画图。**从第一行代码就开始记录,你会在开发过程中就发现问题。

4. 最常见的五个 bug

bug 症状 原因
序号回绕 长时间传输后突然乱套 没有用模运算比较序号
⭐ 窗口左侧不回 ACK 传输在某一点永久卡住 第 12 讲的易错点
定时器没重启 一次丢包后再也不重传 收到 ACK 后忘记重启定时器
⭐ cwnd 单位混乱 cwnd 图形状怪异 一会儿按分组数一会儿按字节数
校验和算错 无损坏信道也报错 忘了把校验和字段先置 0 再算

5. 用参考轨迹对照

python3 test_harness.py --part 5 --compare reference_traces/part5.trace

它会告诉你你的 cwnd 演化在哪一步开始偏离参考实现。