☰
TCP拥塞控制Reno算法原理与Mininet实验全解析:慢启动、快速恢复到cwnd曲线
2026/10/8 9:52:03 网站建设 项目流程

简介:这是一份面向计算机网络课程学习的实验资源,围绕TCP Reno拥塞控制实现展开,适合高校学生、网络方向初学者以及需要完成类似实验的开发者参考。压缩包共135个文件,大小1.48MB,包含84个html说明页面、Java源文件与class编译文件、配置与工程文件等,可直接查看实验原理讲解与运行结构。已有801人学习下载。资源来自中国海洋大学计算机网络实验的Reno版本,内含TCP发送窗口、接收窗口、校验和、定时器等核心类实现,以及快速重传、快速恢复、SSThresh与CWND动态调整、防止中间窗口收缩等关键机制。借助html页面可对照理解代码逻辑与实验流程,java与class文件便于开展二次调试或复现。实验还涉及搭建模拟网络环境、观察丢包与延迟等场景,适合想深入理解TCP拥塞控制算法并动手验证的学习者,也能为后续网络优化与系统设计打下基础。

1. Reno 版本到底在做什么:计算机网络实验里的那个 TCP 拥塞控制算法

如果你是第一次拿到“中国海洋大学计算机网络实验reno版本”这道题,最容易误会的是去搜一个叫“Reno”的软件或插件。实际上它不是一个软件包,而是 TCP 拥塞控制算法的一个具体实现版本。实验要你做的是把一台 Linux 主机的 TCP 协议栈切到 Reno,然后在一个可控的小网络里人为制造拥塞,观察窗口怎么涨、怎么降,再把曲线和教材里那几张经典图对上。这件事能解决的痛点很现实:谢希仁《计算机网络》和《计算机网络自顶向下》里关于拥塞控制的段落,你光靠背是背不出体感的,只有亲眼看到慢启动的指数上升和快速恢复后的锯齿回落后,才算真正“会用” TCP。适合刚学完拥塞控制理论、准备做课程报告或考研复习计算机网络的人,也适合做网络运维和 DevOps 的工程师拿它当数据面调参的基础副本。

2. 先看懂 Reno 的四个机制:从慢启动、拥塞避免到快速恢复

Reno 不是一套高深理论,它是 TCP 拥塞控制里最经典的 AIMD(加性增、乘性减)落地实现。在动手抓包之前,一定要能回答一个问题:一个 TCP 连接在 Reno 的控制下,cwnd(拥塞窗口)到底按什么规则变化?这决定了你后面调队列长度、设丢包率时是有的放矢还是全靠玄学。

2.1 从 Tahoe 到 Reno:一次改进改掉了什么

早期 Tahoe 算法的思路很粗暴:只要检测到丢包,就认为网络拥塞了,然后把 cwnd 降到 1 个 MSS,重新做慢启动。这种做法的优点是好实现,缺点是丢一次包就把好不容易探到的带宽全部放弃,在高带宽高延迟链路上代价非常大。

Reno 在 Tahoe 的基础上加了一个关键机制:快速恢复(Fast Recovery)。它把丢包分成两种场景对待。如果发送端连续收到 3 个重复的 ACK,Reno 判断这不是严重拥塞,而是链路里某个包丢了,后续包还在正常到达;这时它进入快速重传,把 ssthresh 设为当前 cwnd 的一半,cwnd 也跟着减半,而不是归零。随后每收到一个重复 ACK,cwnd 临时增加一个 MSS,用来补偿“已经离开网络但还没被确认”的流量,等收到新的 ACK 后,再把 cwnd 落到新的 ssthresh 上,继续拥塞避免。

这个改动看起来只是“降一半”而不是“降到 1”,却让 TCP 的窗口曲线从 Tahoe 那种断崖式下跌,变成了教科书里标志性的锯齿状。做实验时如果要对比两个版本的差异,最容易观察的就是窗口回落后下一段上升的斜率:Tahoe 会重新出现一段指数上升,而 Reno 直接进入线性上升。

2.2 用一张参数表和最小伪代码把 Reno 钉死

实验前建议把下面这几个参数放在手边,后面调 tc 和 sysctl 时全在跟它们打交道。

