简介:这是中国海洋大学计算机网络课程中TCP Reno版本实验的配套资源包,适合高校网络课程学生或对TCP拥塞控制算法感兴趣的初学者。包内含完整的实验工程与说明,覆盖快速重传、快速恢复、拥塞窗口调整等核心机制,并配有可运行代码与实验分析文档,便于对照学习。资源共135个文件,以84个HTML说明/报告页面、Java与class源码为主,另有XML配置、jar依赖及少量文本记录,整体仅1.48MB,结构清晰,适合直接导入集成开发环境或用作实验参考。已有801人浏览学习,口碑较佳。对想深入理解TCP Reno从理论到实现、以及如何通过模拟网络观察丢包与拥塞行为的读者来说,这是一份能够快速上手、兼具代码与讲解的实用资料。
1. 计算机网络实验里的Reno版本:为什么它是拥塞控制的“基准盘”
计算机网络实验里,TCP拥塞控制中的Reno版本几乎是每个学生都绕不开的“基准算法”。某高校的课程实验要求自己搭一个网络仿真,观察Reno的拥塞窗口(cwnd)在慢启动、拥塞避免、快速重传和快速恢复几个阶段里怎么变化,再和Tahoe版本做对比。这个实验看起来只是跑一个脚本,实际上要踩的坑不少:没丢包时曲线单调上升看不到恢复过程、丢包太猛又触发超时导致算法退化、画出来的图和理论对不上。这篇笔记把我自己复现这套实验的完整路径写出来,从选NS2还是ns-3、最小脚本怎么写、cwnd曲线怎么画,到三组常见的翻车现场,以及最后怎么用对比实验验证你已经跑对了方向。适合正在做课程实验、或者想彻底搞懂Reno版本行为的人照着做一遍。
2. Reno算法在NS2里长什么样:原理与实验选型
2.1 从Tahoe到Reno:一次丢包后的两种反应
Reno版本的核心价值在于它把“丢包后怎么办”这件事改得更聪明了。早期的Tahoe算法收到三个重复ACK后,把ssthresh设为当前cwnd的一半,然后直接把cwnd重置为1,从头开始慢启动。这样做保守,但代价是链路立刻空出来后吞吐量塌方。Reno版本在Tahoe的基础上补上了快速恢复:收到第三个重复ACK时进入快速重传,把cwnd减半并加上3个报文段,此后每收到一个重复ACK就把cwnd临时增加一段,直到ACK确认了新数据,再退出快速恢复,回到拥塞避免阶段。
课程实验让你观察Reno版本,本质上就是让你亲眼看到这几个状态切换。你需要在仿真里制造一次丢包,然后从cwnd时间序列里读出:丢包前是指数增长的慢启动,还是线性增长的拥塞避免;丢包后是cwnd减半还是直接归零。只看教科书上的伪代码很难有体感,跑一次NS2的trace曲线,比背十遍状态机都管用。很多同学把Tahoe和Reno跑成了同一条线,就是因为没有制造出足以触发快速重传的重复ACK,最后都走了超时重传路径,算法差异全被掩盖了。
这里还要提一下“快速恢复”的触发条件。它依赖接收方收到乱序包后立刻回复重复ACK,也就是所谓的“报文段已经离开网络,但它后面的包还在路上”。所以链路带宽延迟积(BDP)太小、窗口还没打开几个段就丢包时,即使丢了包也凑不齐三个重复ACK,Reno版本只能超时。这也是实验里“丢包率不是越高越好”的根源。
2.2 为什么课程实验几乎都用NS2而不是ns-3
我接触到的课程实验资料,绝大多数都是NS2版本。原因很实际:NS2用Tcl写仿真脚本,几十行就能跑通一个Reno实验;ns-3虽然更现代,但拓扑、协议栈、应用层的配置要写C++或Python,对一个只想看cwnd曲线的课程作业来说,学习成本高到不值。NS2里Agent/TCP/Reno和Agent/TCPSink都已经封装好,你要做的只是创建三个对象:两个节点、一条带丢包的双向链路、一个FTP流。
另一个选NS2的理由是trace格式足够简单。NS2的trace文件每一行都是约10个字段,自己用awk就能把吞吐量、丢包数、RTT全部抽出来,这对于实验报告里的数据分析环节友好。ns-3的标准trace更结构化,但很多人第一步都被它的事件系统和命令行参数折腾得够呛。你如果只想快速得到一条可以放进实验报告的cwnd曲线,NS2是最稳的路径。
不过我也要提醒一句:NS2的安装是第一个大坑。它依赖的OTcl/Tk版本都比较老,在较新的Linux发行版上编译时容易报错。常见的做法是装一个2.35版本的虚拟机镜像,或者用Docker拉现成的NS2镜像,省去编译依赖的时间。如果你在Windows上做,我的建议是不要直接在Windows下硬折腾,用WSL2里跑纯命令行模式就够了,反正我们的实验只需要ns和gnuplot,不依赖NAM图形窗口。
3. 写一个最小Reno仿真脚本:Tcl代码与参数说明
3.1 拓扑、代理和FTP流:最小脚本骨架
下面这段脚本是我做这个实验时最常用的一套骨架,它搭建了一个两节点拓扑,节点0向节点1发送FTP数据,发送方的TCP代理显式指定为Reno版本。链路带宽设成1Mb、延迟50ms,队列类型DropTail,并把队列上限设为10个包。链路丢包率我用ErrorModel单独挂上去,而不是直接在链路参数里写,这样方便后续做多个实验时只改一个值。
# reno.tcl - 最小TCP Reno拥塞控制仿真脚本 set ns [new Simulator] # 打开trace文件,记录所有网络事件 set tracefile [open reno.tr w] $ns trace-all $tracefile # 创建两个节点 set n0 [$ns node] set n1 [$ns node] # 双向链路:1Mb带宽,50ms传播延迟,DropTail队列 $ns duplex-link $n0 $n1 1Mb 50ms DropTail # 队列长度上限为10个包 $ns queue-limit $n0 $n1 10 # 丢包模块:0.01的随机丢包率,按包丢弃 set loss [new ErrorModel] $loss set rate_ 0.01 $loss set unit_ pkt $loss set eventtype_ Packet $ns lossmodel $loss $n0 $n1 # 发送方:显式使用Reno版本,而不是默认的Tahoe set tcp [new Agent/TCP/Reno] $tcp set fid_ 1 $tcp set packetSize_ 1460 $ns attach-agent $n0 $tcp # 接收方:TCPSink(接收端负责回ACK) set sink [new Agent/TCPSink] $ns attach-agent $n1 $sink $ns connect $tcp $sink # 应用层:FTP无限流,从0.1秒开始,10秒停止 set ftp [new Application/FTP] $ftp attach-agent $tcp $ns at 0.1 "$ftp start" $ns at 10.0 "$ftp stop" # 仿真结束时间,留0.5秒让队列里的包清空 $ns at 10.5 "exit 0" $ns run这段脚本里最关键的一行是set tcp [new Agent/TCP/Reno]。如果只写Agent/TCP,NS2里默认的是Tahoe行为,那你后面画出来的曲线就是Tahoe版本,实验结论全错。$tcp set fid_ 1用来标记流ID,后面用awk统计吞吐量时,靠这个字段过滤出属于本实验TCP流的数据包。packetSize_设为1460字节,这是常见的MSS取值,它决定了窗口的“段数”含义,也影响BDP的计算。
ErrorModel挂在双向链路上,方向是从n0到n1。rate_ 0.01表示每个包有1%的概率被丢弃,unit_ pkt表示丢包单位是包而不是时间间隔。这个值是我调整过比较稳的起步值,既能让trace里出现不少d事件,又不会因为丢包太密集导致TCP频繁超时。队列上限10个包对这个BDP(约6.25个段)来说够用,窗口展开后能攒出足够的在途包来触发快速重传。
3.2 记录cwnd的两个关键动作
只跑脚本只能得到trace文件,拿不到cwnd的逐时刻值。我通常在同一个脚本里加一段记录过程,每隔0.05秒采集一次当前发送方的拥塞窗口,追加写入cwnd.dat。两个关键动作是:定义record过程时用global声明变量,以及用$ns at一次性调度后让过程自己递归调度自己。
# 打开cwnd数据文件 set cwfile [open cwnd.dat w] # 记录过程:每0.05秒写一行 "时间 cwnd" proc record {} { global ns tcp cwfile set now [$ns now] set cwnd [$tcp set cwnd_] puts $cwfile "$now $cwnd" $ns at [expr $now + 0.05] record } # 在TCP开始发送前启动记录 $ns at 0.05 record$tcp set cwnd_是直接读取OTcl实例的内部变量cwnd_,单位是报文段数,不是字节。有人会在这里踩坑,以为输出值和Wireshark里看到的字节窗口一致,实际差了一个packetSize_的倍数。如果你想同时记录ssthresh,可以把puts那行改成puts $cwfile "$now $cwnd [$tcp set ssthresh_]",这样画图时就能看到慢启动阈值的变化线条。
采样间隔0.05秒是经验值。间隔太大会漏掉快速恢复期间cwnd的细节,比如Reno在重复ACK阶段每段增加的那一格;间隔太小cwnd.dat会膨胀到几千行,也看不出整体趋势。如果你之后要对曲线做平滑处理,建议在画图时对数据做滑动平均,而不是把采样间隔调得更密。还有一点:record过程在10.5秒exit 0触发前会一直调度下去,所以不需要手动停止,仿真结束自然就被截断。
4. 跑通并抽数据:从trace文件到cwnd曲线
4.1 用awk和gnuplot把曲线画出来
脚本保存为reno.tcl后,在终端里执行ns reno.tcl。如果一切正常,你会看到目录下出现reno.tr和cwnd.dat。cwnd.dat已经是两列纯文本,可以直接给gnuplot;reno.tr则是用来校验“到底有没有发生丢包、丢包后有没有走快速重传”的原始证据。
# 统计trace中接收方收到的属于流ID=1的字节总数,并估算吞吐量 awk '$1=="r" && $8==1 {sum += $6} END {printf "throughput: %.2f Kbps\n", sum*8/9.9/1000}' reno.tr# cwnd.plot set terminal png size 800,500 set output "cwnd.png" set title "TCP Reno Congestion Window" set xlabel "Time (s)" set ylabel "cwnd (packets)" plot "cwnd.dat" with lines title "cwnd"运行gnuplot cwnd.plot后得到png图片。这里的awk对NS2标准trace字段做了假设:$1是事件类型,r表示接收;$6是包大小,$8是流ID。这和我们脚本中$tcp set fid_ 1对应。9.9是FTP实际发送时长(从0.1到10秒),除以后得到平均吞吐量。
画出来的cwnd曲线理论上应该有三种典型形态:前几秒快速上升的慢启动段,到达阈值后转为线性爬坡,丢包发生后cwnd骤降到一半再快速恢复。如果曲线几乎是一条直线升到很高,说明没有丢包发生;如果曲线反复跌到1然后重新指数爬升,说明丢包全部触发了超时而非快速重传。这两种都说明参数没调对,后面第五章会讲排查方法。
4.2 丢包率怎么设:数值与理论带宽的关系
丢包率是这个实验里最敏感的参数。很多人贪方便直接设0.05甚至0.1,结果Reno版本的曲线完全看不出快速恢复,全是超时。原因在于TCP的快速重传需要三个重复ACK,而重复ACK的数量取决于丢包后还有多少个在途包能回来。我们的链路是1Mb带宽、50ms单向延迟,RTT约100ms,带宽延迟积BDP = 1Mbps × 0.1s ÷ (1460×8) ≈ 8.6段。也就是说,发送方窗口撑到8段左右时,链路上最多同时有8个包在飞。这时随机丢掉1个包,接收方最多能产生7个重复ACK,足够触发快速重传。
如果丢包率太高,比如0.05,那么在窗口还没长大到BDP之前就可能连续丢多个包,重传定时器超时,Reno版本退化成和Tahoe一样的慢启动,快速恢复完全展现不出来。我的建议是丢包率先在0.01试,看曲线有没有“缺口”。如果丢包发生得太早,cwnd还没爬到BDP,就再减小到0.005,或者推迟丢包时间。你还可以用ErrorModel的burst_参数制造“短时间内连丢两个包”,专门观察Reno在连续丢包时快速恢复失效的现象,这个在第六章会讲。
# 检查trace中丢包事件的位置和数量 grep "^d" reno.tr | wc -l grep "^d" reno.tr | head -5这行命令能快速确认丢包是否真实发生。如果d事件数量为0,说明ErrorModel没挂上,或者丢包率太低、仿真时间太短。注意,NS2的trace里d事件表示队列丢弃,是发送端看得到的丢包;如果包在接收端被检测为失序但没被丢弃,trace里只会有乱序标识,不会出现d事件,这是正常的。
5. Reno实验避坑记录:6个常见问题与排查方法
5.1 脚本跑不起来的3个坑
坑1:终端输入ns reno.tcl直接报command not found。现象是NS2已经装好,但可执行文件路径不在PATH里。原因是安装时没有把ns所属的bin目录加入环境变量。解决:先找到ns位置,比如which ns或ls /usr/local/ns-allinone-2.35/bin/ns,然后export PATH=$PATH:/usr/local/ns-allinone-2.35/bin。如果你用的是别人给的脚本启动ns,那就要检查脚本里的路径是不是写死了某台机器上的目录。
坑2:运行后错误提示unknown procedure "new"或can't read "ns"。现象是脚本在set ns [new Simulator]处报错,或者第一行就被卡住。原因往往是脚本文件保存时带了BOM头,或者被Windows换行符污染,Tcl解释器把第一行当成了乱码。解决:用sed -i 's/\r$//' reno.tcl清理回车,或者直接在Linux下用vim重新保存。还有一种情况是脚本里用了中文注释但文件编码不是UTF-8,也会让Tcl解析报错,我一般全用英文注释。
坑3:挂ErrorModel时提示invalid command name "lossmodel"。现象是脚本执行到$ns lossmodel $loss $n0 $n1时崩溃。原因不是命令不存在,而是$loss变量没正确创建,或者NS2的Simulator类缺少对应方法(极少见,多发生在非常旧的版本)。解决:确认set loss [new ErrorModel]这行没有拼错,并且$ns lossmodel的语法确实是“第一个参数是ErrorModel对象,后面两个是节点”。如果还不行,检查你的NS2是否用的是别人裁剪过的版本,换标准2.35即可。
5.2 曲线不对的3个坑
坑4:cwnd曲线是一条没有下落的直线,直到实验结束都保持高位。现象是图上完全看不到快速恢复的锯齿,只有慢启动和拥塞避免。原因是ErrorModel没有生效,可能rate_设成了0,或者丢包模块挂载之后没有调用$ns run之前的$ns lossmodel,也可能是$tcp set fid_ 1没有生效导致你统计的cwnd不是这条流。解决:先用grep "^d" reno.tr | wc -l确认丢包事件数量不为0;再看是不是丢包率太高导致窗口从未打到BDP,所有丢包都超时,曲线跌到1后重来。调整方向是把丢包率降到0.005到0.01之间,或者把链路带宽提高到2Mb。
坑5:曲线在丢包后一路跌到1,随后又指数猛爬,完全不像是Reno的“降到一半”。现象是快速重传没发生,走了超时重传路径。原因有两种:一是丢包时窗口太小,发出的在途包不足三个,接收方凑不够重复ACK;二是丢包率太高,三个重复ACK中间又丢了包,重传定时器先到期。解决:把丢包率降下来,并且让FTP开始时间提前,比如从0.05秒就启动,使cwnd有足够时间在第一个丢包前长到8段以上。你也可以在trace里检查是否有三个连续的d事件之间夹杂着r事件,确认重复ACK存在。
坑6:把发送方的代理改成Agent/TCP后,跑出来的曲线和Agent/TCP/Reno一模一样。现象是课程要求做Tahoe和Reno的对比实验,但你得到两条完全重叠的线。原因是NS2里Agent/TCP本身默认就是Tahoe行为,而你对比的时候可能两个脚本都用了同一个类名;还有一个隐蔽原因是两个脚本虽然用了不同类名,但丢包率或随机数种子不同导致一个触发了RTO,一个触发了快速重传,结果从曲线形状上看反而差不多。解决:明确对比脚本A用Agent/TCP,脚本B用Agent/TCP/Reno;保证除了代理类名以外,所有拓扑、丢包率、FTP时间都完全一致;同时把丢包率调到能稳定触发快速重传的区间,并固定随机数种子让两个脚本走同一条丢包序列。
# 在脚本开头固定随机数种子,保证两次对比实验的丢包序列一致 global defaultRNG $defaultRNG seed 12345这行代码是NS2里固定随机数种子的常见写法。不加的话,两次运行的随机丢包位置不同,对比实验就变成了“运气对比”。注意不同NS2版本对RNG类名和方法有差异,如果是2.35,上面的写法通常有效。
6. 把Reno实验做深一步:用对比实验验证结果
验证Reno版本没有跑偏,最直接的方法是做一个Tahoe/Reno双跑对比,并故意在特定时刻制造一个单包丢失。下面这张表总结了两种算法在相同丢包条件下的预期行为,我建议你在写实验结论前先按这个框架自检。
| 观察点 | Tahoe | Reno |
|---|---|---|
| 收到3个重复ACK后cwnd | 设为1,重新慢启动 | 设为ssthresh+3,进入快速恢复 |
| 快速恢复期间cwnd | 不存在 | 每收到一个重复ACK加1段 |
| 恢复后进入的阶段 | 慢启动 | 拥塞避免 |
| 连续丢2个包 | 第二次超时可能性高 | 可能退化为超时,这点和Tahoe会重合 |
进阶做法是把ssthresh_初始值设成一个很小的数字,比如4,强迫发送方在慢启动阶段很快进入拥塞避免。这样你能在同一条曲线上看到更短的慢启动、更长的线性爬坡,实验报告里更容易区分两个阶段。我用$tcp set ssthresh_ 4跑过一次,曲线从第1秒开始就在做线性增长,配合0.05秒采样,能清楚看到每次RTT加1段的阶梯感,那是Reno版本拥塞避免最典型的证据。
另一个我常用的验证技巧是:把采样间隔改成0.02秒,然后放大快速恢复区域,看cwnd是否有“一加一减”的毛刺。快速恢复期间每收到一个重复ACK,cwnd临时增加一段,这是Reno版本区别于所有其他版本的指纹。如果这段没有毛刺,说明你看到的只是单纯的超时重传,实验完全失败。
我自己的血泪经验是:一开始太贪心,丢包率直接设0.05,结果整个窗口一直在1到3之间震荡,曲线像梳子齿,根本没法讲清楚任何算法行为。后来老老实实把丢包率降到0.01,并把FTP起始时间提前到0.05秒,才第一次在trace里同时看到了慢启动、拥塞避免、快速重传和快速恢复四个阶段的完整序列。做这种仿真实验,大部分时间不花在写脚本上,而是在反复调参数、看trace、再调参数的循环里。希望这篇笔记能帮你跳过那些我已经踩过的坑,把时间省下来真正读懂Reno版本的每一次cwnd升降。
本文还有配套的精品资源,点击获取