简介:中国海洋大学计算机网络实验(TCP Reno版本)资源包,面向计算机网络课程学生与TCP拥塞控制初学者,聚焦TCP Reno快速重传与快速恢复算法的实验设计与实现。压缩包内含135个文件,约1.48MB,以84个HTML实验文档/说明为主,辅以Java源码、class字节码、XML配置、jar依赖及txt记录,便于直接导入工程查看与运行。已有799人学习浏览,是校内实验常用的参考资料。资源内不仅提供可运行的发送端/接收端类、校验和工具及测试运行入口,还包含完整的实验环境与结果分析文档。学生可按“环境搭建—算法实现—测试分析—结果讨论”流程复现实验,在模拟网络中观察丢包、延迟和吞吐量指标,深入理解拥塞窗口与慢启动阈值的调节逻辑,为后续网络优化与系统设计打下基础。
1. 课程实验为什么要单独指定Reno:一次对AIMD机制的回归
1.1 从Tahoe到Reno:一段TCP演进主线的缩影
如果你看过《计算机网络》教材里TCP拥塞控制的章节,大概率见过一个曲线图——cwnd先垂直爬升,撞到阈值后改成线性爬坡,然后突然断崖式下跌,再循环。这张图大多数教材都直接标注为Reno或者NewReno的行为。中国海洋大学计算机网络实验指定“reno版本”,本质上是让你亲手把这张图跑出来,而不是停留在背概念的层面。
TCP拥塞控制从Tahoe开始就有了慢启动和拥塞避免,但Tahoe丢包后直接回到cwnd=1重新慢启动,这种做法在带宽已经不算小的链路上浪费严重。Reno在Tahoe基础上加了快速重传和快速恢复:收到3个重复确认就认为网络出现拥塞,但没有完全断流,于是只把ssthresh和cwnd各减半,继续在拥塞避免阶段维持发送。Networking课上讲的AIMD(加性增、乘性减)原则,在Reno身上体现得最纯粹。后来的NewReno、Cubic、BBR本质上都是在Reno这个骨架上做修补和优化——NewReno解决了同一个窗口内多个丢包恢复慢的问题,Cubic换了一条更适合高带宽长链路的增长曲线,但“快恢复后回到拥塞避免”这一核心逻辑,至今还在用。
1.2 Reno实验真正要验证的三个行为
做这个实验之前,先想清楚要验证什么。Reno的核心行为可以拆成三条:
- 慢启动阶段cwnd每个RTT翻倍,指数增长;
- 达到ssthresh后进入拥塞避免,cwnd每个RTT只加1个MSS,线性增长;
- 收到3个重复ACK后,ssthresh设为当前cwnd的一半,cwnd也降为一半,进入快速恢复;若发生超时,则ssthresh减半、cwnd回到1,重新慢启动。
这三条是实验报告的“灵魂”。如果你的仿真脚本跑完,绘制出来的cwnd曲线跟这三条对不上,那实验步骤一定有错;如果对上了,你就把教材上最抽象的一段机制变成了可复现的事实。这也是为什么老师指定“reno版本”而不是直接用默认协议——默认设置往往用了NewReno甚至Cubic,它们的行为更复杂,反而不方便对照教科书做分析。
1.3 为什么不用Linux默认的Cubic
很多同学会有疑问:现在的Linux服务器上默认拥塞控制算法是Cubic,为什么实验不直接做Cubic?Cubic用三次函数描述窗口增长,丢包后的窗口恢复更快,在高速网络上效率高,但它对拥塞的反应不是“减半-线性增加”这种简单规则,数学上分析和画图都不直观。Reno的好处在于参数少、行为简单、每一步都对应一个理论值,拿它来理解“反馈回路”的概念再合适不过。
还有一个隐性原因:实验环境往往是小规模的仿真拓扑,瓶颈带宽和时延都是自己设的,Reno在这种环境下刚好能把各种阶段完整跑出来。Cubic在高带宽下会长时间处于平台期,反而看不出慢启动、快速重传这些细节。所以“reno版本”本身就是一个经过权衡的教学选择。
2. 仿真环境准备:ns-3安装与基线网络拓扑搭建
2.1 环境依赖与安装时要注意的版本差异
海大这个实验我建议用ns-3来做,环境干净、可控性强,而且脚本从提交到复现都方便。ns-3的编译体系在新版本里已经从waf切换到了CMake,如果你拿到的教程还是./waf configure这种老命令,要先确认版本。ns-3.36之后基本都是:
./ns3 configure --enable-examples --enable-tests ./ns3 buildconfigure阶段最容易被卡的是缺少依赖库。Ubuntu/Debian系可以直接装这一组:
sudo apt install g++ python3 python3-dev cmake ninja-build sudo apt install libgsl-dev libsqlite3-dev libxml2-dev装完后跑一下./ns3 build,编译时间取决于机器,通常10到20分钟。第一次build建议耐住性子,不要因为编译输出看起来像卡住就中断,等出现“build finished”才算完。编译完成后可以跑一个简单示例验证环境:
./ns3 run first能正常打印输出,说明环境OK。
2.2 哑铃拓扑:最容易理解拥塞控制的最小网络
为了观察Reno的拥塞控制行为,网络拓扑不需要复杂,一条带瓶颈链路的哑铃状拓扑就够。我用的拓扑是这个:
- 发送节点n0 -> 路由器r0 -> 路由器r1 -> 接收节点n1;
- r0到r1之间是瓶颈链路,带宽设为1 Mbps,单向时延10 ms;
- n0到r0、r1到n1的链路带宽设成10 Mbps,时延1 ms,确保瓶颈只出现在中间链路上;
- 队列管理用最简单的DropTail(尾部丢弃),队列长度设置为关键参数。
为什么必须刻意制造瓶颈?因为拥塞控制说白了就是对瓶颈链路资源的争抢。如果没有瓶颈,TCP发送方永远不会触发拥塞信号,cwnd会一直涨到接收窗口上限,实验也就没有意义。设置瓶颈之后,TCP流量会持续塞满队列并触发丢包,进而看到快速重传、快速恢复的完整链条。
2.3 队列长度参数:直接决定你能看到几次“锯齿”
队列长度这个参数特别值得讲。很多同学随便设一个100个包的缓冲区,结果cwnd曲线几乎不下降,因为缓冲区太大了,丢包迟迟不发生。理论上,瓶颈链路的带宽时延积BDP约为带宽乘以RTT,RTT按两个方向的时延粗算约22 ms,BDP ≈ 1 Mbps × 22 ms ≈ 22 kbit,按每个包约1 KB计算,大约就是3到4个包。实际为了模拟真实设备,我通常把队列长度设为BDP的数倍,比如20个包。如果设成100个包,丢包前需要积累大量排队数据,整个实验可能要很久才能看到一次窗口减半;如果设成2个包,又会过于频繁地丢包,导致大部分时候都在慢启动。这个参数需要根据你的链路调整。
PointToPointHelper bottleNeck; bottleNeck.SetDeviceAttribute("DataRate", StringValue("1Mbps")); bottleNeck.SetChannelAttribute("Delay", StringValue("10ms")); // 瓶颈链路设备 NetDeviceContainer devices = bottleNeck.Install(nodeRouter0.Get(0), nodeRouter1.Get(0)); // 设置队列长度 TrafficControlHelper tch; tch.SetRootQueueDisc("ns3::PfifoFastQueueDisc", "MaxSize", StringValue("20p")); tch.Install(devices);队列管理这里需要再提一句:ns-3中默认的流量控制模块可能会安装CoDel等主动队列管理算法,如果你用的是较新版本,最好显式指定DropTail/Fifo队列,避免CoDel提前丢包,干扰了你对Reno行为的观察。这也是很多实验报告里cwnd曲线“不标准”的隐形原因。
3. 仿真脚本中的关键配置:把TCP协议换成Reno
3.1 SocketType的全局替换与生效范围
ns-3里TCP协议栈是通过TcpL4Protocol的SocketType属性来指定的。默认情况下会选TcpNewReno,要做Reno版本就必须在仿真脚本开头加一行:
#include "ns3/tcp-reno.h" Config::SetDefault("ns3::TcpL4Protocol::SocketType", TypeIdValue(TcpReno::GetTypeId()));这里有个容易被忽略的点:Config::SetDefault设置的是“所有后续创建的TCP套接字”。如果你的脚本里先后创建了多个应用,比如FTP和Web混合流量,那么所有TCP连接都会被替换成Reno。只做单条TCP流实验时没问题,但如果你比较的是Reno和TcpNewReno的差异,就需要在创建Socket后再动态指定类型,或者在两次仿真中分别执行不同配置,不能在一个脚本里混着来。
验证是否设置成功有个土办法:在脚本里打印TcpL4Protocol的SocketType值,或者直接看最终cwnd曲线的形状。Reno在丢包时cwnd只减半,Tahoe会直接跌回1,两种曲线一眼可辨。
3.2 关闭SACK:确保你看到的是“纯Reno”
这里有个细节很多人会踩:ns-3中TcpSocketBase默认可能开启SACK选项,SACK本身不影响快速重传的基本逻辑,但在乱序和多次丢包场景下,SACK会让恢复过程变得比纯Reno更快,导致曲线在某些阶段不够“教科书”。为了实验报告能够严格对应理论,建议在脚本里把SACK关掉:
Config::SetDefault("ns3::TcpSocketBase::Sack", BooleanValue(false));同样的道理,如果想让“3个重复ACK触发快速重传”的行为干净可见,建议把延迟ACK也做一下处理。接收端的延迟ACK机制会把连续两个包的ACK合并成一个,这样收到3个重复ACK需要的乱序报文数量会变化,影响观察。可以通过设置接收端应用的PacketSink的属性,或者直接用EnbAckDelay为0的配置来消除。关闭SACK和延迟ACK后,Reno的行为会更贴近课本描述。
3.3 应用层流量设计:BulkSendApplication + PacketSink
TCP拥塞控制实验的流量最好用长期、饱和的流量来跑,不能只发几秒钟就停了。常用组合是发送端用BulkSendApplication持续推流,接收端用PacketSink接收:
BulkSendHelper bulkSend("ns3::TcpSocketFactory", InetSocketAddress(serverAddr, port)); bulkSend.SetAttribute("MaxBytes", UintegerValue(0)); // 0表示无限发送 bulkSend.SetAttribute("SendSize", UintegerValue(1000)); // 1000字节 PacketSinkHelper packetSink("ns3::TcpSocketFactory", InetSocketAddress(Ipv4Address::GetAny(), port));MaxBytes设置为0很关键,这样发送方会一直有数据要发,拥塞窗口就始终是限制因素。仿真时间一般设成20到60秒。太短跑不出几次拥塞事件,太长后面全是重复的锯齿,徒增处理时间。
3.4 采集cwnd数据:连上CongestionWindow这个信号源
在ns-3中观察cwnd变化不需要去抓报文再解析,TCP协议栈本身就带跟踪源,最常用的是TcpSocketState中的CongestionWindow。连接方式可以这样:
Config::ConnectWithoutContext( "/NodeList/0/$ns3::TcpL4Protocol/SocketList/0/CongestionWindow", MakeCallback(&CwndTracer));注意路径里的$ns3::TcpL4Protocol是向下类型转换的语法,SocketList索引0对应第一个创建的Socket。如果你的脚本里有多个Socket,索引可能对不上,可以先通过Simulator::Schedule打印所有Socket数量来确认。回调函数里把时间和窗口值写入文件:
void CwndTracer(uint32_t oldVal, uint32_t newVal) { std::cout << Simulator::Now().GetSeconds() << " " << newVal << std::endl; }拿到这个文件之后,用gnuplot一画,就是整个实验最核心的交付物。比如:
gnuplot -e "plot 'cwnd.txt' using 1:2 with lines title 'TCP Reno CWND'"提示:节点编号0和1是创建时Order决定的,路由器节点如果先被创建,编号会前置,Socket路径也会变化。跑不通时先打印NodeList确认编号。
4. 运行结果分析与Reno行为验证:从锯齿曲线读故事
4.1 cwnd曲线的四个阶段识别
实验跑完后,打开cwnd数据文件,用gnuplot绘图。下面这个表格是我自己实验过程中的关键判据:
| 时间段/片段 | 曲线特征 | 对应机制 |
|---|---|---|
| 连接建立初期 | 陡峭指数上升,每RTT翻倍 | 慢启动 |
| 第一次达到ssthresh | 转折点,斜率变缓 | 进入拥塞避免 |
| 拥塞避免段 | 斜率接近线性,每个RTT增长1个MSS | AIMD线性增加 |
| 突然掉半 | cwnd骤降到峰值的一半 | 3个重复ACK触发的快速恢复 |
| 从半山腰重新爬升 | 沿用当前ssthresh,线性增长 | 快速恢复后回到拥塞避免 |
| cwnd跌到1 | 从1重新陡峭上升 | 超时重传后重新慢启动 |
对照课本里那张AIMD锯齿图,你会发现Reno版本几乎能复刻它。区别只在于,仿真中的噪音、队列长度、接收窗口上限都会让锯齿的周期和振幅不完全均匀,这是正常的,不是错误。
4.2 用Flow Monitor看吞吐和丢包
除了cwnd曲线,实验报告通常还需要吞吐率、丢包率或者排队时延。ns-3的Flow Monitor模块可以直接统计这些指标:
FlowMonitorHelper flowmon; Ptr<FlowMonitor> monitor = flowmon.InstallAll(); Simulator::Stop(Seconds(30.0)); Simulator::Run(); monitor->CheckForLostPackets(); monitor->SerializeToXmlFile("result.xml", true, true);运行完后查看result.xml,提取出Tx Packets、Rx Packets、Lost Packets、Throughput这些字段。对一个30秒、1 Mbps瓶颈的仿真,如果TCP Reno工作正常,实际吞吐率应该接近0.95 Mbps以上,剩余带宽被协议开销消耗。如果吞吐只有0.5 Mbps,说明参数设计有问题,大概率是队列太短或时延太大,导致TCP频繁进入慢启动,链路利用率上不去。
4.3 和理论值对照:慢启动时间估算
实验报告里如果只是粘贴仿真图,还缺少“理论验证”的说服力。慢启动阶段有个经典估算方法可用:假设初始ssthresh为64个MSS,每个RTT约22 ms,那么从cwnd=1增长到64大约需要6个RTT(2的幂次增长),即132 ms左右。你在cwnd数据文件里找到从开始到2、4、8、16、32、64的时间点,会发现相邻时间差基本都等于一个RTT。把这段代码和理论值对照表写进实验报告,比单纯说“符合预期”要扎实得多。
需要提醒的是,ns-3里慢启动阶段还可能受“初始窗口”设置影响,默认初始cwnd可能是1,也可能按RFC 6928设置为10。如果曲线第一跳就从1跳到2甚至更高,先查初始窗口配置。这不算错误,只是理论计算要对齐。
5. 踩坑实录:为什么我的cwnd曲线不像课本那样
5.1 曲线一路陡峭上涨,从不回落
这是最常遇到的问题。原因通常是瓶颈链路队列太长,或者没有设置瓶颈链路。我见过有同学把三条链路带宽都设为100 Mbps,队列默认1000包,结果整个仿真期间cwnd一路涨到接收窗口上限,实验报告里只有一条几乎没有波动的曲线,根本没法分析。
解决办法:先把瓶颈链路明确设置成1 Mbps或2 Mbps,队列长度设成20包左右,仿真时间拉长到30秒以上。如果还是不出锯齿,可以在脚本里打印丢包事件,确认有没有发生队列溢出。没有丢包就没有拥塞信号,cwnd自然不会下降。
5.2 只有超时重传,没有快速重传
典型现象是cwnd曲线掉到1,然后重新陡峭爬升,整条曲线找不到“只减半”的步骤。这说明TCP没有触发3个重复ACK,而是走了RTO超时路径。可能原因有几个:
- 丢包发生得太集中,队列一下子把窗口内的大量包全部丢弃,接收端无法产生足够的重复ACK;
- 瓶颈队列的丢包策略是随机早期检测,丢包分布不集中;
- 接收端延迟ACK时间太长,重复ACK数量不足。
这个问题的排查顺序是:先用Flow Monitor看丢包数量和时间点,再确认队列管理是DropTail还是CoDel。CoDel这类主动队列管理故意让少量包延迟或者丢弃,反而不容易凑够3个重复ACK。实验里严格保持DropTail队列是保证快速重传可复现的前提。
5.3 曲线减半了,但只有一次,后面全是超时
Reno的一个经典局限是同一窗口内多个丢包时恢复效率差。如果瓶颈队列设置了20个包的缓冲区,而一个窗口内突然涌入超过20个包,就会造成多个包同时被丢弃。Reno收到3个重复ACK后只恢复一个丢包,其他丢包只能靠超时触发,于是曲线表现为“减半一次,紧接着掉回1”。这实际上是Reno机制的正确表现,不算bug,但会让实验图看起来不干净。
想验证纯粹的Reno行为,可以把瓶颈队列缓冲区设大一些,使单次拥塞事件中只丢1到2个包,比如队列长度20p、带宽1 Mbps、时延10ms,发送窗口初始适中,这种配置下大概率能看到漂亮的“减半-爬升-再减半”锯齿。另外,可以在仿真脚本里适当降低接收端发送窗口的上限来减少突发性。
5.4 排查链路总结
我在帮同学排查这个问题时总结了一个顺序,照着走基本都能定位:
- 看拓扑参数:瓶颈链路是否真的存在,带宽和时延是否设置正确;
- 看队列管理方式:DropTail还是主动队列管理,队列长度是否远大于BDP;
- 看TCP协议类型:确认SocketType确实为TcpReno,而不是默认的NewReno;
- 看SACK和延迟ACK是否关闭,是否影响了重复ACK的触发;
- 看发送应用:MaxBytes是否为0,流量是否饱和;
- 看数据文件:从第一次丢包时间点反查那一时刻的cwnd值,对比是超时还是重复ACK触发。
这套流程本身比实验结果更有价值。以后你换到BBR、Cubic或者Veno,排查思路是一样的。
6. 一点个人体会:Reno之外的那些事
这个实验做完后我对“默认值”这个词变得很敏感。教材里讲TCP演进往往一条线走下来,但真实网络里算法选择和参数调优永远是跟场景绑定的。Reno简单,所以适合教学;Cubic、BBR鲁棒,所以适合真实海量网络。实验报告的最后我写了一段总结:把TCP拥塞控制机制抽象成的这些模块,并不是互相替代的版本号,而是在不同带宽、时延、丢包条件下的工程选择。
如果学有余力,可以让实验更完整一些:把脚本里的SocketType改成TcpNewReno,对比同一拓扑下两条曲线的差别;或者改一下瓶颈队列长度,看不同缓冲区下Reno的吞吐变化;再进阶一步,可以引入Veno或者Westwood,观察它们在高丢包链路下的表现。这些都是在海大实验“reno版本”基础上自然延伸的方向。
最后分享一个小技巧:跑完实验后把cwnd数据文件保留好,画图的时候不要只画一条线,把ssthresh的变化曲线也画在同一张图上。这样可以看出每次快速恢复后ssthresh如何更新,也方便回答实验指导书里“为什么拥塞避免阶段的斜率在不同周期内不一样”这类思考题。一张图两条线,报告的说服力会提升不少。
本文还有配套的精品资源,点击获取