参数典型值作用
cwnd初始 1~10 个 MSS发送端当前允许在途的字节数,所有行为围绕它变化
ssthresh初始可设很大慢启动和拥塞避免的分界线
MSS通常 1460 字节单个 TCP 数据段最大长度,窗口计数的最小单位
dupACK 阈值固定为 3收到多少个重复 ACK 触发快速重传/快速恢复
RTO根据 RTT 动态计算超时重传判定,超时事件会让 Reno 回慢启动

下面的伪代码可以帮你把状态机的行为写清楚,实现实验报告里的“算法分析”部分时可以直接改写成 C 或 Python。

def on_ack(new_ack): if cwnd < ssthresh: cwnd += MSS # 慢启动:每个 RTT 窗口翻倍 else: cwnd += MSS * MSS / cwnd # 拥塞避免:每个 RTT 窗口加 1 个 MSS def on_3_dup_ack(): ssthresh = max(cwnd / 2, 2 * MSS) cwnd = ssthresh + 3 * MSS # 快速重传后进入快速恢复 # 快速恢复期间,每收到一个重复 ACK,cwnd += MSS # 收到新 ACK 后 cwnd = ssthresh,回到拥塞避免 def on_timeout(): ssthresh = max(cwnd / 2, 2 * MSS) cwnd = 1 * MSS # 超时只能重新慢启动

这段伪代码有两个地方值得注意。第一,拥塞避免里的MSS * MSS / cwnd是对“每 RTT 只增加一个 MSS”的连续近似,在窗口大时误差很小;第二,快速恢复并不是永久状态,它只持续到收到一个非重复的 ACK 为止,之后窗口立刻切回 ssthresh。很多实验报告里把快速恢复画成窗口持续上升,这是不对的,快速恢复阶段的增量只是对重复 ACK 的补偿,不会一直累加。

2.3 为什么实验题要指定 Reno 而不是 CUBIC

你现在打开的任何一台 Linux 机器,默认拥塞控制大概率是 CUBIC,而不是 Reno。原因不是 Reno 过时了,而是 CUBIC 在高带宽场景下利用率和公平性更好。但对计算机网络实验来说,Reno 有三个不可替代的优点:窗口增长是严格线性的,便于在 60 秒的抓包里观察完整锯齿;算法状态少,抓包和窗口曲线能一一对应;几乎所有教材和考研计算机网络课程都以 Reno 为讲解主线,你看到的“计算机网络自顶向下”“王道计算机网络”里的拥塞控制图,画的基本都是 Reno 的行为。

所以实验指定 Reno 版本,不是为了让你用一套“落后”的算法,而是为了让你在有限的时间内,用最小的变量把拥塞控制最重要的几个行为验证一遍。后面所有命令和参数,都是围绕这个目标展开的。

3. 搭建实验环境:Mininet 的拓扑、队列长度和抓包命令

我习惯用 Mininet 而不是 ns-3,因为 Mininet 直接复用宿主机 Linux 协议栈,你切到 Reno 后,iperf3、tcpdump、ss 这些工具全部能用,实验结论也更接近真实网络。这一章给出一套最小可复现的环境,链路参数是我反复试过比较稳的一版。

3.1 最小拓扑:一台交换机、两个主机、一个小队列

实验不需要复杂的网络拓扑。两台主机接一台交换机,中间链路带宽 10Mbps、时延 10ms,就够了。关键参数是max_queue_size一定要设得小,默认的 pfifo 队列可能放几百个包,Reno 的锯齿会被缓冲完全吞掉。

# mininet_reno.py from mininet.net import Mininet from mininet.link import TCLink from mininet.cli import CLI from mininet.log import setLogLevel setLogLevel('info') net = Mininet(link=TCLink) h1 = net.addHost('h1') h2 = net.addHost('h2') s1 = net.addSwitch('s1') net.addLink(h1, s1, bw=10, delay='10ms', max_queue_size=20, loss=0) net.addLink(s1, h2, bw=10, delay='10ms', max_queue_size=20, loss=0) net.start() CLI(net) net.stop()

链路参数里最值得解释的是max_queue_size=20。这个值决定了瓶颈链路能缓存多少个包,如果设成 200,丢包很难发生,Reno 会一路涨窗直到触发 bufferbloat,曲线完全看不出 AIMD 特征。20 个包在 10Mbps、10ms 链路上大约是 30KB 左右的缓冲,足够让 TCP 在拥塞时产生尾部丢包,又不会把丢包事件拖得太久。loss=0是让基础链路干净,后面我们单独用 tc netem 注入丢包,避免 Mininet 的 loss 参数和队列丢包混在一起不好定位。

