基于NS2的TCP Reno拥塞控制仿真:从trace到cwnd曲线 简介中国海洋大学计算机网络课程的TCP Reno实验包面向正在学习传输层协议、拥塞控制与可靠传输机制的高校学生及自学者。全套围绕TCP Reno的快速重传、快速恢复、慢启动阈值与拥塞窗口动态调整等核心原理并覆盖实验环境搭建、代码实现和网络行为测试分析可帮助从算法层面理解拥塞控制如何提升网络效率与传输可靠性。资源共135个文件压缩包约1.48MB以Java源码、class字节码及HTML页面为主同时包含XML工程配置、JAR依赖、TXT说明等辅助内容源码中可见TCP_Sender、TCP_Receiver、CheckSum、OverTimerTask等模块对应发送窗口、接收窗口、校验和计算与超时定时等关键环节便于按模块研读和独立运行。目前已有801人学习下载适合作为计算机网络实验报告撰写、协议代码剖析或考前复习的参考资料也可直接参考其中“防止中间窗口收缩”等策略分析加深对TCP性能调优的理解。1. 高校计网课的 Reno 实验为什么跑完仿真你手里只有一坨 trace很多同学做计算机网络实验时把 Reno 的慢启动、拥塞避免、快速重传、快速恢复背得很熟但真到实验周装仿真环境装了半天跑完一段脚本打开输出文件发现全是字符完全不知道从哪里看拥塞窗口的变化轨迹。这个实验的难点不在理解 Reno而在把课堂上的一段话变成一条能画出来的锯齿曲线。我收到过不少求助基本都是同一个问题脚本跑完了但拿不出能证明“Reno 在工作”的图。这篇文章就围绕“基于 NS2 的 TCP Reno 版本仿真实验”展开讲清楚三件事Reno 到底在改哪些参数、最小仿真脚本怎么搭、以及为什么绝大多数人卡在 trace 解析和参数调不出来。适合计网课学生、接手实验课的助教以及想在本地把拥塞控制行为复现一遍的开发者。跳过了“背概念”的部分直接讲可落地的实验路径。2. 先把 Reno 的四个动作记牢慢启动、拥塞避免、快速重传、快速恢复2.1 慢启动与 ssthresh窗口翻倍直到阈值TCP Reno 的拥塞控制围绕两个变量展开拥塞窗口cwnd和慢启动阈值ssthresh。连接刚建立时发送方并不知道可用带宽所以从很小的窗口常见是 1 个 MSS开始试探每个 RTT 内窗口翻倍。这个过程叫慢启动虽然名字里有“慢”实际窗口是爆炸式增长1、2、4、8……直到cwnd碰到ssthresh。在仿真实验里你看到的第一个现象就是 cwnd 陡峭上升。如果脚本里ssthresh_设得很大慢启动会持续很久在那个阶段曲线几乎是一条竖线如果设得太小还没看出指数增长就切换到拥塞避免了。我第一次做这个实验时把 ssthresh 设成了 1000仿真跑了 30 秒曲线还在直线爬升浪费了不少时间。课堂上讲“翻倍增长”只是一句话在图上对应的是前几秒那段急剧拉升的线段这个对应关系值得在实验报告里写清楚。2.2 拥塞避免的加法递增与丢包时的乘法递减当cwnd越过ssthreshTCP 进入拥塞避免阶段。此时窗口不再翻倍而是每个 RTT 大约增加 1 个 MSS也就是线性爬升。这个阶段曲线斜率明显变缓是判断脚本是否配置正确的第二个标志。如果整条 cwnd 曲线只有线性爬升、没有丢包后的大幅回落说明链路上没有注入丢包或者丢包模型没有真正生效。Reno 对丢包的反应是乘法递减一旦检测到丢包ssthresh被更新为当前cwnd的一半cwnd也会降到ssthresh快速恢复阶段或直接掉到 1超时重传。这就是经典锯齿的来源。上升是线和缓的下降是陡峭的。很多实验报告只画出了上升段没有下降段就是因为链路丢包率太低或仿真时间太短这类报告一眼就能看出没跑通。2.3 快速重传与快速恢复为什么 Reno 恢复完 cwnd 砍半丢包检测有两种方式超时重传和重复 ACK。如果接收方收到乱序数据会立即对期望序号发送 ACK 确认发送方收到 3 个重复 ACK就判断数据丢失不等超时就重传这就是快速重传。Reno 的快速恢复机制会在重传后把cwnd减半而不是降到 1这样能继续发送新数据避免链路空闲。这个机制是 Reno 区别于 Tahoe 的核心也是仿真实验里最值得验证的点。你在 cwnd 曲线上会看到一次丢包后窗口不是掉到 1而是降到峰值的一半附近然后继续线性爬升。如果看到掉到 1说明丢包触发了超时而不是重复 ACK可能是丢包率过高、RTT 过大或者接收窗口太窄导致 ACK 无法及时返回。实验报告里如果能抓到一次完整的“快重传-快恢复”曲线含金量会高很多。2.4 为什么用 NS2 而不是直接改 Linux 内核做这个实验首先会面临选型问题真机上跑 TCP 不行吗我的经验是真机只能看到宏观的吞吐和 RTT 统计很难精确控制哪一段链路丢多少包更没法直接读 cwnd 的内部变化。Linux 里可以通过ss -tin观察部分的 cwnd 信息但你需要 root 权限、要控制网络延迟、要制造指定概率的丢包这些在教室的共享网络环境里几乎做不到。NS2 的常见用法是把一条端到端链路拆成固定的拓扑给链路设置带宽、延迟、队列管理和丢包模型TCP 的窗口变化会以事件的形式写入 trace 文件。你可以随时改参数重跑同一个脚本在不同机器上行为基本一致。用它可以准确看到慢启动、拥塞避免、快速恢复的切换时机。当然 NS2 年代久远安装可能遇到兼容性问题后面避坑章会专门讲。选择它的理由是可控性和可观测性实验报告需要的所有数据它都能给。3. 用 NS2 搭最小仿真链路Reno 连接的 Tcl 脚本与参数怎么设3.1 最小拓扑两节点一条瓶颈链路Reno 仿真不需要复杂的哑铃拓扑最精简的配置是 4 个节点、3 条链路发送端 n0、接收端 n3、中间节点 n2 作为瓶颈路由器。n0 到 n2 是接入链路n2 到 n3 是瓶颈链路瓶颈带宽必须小于接入带宽否则拥塞发生在最后一跳观测不到窗口收敛。下面是完整的最小脚本直接保存为reno.tcl运行。# 创建仿真器 set ns [new Simulator] # 打开 trace 文件记录所有事件 set tracefile [open out.tr w] $ns trace-all $tracefile # 打开 nam 文件用于可视化可选 set namfile [open out.nam w] $ns namtrace-all $namfile # 创建 4 个节点 set n0 [$ns node] set n1 [$ns node] set n2 [$ns node] set n3 [$ns node] # 接入链路10Mb 带宽5ms 延迟 $ns duplex-link $n0 $n2 10Mb 5ms DropTail $ns duplex-link $n1 $n2 10Mb 5ms DropTail # 瓶颈链路1.5Mb 带宽20ms 延迟使用 DropTail 队列 $ns duplex-link $n2 $n3 1.5Mb 20ms DropTail # 瓶颈队列长度限制为 20 个包 $ns queue-limit $n2 $n3 20 # 创建 Reno TCP 连接n0 发送n3 接收 set tcp0 [new Agent/TCP/Reno] $tcp0 set window_ 100 $tcp0 set ssthresh_ 64 $ns attach-agent $n0 $tcp0 set sink0 [new Agent/TCPSink] $ns attach-agent $n3 $sink0 $ns connect $tcp0 $sink0 # 挂一个 FTP 应用让连接一直有数据发送 set ftp [new Application/FTP] $ftp attach-agent $tcp0 $ftp start 0.5 # 错误模型在瓶颈链路上注入 2% 随机丢包 set err [new ErrorModel] $err set rate_ 0.02 $err set event_ drop $ns lossmodel $err $n2 $n3 # 每隔 0.1 秒采样一次 cwnd写入 cwnd.dat set cwndfile [open cwnd.dat w] proc plot_cwnd {} { global ns tcp0 cwndfile set now [$ns now] set cwnd [$tcp0 set cwnd_] puts $cwndfile $now $cwnd $ns at [expr $now 0.1] plot_cwnd } $ns at 0.5 plot_cwnd # 20 秒后结束仿真 $ns at 20.0 finish proc finish {} { global ns tracefile namfile $ns flush-trace close $tracefile close $namfile exit 0 } $ns run这个脚本里window_是发送窗口上限设成 100 表示 cwnd 最多涨到 100 个段ssthresh_是慢启动阈值初始设 64这样慢启动在 6 个 RTT 内就会触发切换曲线图能同时看到指数段和线性段。lossmodel挂在 n2 到 n3 方向的链路上rate 0.02 表示约 2% 的包会被丢弃。采样部分直接读取 TCP 代理的cwnd_变量每 0.1 秒记一次得到的cwnd.dat就是画图用的原始数据。3.2 把丢包注入链路错误模型挂对方向上一节里错误模型只挂了一个方向这是很多同学忽略的点。NS2 的duplex-link是双向链路但lossmodel的语法$ns lossmodel $err $n2 $n3只对 n2 到 n3 方向生效。如果反向 n3 到 n2 也需要丢包比如双向都对传必须再创建一个错误模型挂回去。丢包率也不是越大越好。2% 丢包率在窗口接近 100 时一个窗口内平均丢 2 个包Reno 可能一次丢包后还没恢复完又一个丢包曲线会变得很乱。而 0.5% 丢包率在 20 秒仿真里可能只触发一两次丢包图形更清晰适合演示快速重传。我一般先用 1% 跑通流程确认曲线有锯齿后再分别用 0.5% 和 2% 做对比实验观察丢包率对窗口均值的影响。3.3 跑仿真与检查输出从 nam 动画到 trace 文件脚本保存后在终端运行命令启动 NS2ns reno.tcl跑完后当前目录下会生成三个文件out.tr是事件级 traceout.nam是动画文件cwnd.dat是采样到的拥塞窗口序列。先别急着画图用head看下cwnd.dat的内容head -20 cwnd.dat正常输出是两列第一列时间秒第二列 cwnd 值段。如果第二列恒定不变说明采样逻辑没问题但链路丢包没生效如果第一列没有从 0.5 开始说明采样函数没被触发。此时可以用nam reno.nam 打开动画直观看到数据包从 n0 发到 n3 的过程红色方块代表丢弃的包。nam 动画适合给报告截图但判断实验是否成功还要看数据。一条正在慢启动的曲线cwnd 会在前几秒从 1 迅速爬到 ssthresh进入拥塞避免后斜率变平遇到丢包时快速掉到峰值一半。如果整条曲线是平的或只有一次下跌先回 3.1 检查瓶颈链路带宽是否比接入链路低、丢包模型是否挂对、ssthresh 是否设得过大。4. 从脚本输出里画出 cwnd 锯齿数据提取、gnuplot 画图与调参4.1 用 gnuplot 画 cwnd 曲线基础脚本与输出格式cwnd.dat 已经是两列纯文本gnuplot 可以直接读。写一个plot_cwnd.gp脚本set terminal pngcairo size 800,600 set output cwnd.png set xlabel Time (s) set ylabel cwnd (segments) set grid plot cwnd.dat using 1:2 with lines title Reno cwnd运行gnuplot plot_cwnd.gp生成cwnd.png。这张图是你实验报告的核心证据。如果曲线不是锯齿而是持续上升大概率是丢包没挂上如果锯齿很密且 cwnd 长期在 2~4 之间跳动大概率是丢包率过高。用 0.02 丢包率 20 秒仿真预期得到 3~5 次明显的下跌每次下跌后 cwnd 回到峰值一半左右。4.2 从 out.tr 里提取丢包与吞吐awk 的两种常用统计cwnd 采样已经直接写在文件里但有时还需要从 out.tr 统计链路吞吐量、丢包次数这些需要 awk 处理。out.tr 每一行以、-、r、d开头分别代表入队、出队、接收、丢弃。下面的命令统计 n2 到 n3 链路上接收和丢弃的字节数# 统计接收数据量链路 n2-n3 方向 awk $1r $32 $43 {sum $8} END {print received bytes:, sum} out.tr # 统计丢弃的包个数 awk $1d $32 $43 {count} END {print dropped packets:, count} out.tr字段含义取决于 NS2 版本但常见格式里$3和$4是源节点和目的节点$8是包大小。用这个统计可以算出有效吞吐接收字节数除以仿真时间再对比理论值。理论最大吞吐约等于瓶颈带宽乘以1 - 丢包率实际值会略低如果差太多说明队列丢弃占了很大比例。4.3 三个必调参数丢包率、瓶颈带宽与队列长度我把这个实验里对 cwnd 形状影响最大的三个参数列成表方便对照调整参数默认值示例调大后的效果调小后的效果丢包率 rate_0.02锯齿变密cwnd 长期偏低锯齿变疏可能出现长时间无丢包瓶颈带宽1.5Mbcwnd 可涨到更高值cwnd 更早触顶吞吐受限队列长度20队列吸收突发丢包减少队列容易满大量丢包引发超时队列长度是容易被忽略的变量。DropTail 队列满后直接丢弃新到的包如果队列只有 5 个包一旦慢启动阶段窗口翻倍过快大量包在同一轮被丢弃Reno 会检测到多个丢包而不是一个快速恢复来不及完成就进入超时重传。20 个包的队列能让单次丢包事件更可控曲线也更有规律。修改队列长度时同时观察 out.tr 里丢弃事件是否成簇出现。4.4 对比实验同一脚本只改丢包率实验报告只画一条曲线说服力有限。一个常见的做法是保持其他参数不变将丢包率从 0.01 改成 0.05对比两条曲线。修改方法是重新复制脚本改一行$err set rate_跑完后用 gnuplot 把两个 cwnd.dat 画在同一张图里。此时能直观看到低丢包率下 cwnd 平均水平更高震荡幅度更大高丢包率下 cwnd 被压得很低传输效率显著下降。这个对比本身就是对 Reno 行为的一个可验证结论在随机丢包面前cwnd 的均衡值大致反比于丢包率的平方根。不要求你推导这个公式但实验曲线能反映趋势报告里写一句“丢包率从 1% 提高到 5%平均窗口大约降低到原来的三分之一”这份实验就比纯截图有分量得多。5. 避坑Reno 实验里五个最容易翻车的点5.1 现象cwnd 曲线只有上升没有下跌这是最典型的翻车现场脚本跑得很顺没有报错但 cwnd 一直涨到 window 上限整条曲线像一条斜线。原因是错误模型没有真正挂到瓶颈链路上。常见情况有两种一是lossmodel挂反了方向比如链路是 n2 到 n3 通你写成了$ns lossmodel $err $n3 $n2二是挂到了接入链路而不是瓶颈链路接入链路带宽 10Mbps 本来就空余丢一点点包很快被重传窗口不会出现明显回落。解决方法是先把丢包率临时调到 0.1重新跑仿真如果曲线还是平的检查脚本里 lossmodel 那一行是否早于 duplex-link 执行——NS2 解析脚本是从上往下执行必须先定义链路再挂错误模型。调大丢包率只是为了快速验证模型生效验证完再改回 0.01~0.02 即可。5.2 现象慢启动阶段短得看不见第一眼就是线性爬升有些同学的 cwnd 曲线从第 0 秒起就是平缓上升完全看不到指数段。原因多半是 ssthresh 被设置得很低或者连接没有真正从 0 开始。比如ssthresh_设成了 4cwnd 在 2 个 RTT 内就越过阈值慢启动根本没来得及展示。解决方法是显式设置window_和ssthresh_不要依赖 NS2 版本的默认值。不同 NS2 编译版本对 TCP 默认参数的定义有差异有些版本默认 ssthresh 很大有些版本默认开启 SACK这都会改变曲线形状。我在脚本里固定为window_ 100、ssthresh_ 64就是为了让结果在不同机器上可复现。如果你想展示更长慢启动段把 ssthresh 设成 128 或更大即可但窗口上限也要同步放大。5.3 现象跑完没有生成 cwnd.dat采样函数没生效。常见原因是在$ns run之前没有调用$ns at 0.5 plot_cwnd或者采样函数里使用了局部变量名而没有global声明。Tcl 脚本里过程内部访问的变量默认是局部的不写global tcp0 cwndfile拿到的就是一个空值写进文件的全是0 0。另一个容易被忽略的坑是采样开始时间早于 FTP 启动时间。如果 FTP 在 0.5 秒启动采样在 0.1 秒启动前 0.4 秒记录到的 cwnd 是初始值画图时会出现一段水平的基线。把两者设为同一时间或者让采样稍晚于 FTP 开始曲线起点才干净。5.4 现象丢包后 cwnd 直接掉到 1而不是减半前面说过Reno 的快速恢复应该让 cwnd 减半但有时候你看到的是崩溃式下跌。原因有两个一是丢包率过高多个连续丢包让发送方等不到重复 ACK触发了超时重传超时后 cwnd 必然降到 1二是队列长度太小丢包成群出现快速重传机制根本没有机会完成。解决方法是降低丢包率到 0.01 左右同时把队列长度从 20 提到 30。如果还是掉到 1检查 trace 里丢包时间戳是不是集中在同一个窗口内。Reno 只能处理一个窗口内的单个丢包连续丢包是它的大脑后续实验换 NewReno 或 SACK 也是讨论这些场景。5.5 现象gnuplot 画出的图和 nam 动画对不上同一份 tracenam 里看到的拥塞发生在某个时间点但 cwnd 曲线的下跌发生在几秒之后。这不是 bug而是重复 ACK 触发快重传需要时间。数据包丢失发生在 t 时刻但接收方要等到后续序号的数据到达才会发出重复 ACK发送方收到 3 个重复 ACK 再降窗口这个延迟等于一个 RTT 加处理时间。所以画图时不需要让下跌点和 nam 的丢包点完全对齐只要差值是 RTT 量级就是正常的。如果差值达到好几秒检查是不是瓶颈链路延迟设得过大或者 ACK 路径上也有丢包导致重复 ACK 被延迟。把这些时间关系写进实验结果分析里也是一条很好的思考记录。6. 把实验再往前推一步用 RED 队列压平 cwnd 锯齿DropTail 队列满才丢包所有 TCP 流同一时间丢包恢复时又同步增长造成所谓的全局同步。一个常见的进阶做法是把瓶颈链路的队列换成 RED随机早期检测让丢包发生在队列还没满的时候并且丢包概率随队列长度增长而增大。这样各个连接被丢包的时机不同整体吞吐更平稳。修改脚本只需要把duplex-link的队列类型和 RED 参数补上# 瓶颈链路改用 RED 队列 $ns duplex-link $n2 $n3 1.5Mb 20ms RED # 获取 RED 队列对象并设置参数 set redq [[$ns link $n2 $n3] queue] $redq set thresh_ 5 $redq set maxthresh_ 15 $redq set q_weight_ 0.002 $redq set bytes_ falsethresh 和 maxthresh 是队列长度的两个阈值平均队列长度低于 thresh 时不丢包高于 maxthresh 时所有到达的包都标记为丢弃介于两者之间时按概率丢包。q_weight 决定了平均队列长度对瞬时队列变化的敏感度值越小越平滑。第一次跑 RED 时用默认的 5 和 15 即可。跑完对比 cwnd 曲线你会发现 RED 场景下的锯齿幅度明显小于 DropTail且曲线在高丢包率下不会出现长时间低谷。这就是 RED 的价值以轻微且随机的丢包为代价换取更高的链路利用率和更小的窗口波动。做这个对比实验时其余参数带宽、延迟、丢包率、窗口上限全部保持一致只有队列机制不同才说明差异来自队列策略。这个实验之后我养成了一个习惯每次跑仿真脚本前先打印所有关键参数的初始值。NS2 的脚本语言不够显式依赖默认值很容易踩坑而一条可信的实验曲线必须能回答“哪些参数是故意设的、哪些是默认继承的”。希望这篇笔记能帮你少走一些我当年走过的弯路祝实验顺利。本文还有配套的精品资源点击获取