基于 Stanford CS144(2024 Winter, Minnow 框架)的完整协议栈实现 —— 自底向上实现字节流、流重组、序列号回绕、TCP 收发引擎、11 状态连接机、ARP 与 IP 路由。
并在课程范围之外自行扩展了 TCP 拥塞控制(Reno)、Nagle、延迟确认与 SACK,配一套消融实验量化每个机制的独立贡献。
32 项功能测试通过,实现代码分布在 17 个源文件中,约 1350 行。
课程提供 Minnow 框架(构建系统、测试框架、基础类型)与各 Lab 的骨架,以下实现代码均为本人完成:
| 模块 | 文件 | 内容 |
|---|---|---|
| 有序字节流 | src/byte_stream.* |
定容 FIFO 缓冲区,best-effort 写入,生命周期状态管理 |
| 流重组器 | src/reassembler.* |
乱序碎片缓存 + 区间合并去重,窗口容量控制,智能关闭 |
| 序列号回绕 | src/wrapping_integers.* |
32 位回绕序列号 ↔ 64 位绝对序列号转换;解回绕用 O(1) 除法而非 O(n) 迭代 |
| TCP 接收方 | src/tcp_receiver.* |
累计确认,SYN/FIN 处理,{ackno, window_size} 计算 |
| TCP 发送方 | src/tcp_sender.* |
双队列收发分离,RFC 6298 重传定时器 + 指数退避,零窗口探测 |
| TCP 连接状态机 ★ | src/tcp_connection.* |
课程未提供,自行设计:11 状态机、自定义线上段格式(合并收发消息以支持捎带 ACK)、三次握手 / 四次挥手 / TIME_WAIT / RST 异常 |
| ARP 与网络接口 | src/network_interface.* |
IP→MAC 缓存(30s 过期),ARP Request 广播与 5s 重发,积压帧批量发送 |
| IP 路由器 | src/router.* |
最长前缀匹配转发,TTL 递减 + 校验和重算,回声去重 |
★ 标记为课程基线中不存在的模块。
课程的 Lab 只要求「能正确传输」。以下四项是我在跑通之后自己加的,并配了独立开关(use_reno / use_sack / use_delayed_ack / use_nagle),关闭时回退到原始路径:
| 机制 | 说明 |
|---|---|
| TCP Reno 拥塞控制 | 慢启动 / 拥塞避免 / 快重传(3 dup ACK)/ 快恢复;初始 cwnd = 10 MSS(RFC 6928) |
| SACK 选择性确认 | 发送方维护空洞表,精确重传真正丢失的区间,而非整窗重传 |
| 延迟确认 | 顺序到达时合并 ACK(最多等 2 个段或 200ms),乱序立即回 ACK |
| Nagle 算法 | 有在途数据时延迟小段发送,合并成满段 |
tests/tcp_benchmark.cc 建立内存回环:两个 TCPConnection 直连,中间经 droptail 瓶颈(带宽排水 + 缓冲溢出尾丢)——丢包是发送方超发的涌现结果,而非按概率注入。
- 规模:32 MB × 50 种子,低 / 高拥塞两档
- 参数:随机丢包 0
1%、单向延迟 0100 ms、突发 1~3 - 公平性关键:同一 seed 下
decide(seed, byte, tx)确定性判定,不同配置经历完全相同的丢包判定序列,差异可归因到配置本身而非随机性
config 完成率 goodput(KB/s) RST 重传率 ACK/段
===== 低拥塞 =====
all 50/50 154.3KB/s 0 1.0% 0.57
nodelack 50/50 225.8KB/s 0 1.0% 1.00
noreno 50/50 85.4KB/s 0 1.0% 0.65
none 50/50 93.7KB/s 0 1.0% 1.00
===== 高拥塞 =====
all 49/50 97.7KB/s 1 1.3% 0.57
nosack 45/50 16.1KB/s 5 8.2% 0.68
noreno 34/50 1.8KB/s 16 24.2% 0.98
none 37/50 2.0KB/s 13 24.2% 0.99
===== 平均(低+高) =====
all 99/100 126.0KB/s 1 1.2% 0.57
nodelack 100/100 142.5KB/s 0 2.1% 0.99
noreno 84/100 43.6KB/s 16 12.6% 0.81
none 87/100 47.8KB/s 13 12.6% 1.00
| 对比 | 关键差异 | 结论 |
|---|---|---|
| all vs noreno | 高拥塞完成率 49/50 → 34/50;goodput 97.7 → 1.8 KB/s(54×);RST 1 → 16 | Reno 是生存前提:无 cwnd 限速则持续灌爆缓冲,最终 RST 断开 |
| all vs nosack | 高拥塞 goodput 97.7 → 16.1 KB/s(6×);重传率 1.3% → 8.2% | SACK 是精确恢复的关键:无 SACK 只能靠累计 ACK 推断,RTO 重传队头未必是空洞 |
| all vs nodelack | 低拥塞 goodput 154.3 → 225.8 KB/s(无延迟确认快 46%);平均完成率 99 → 100/100 | 延迟确认是双刃剑:低拥塞拖慢 ACK 时钟,高拥塞减少 ACK 风暴。本次参数下弊大于利 |
| all vs nonagle | 平均 goodput 126.0 vs 126.0 KB/s | Nagle 在 bulk 负载下几乎零影响 |
综合最优配置是 nodelack(Reno + SACK + Nagle,不开延迟确认):平均 142.5 KB/s、100% 完成。
初版 benchmark 中 Nagle 完全无效果。排查后确认不是 bug 而是负载问题:写入循环「尽量填满窗口」,发送方把随机 1–1000 B 的写入攒成 1000 B 满段,而 Nagle 只在「要发 < MSS 小段且有在途数据」时拦截——bulk 模式根本不产生小段。
为此补了三种写入模式(argv[4]:0=bulk / 1=interactive / 2=mixed),在 interactive 模式下 Nagle 的段合并效果才显现(ACK/段 0.80 → 0.53)。
minnow/
├── src/ 实现代码(本人完成,见上表)
├── util/ 基础类型 + 自定义 RenoConfig.hh / tcp_message.hh
├── tests/
│ ├── tcp_benchmark.cc 消融实验 benchmark ★
│ ├── fsm_connect.cc 连接建立测试(6 项)★
│ ├── fsm_close.cc 关闭 / RST / TIME_WAIT 测试(5 项)★
│ ├── tcp_connection_test_harness.hh ★
│ └── ... 课程原有测试
├── apps/ scripts/ etc/ 课程框架
└── CMakeLists.txt
★ 为自行新增的文件。
依赖:CMake ≥ 3.24.2、GCC 12+(constexpr std::string 需要 GCC 12 的 libstdc++,GCC 11 无法编译)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j$(nproc)
# 跑全部测试
cd build && ctest
# 单独跑性能基准
./build/tests/tcp_benchmarkt_webget需要联网 —— 该测试会真实发起 HTTP 请求,无网络环境下必然失败,与协议栈实现无关router在整套并行测试下偶发失败 —— 单独运行稳定通过(0.03s),疑与测试间资源竞争有关,尚未定位- 尾部丢包与连续丢包仍需 RTO 兜底 —— 窗口最后一段丢失时无后续 dup ACK 触发快重传,此为 Reno + SACK 的固有限制,需 FACK / RACK 才能消除
- 仅对接模拟瓶颈 —— 目前是内存回环 + droptail 模拟,未接 Linux TUN/TAP 走真实协议栈;真实网络下的 ACK 压缩、乱序等场景未覆盖
- 已删除课程原有的
send_transmit/send_extra两个测试 —— 二者的场景假设与加入拥塞控制后的发送行为冲突(例如假定发送方不受 cwnd 约束),无法同时成立,故移除。代价是发送方的部分边界场景不再有测试覆盖 - SACK 未实现 DSACK 与重排序容忍 —— 收到乱序段时可能重复标记空洞
本项目基于 Stanford CS144(Introduction to Computer Networking)2024 Winter 的公开课程材料与 Minnow 框架自学完成。课程原始 README 见 README.course.md。