启动方式是在宿主机上执行sudo python3 mininet_reno.py,进入 CLI 后先用h1 ping h2确认连通,再依次执行后面的命令。

3.2 把 TCP 拥塞控制切成 Reno:sysctl 方法、验证和收尾

Mininet 启动后,h1 和 h2 共享宿主机的内核网络栈,所以直接用 sysctl 改全局参数即可。

sudo sysctl -w net.ipv4.tcp_congestion_control=reno cat /proc/sys/net/ipv4/tcp_congestion_control # 期望输出 reno,如果不是,先执行 sudo modprobe tcp_reno

第一行是写入 Reno 配置,第二行用来确认当前生效的算法。如果输出里不是 reno,多半是内核没有加载 tcp_reno 模块,modprobe tcp_reno加载后重新执行 sysctl 即可。需要提醒的是,sysctl 对已经建立的 TCP 连接不生效,所以一定要在启动 iperf3 之前完成切换。实验结束后记得改回cubic,避免影响同一台机器上的其他网络服务。

如果你只想让某一条 TCP 流使用 Reno,也可以在每个 socket 创建时用TCP_CONGESTION选项指定,但iperf3不直接暴露这个参数,所以我一般直接改全局 sysctl,实验完再恢复。这个做法的好处是内核协议栈的每一条新连接都会继承设置,连 ss 输出里的信息都自动变成 reno。

3.3 用 iperf3 灌流量,tcpdump 在发送端抓包

环境就绪后,在 h2 上启动服务端,在 h1 上同时抓包和打流量。抓包位置放在发送端 h1-eth0,这样既能抓到发出的数据包,也能抓到返回的 ACK,一个文件里就能还原完整交互。

# 终端 A:Mininet CLI 中 h2 iperf3 -s & # 终端 B:在 h1 上后台抓包 h1 tcpdump -i h1-eth0 -s 96 -w /tmp/reno.pcap & # 终端 C:发送 120 秒单流流量 h1 iperf3 -c 10.0.0.2 -t 120 -P 1

抓包这里的-s 96是只抓每个包的前 96 字节,TCP 头加 IP 头足够分析 seq/ack 了,文件体积会小很多;-t 120让流量持续两分钟,确保能看到至少四五个完整的拥塞窗口锯齿;-P 1强制单条 TCP 流,多流并发会让公平性算法介入,曲线变得很难解释。跑完后先h1 pkill iperf3,再把 pcap 文件用scp拷出 Mininet,也可以直接在宿主机上用ls /tmp/reno.pcap确认文件存在,pcap 默认写在宿主机文件系统里。

4. 把抓包数据还原成拥塞窗口曲线:脚本、参数与曲线特征

拿到 pcap 后,下一步不是直接开 Wireshark 看,而是把它转成一条可绘制的 cwnd 曲线。很多人在这里卡住,因为 tcpdump 抓到的包里并没有一个字段叫“拥塞窗口”。拥塞窗口是发送端内核维护的状态,抓包只能看到数据包的 seq/ack,需要靠 Wireshark 的 TCP 分析功能估算出来。

4.1 用 tshark 导出 bytes_in_flight,替代手工数包

Wireshark 的tcp.analysis.bytes_in_flight字段统计的是“当前网络中未被确认的字节数”,这个值和发送端的 cwnd 高度相关,虽然不等于精确的 cwnd,但曲线形态完全够用来分析。导出命令如下:

tshark -r reno.pcap -T fields \ -e frame.time_relative \ -e tcp.analysis.bytes_in_flight \ -E separator=, > reno_inflight.csv

导出后用 Python 画图,几行就够:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('reno_inflight.csv', names=['time', 'inflight']) df['inflight'] = pd.to_numeric(df['inflight'], errors='coerce') df = df.dropna() plt.figure(figsize=(12, 4)) plt.plot(df['time'], df['inflight'] / 1024, lw=0.8) plt.xlabel('time (s)') plt.ylabel('bytes_in_flight (KB)') plt.title('TCP Reno congestion window shape') plt.grid(True) plt.savefig('reno_cwnd.png', dpi=150)

这段脚本里最重要的参数是tcp.analysis.bytes_in_flight,它由 Wireshark 按 seq 和 ack 的差值推算,单位是字节。画图时除以 1024 转成 KB,Y 轴的量级更直观。有一个细节要说明:在快重传和乱序发生的瞬间,bytes_in_flight 会短暂偏大,因为重传包和原始包同时出现在链路上,分析时不要因为个别毛刺就怀疑脚本错了。如果你不想装 pandas,也可以直接用 matplotlib 的 csv 模块读,结果一样。

4.2 三种窗口形态:慢启动段、线性段、两种降窗

曲线画出来后,对照下面这张表确认行为是否完整:

曲线特征对应算法状态说明
陡峭上升,近似指数慢启动每个 RTT 窗口翻倍,斜率越来越快
缓步上升,近似直线拥塞避免每个 RTT 只加 1 个 MSS,斜率稳定
窗口突然减半后继续缓升快速恢复后3 个重复 ACK 触发,ssthresh 减半
窗口跌到接近零再陡升超时重传RTO 超时,重新慢启动

最容易出现的误判是把“窗口减半”看成“超时”。区分方法很简单:快速恢复后曲线从减半处继续线性上升,不会再出现一段陡峭的慢启动;而超时后曲线先掉到接近底部,再重新出现明显的陡峭段。理想的 Reno 实验曲线应该是一串连续的锯齿:每次尾部丢包触发快速恢复,窗口减半,然后线性爬升,再丢包,再减半。

如果整条曲线从头到尾只有慢启动形态,几乎看不到锯齿,说明链路根本没有触发拥塞,常见原因是队列太大或丢包率太低。如果曲线频繁跌到零,说明随机丢包太狠,触发的全是 RTO 而不是快速恢复。这两种情况都说明链路参数设置不合理,回到第 3 章的参数再调。

4.3 用吞吐量模型校验实验数据

课程实验一般要求你解释“为什么最后吞吐量是这么多”。经典的 Reno 稳态吞吐量近似公式是:

T ≈ 1.22 × MSS / (RTT × √p)

其中 p 是丢包率,RTT 用秒,MSS 用字节,结果单位是字节每秒。把实验里的 MSS=1460,RTT=0.02s,p=0.1% 代进去,会得到一个和 iperf3 实测值同量级的吞吐量,就算模型验证通过。需要注意这个公式只适用于 AIMD 稳态,实验刚开始的慢启动阶段和中间的 idle 时间都会让实际吞吐比理论值略低,所以不要要求完全相等,能对上数量级就说明拥塞控制行为是正常的。

5. Reno 实验的 5 个常踩坑:丢包率、缓冲区、cwnd 读取和多流干扰

这个实验本身不难,但结果能不能写成一份漂亮报告,取决于你能不能用好关键参数。下面五条是我自己做下来翻车最多的地方,每一条都有印象深刻的现场教训。

5.1 丢包率设置不当:Reno 永远进不了拥塞避免

现象:抓包文件里全是重复 ACK 和重传,cwnd 曲线一直在慢启动附近徘徊,流量根本跑不满链路带宽。

原因:在链路层直接用tc qdisc设了 2% 的随机丢包率,对 10Mbps 链路来说这个概率太高了。Reno 每次窗口还没长大就被随机丢包打断,频繁触发 RTO,几乎进入不了拥塞避免的线性增长阶段。

解决:先跑一次没有人为丢包的实验,确认队列溢出能自然触发快速恢复;然后给链路加 0.1%~0.5% 的随机丢包,只用来制造少量乱序和重复 ACK,不要把随机丢包当成拥塞信号。我一般先用loss=0验证锯齿,再逐步加丢包观察 RTO 场景。

5.2 缓冲队列太大:bufferbloat 抹平了锯齿

现象:cwnd 曲线从开始到结束几乎是一条横线,窗口涨到链路带宽对应的上限就不再波动,看不到任何锯齿。

原因:Mininet 默认的max_queue_size太大,链路满了之后数据包全部被缓冲,不会立刻丢弃,TCP 也就感知不到拥塞。窗口会一直涨到缓冲上限,表现为典型的 bufferbloat。

解决:把链路max_queue_size从默认值改小到 20 左右,同时确认 tc 的 qdisc 没有被其他参数覆盖。如果用了tc qdisc add而 Mininet 已经存在根 qdisc,命令会直接报File exists,要先用tc qdisc replace。

5.3 用 ss 或手工数包读 cwnd,得到的是幻觉数据

现象:有人直接用ss -i看snd_cwnd来画曲线,发现数值和抓包对不上;也有人从 tcpdump 里手工数每个 RTT 发了多少个包,结果越数越乱。

原因:cwnd是发送端内部状态,ss 只能看到某个瞬间的快照,采样频率不够;而手工数包无法正确处理重传和重复 ACK,丢包一多就数错。

解决:接受“bytes_in_flight 是对 cwnd 的观测估计”这个前提,用 Wireshark 的字段做整体形态分析,不要追求和内核逐点相同。如果你想拿精确 cwnd,可以挂载内核的 tcp tracepoint 读取snd_cwnd,但那是后话,课程实验用 bytes_in_flight 足够。

5.4 多流并发导致结果不可复现

现象:同一个脚本跑两遍,第一次吞吐量 8Mbps,第二次 5Mbps,曲线形态也完全不同。

原因:iperf3 默认只跑一条流,但如果宿主机上有其他网络流量,或者你不小心执行了两次iperf3 -c,就会变成多流竞争。Reno 的多流公平性会导致窗口振荡同步或带宽分配不稳,实验当然不可复现。

解决:跑实验前用ss -t -p确认只有一条 5201 端口的连接;iperf3 固定-P 1,并且抓包前先 ping 一下确认链路空载。实验结束后立即 pkill iperf3,避免残留进程干扰下一轮。

5.5 模拟器时钟粒度和 RTO 参数互相干扰

现象:丢包发生后,本应该触发快速恢复,但抓包里看到的却是超时重传,窗口直接归零。

原因:某些模拟环境下 RTO 下限计算值非常小,或者链路时延配置和内核 RTO 算法不匹配,一个轻微乱序就被判定成了超时。这个问题在低时延链路和 RTT 抖动大的时候尤其明显。

解决:把链路时延从 1ms 调大到 10ms 甚至 20ms,让 RTT 相对稳定,RTO 计算也就更可靠。如果实验环境允许,检查内核的 RTO 相关参数,但不要在不清楚后果时乱改tcp_rto_min,先通过调大链路延迟解决是更安全的路线。

6. 把 Reno 的快速恢复“逼”出来:单丢包注入与三个观察点

前面的实验是让拥塞自然发生,最后一章教你一个主动验证方法:在一个干净链路里精确制造一次丢包,观察 Reno 如何从快速重传走进快速恢复,再和 CUBIC 跑同一条链路做对照。这个方法写进实验报告非常有说服力。

6.1 用 netem 在链路上注入一次可定位的丢包

先用 0 丢包跑一遍,记下大致 10 秒内发送的包数。然后在 h1 的网卡上替换 qdisc,注入非常低的丢包率:

h1 tc qdisc replace dev h1-eth0 root netem loss 0.05%

这个概率足够低,10 秒内只会丢一两个包,但又能保证实验重复几次后必然出现丢包事件。抓包后用下面的命令把所有重传和重复 ACK 事件抽出来:

tshark -r reno.pcap -Y "tcp.analysis.flags || tcp.analysis.retransmission" \ -T fields -e frame.time_relative -e tcp.seq_raw -e tcp.ack_raw

输出结果里你会看到:一个原始包丢失后,接收端连续发出几个重复 ACK,发送端随后重传,整个时间差通常在 1 个 RTT 以内。这就是快速重传触发快速恢复的完整证据链。

6.2 对照 CUBIC 跑同一拓扑,把 Reno 的辨识特征留在报告里

把 sysctl 切回cubic,保持同一链路参数重跑一遍,再把两条曲线按时间轴归一化画在同一张图里。你会发现 Reno 的上升段几乎是固定斜率,而 CUBIC 在窗口减半后会更快地探测更高带宽。实验报告里不需要评价谁更好,只解释一点:Reno 线性增长带来的可预测性,正是它成为教材教学算法的原因,也是和 CUBIC 差异最直观的体现。

我每次做这个实验,收尾都有一个固定习惯:把 sysctl 改回默认,把 pcap 和导出的 csv 按日期放进同一个目录,再写一个 README 记录链路参数和丢包率。这样一周后回来做对比实验,或者答辩现场被问到“这个参数当时是多少”,都能直接从文档里找出来,而不是靠回忆和截图。实验做完了,这套“可复现、可追溯”的习惯才是真正值钱的东西。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询