拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

NS2网络仿真入门:Tcl脚本编写到轨迹数据分析与绘图

NS2网络仿真入门:Tcl脚本编写到轨迹数据分析与绘图

简介:面向网络仿真初学者的NS2入门代码包,覆盖TCP/IP、DSDV/AODV路由、拥塞控制、移动模型与OOPSI接口等典型实验,适合高校网络课程实验或自学理解NS2事件驱动机制。包内共28个文件,以Tcl脚本为核心,辅以C/C++源码、头文件、awk统计脚本、nam轨迹及tr仿真数据,整体623KB,轻量易用。当前已有223人学习。示例按NS2教材章节组织,从基础拓扑搭建到mflood协议源码分析,演示如何编写Tcl脚本生成nam动画、用awk提取吞吐量与丢包率、以gnuplot绘制性能图表。研读这些代码可掌握NS2的事件调度、协议注册与结果分析流程,为独立设计网络仿真实验打基础。

1. 这份 NS2 代码包值不值得下:先看清它覆盖了什么

如果你是刚上手 NS2、对着英文 Manual 和一堆 Tcl 脚本不知道从哪下手的人,这份压缩包其实比很多教程都实在。它把第二章的 Tcl 基础示例、第三章的 class 和 common 脚本、后面的 example1.tcl 和 example2.tcl、轨迹文件 example1.tr、nam 回放文件 example1.nam,以及第六章的 Xgraph/Gnuplot 分析和第七章的 mflood 泛洪协议源码都收在了一起。换句话说,它不是单个孤立的代码文件,而是一条按教材章节组织的完整链路——从搭拓扑到跑仿真,从读轨迹文件到画性能曲线,你能在这套代码里完整走一遍。适合两类人:一是课程实验需要 NS2 出图出数据的本科生,二是准备在这套仿真器上做路由协议或拥塞控制二次开发的入门研究者。

2. 从 example1.tcl 到 nam 回放:先把仿真链路跑通

拿到代码包以后,最忌讳的就是挨个ns跑一遍,跑完不知道每一步生成了什么文件,最后只能对着终端发呆。这一章你就跟着我按顺序拆,先把example1.tcl到.nam回放这条最小链路吃透。

2.1 代码包里文件太多,先按这个映射表找东西

解压之后你会发现ns2.rar里不是平铺的一堆.tcl,而是按章节分组的。我第一次翻这个包的时候也觉得乱,后来对照着教材章节和你这份资源里的文件名整理了一份映射表,按表找文件效率高得多:

文件/目录标记对应章节用途
example1.tcl/example1.nam/example1.tr第二章最基础的双节点拓扑,配 UDP 与 CBR 流量,用于跑通 NS2 命令链路
example2.tcl/example2.nam/example2.tr第二章多节点或者不同队列参数的对比场景,适合看拓扑变化对轨迹的影响
common_example.tcl/class_example.tcl第三章Tcl 面向对象写法在 NS2 里的体现,讲解Class和Simulator的封装
gnuplot / xgraph 相关脚本第六章把.tr或.dat的数据可视化,出论文用图
mflood 测试代码与源码第七章泛洪协议源码,属于需要交叉编译进 NS2 的 C++ 代码

注意看,example1.tcl和example1.tr是一对,.tcl是输入,.tr是 NS2 运行后生成的轨迹输出。这个压缩包很贴心地把输出文件也放进来了,方便你对拍——自己跑出来的example1.tr和附带的原始文件对比,看差异就能及时发现配置错误,这也是我后来排查问题最喜欢用的方法。

2.2 最简拓扑:example1.tcl 逐段拆解

这份example1.tcl属于 NS2 里门槛最低的一类脚本:两个节点,一条链路,一个 UDP 代理加 CBR 流量。全部内容展开来也就十几行,但每一行的作用我都拆开讲清楚,因为后续所有复杂脚本都是从这些基本语句生长出来的。

# 创建一个模拟器实例,simulator 是 NS2 的核心对象 set ns [new Simulator] # 给 trace 和 nam 输出文件起名字 set tracefile [open example1.tr w] $ns trace-all $tracefile set namfile [open example1.nam w] $ns namtrace-all $namfile # 创建两个网络节点,node 实例是后续一切拓扑的基础 set n0 [$ns node] set n1 [$ns node] # 在 n0 和 n1 之间建立一条带宽 1Mb、延迟 10ms 的双向链路 # 丢包模型为 DropTail 队列,队列最大长度 10 个包 $ns duplex-link $n0 $n1 1Mb 10ms DropTail # 创建 UDP 代理,挂到 n0 上,远端指向 n1 的端口 0 set udp [new Agent/UDP] $ns attach-agent $n0 $udp set null [new Agent/Null] $ns attach-agent $n1 $null $ns connect $udp $null # CBR 流量发生器:每个包 512 字节,每 0.005 秒发一个 set cbr [new Application/Traffic/CBR] $cbr attach-agent $udp $cbr set packetSize_ 512 $cbr set interval_ 0.005 # 定义仿真结束时间点:0.5 秒时启动流量,4.5 秒时停止,5.0 秒时结束仿真 $ns at 0.5 "$cbr start" $ns at 4.5 "$cbr stop" $ns at 5.0 "finish" # 定义 finish 过程:刷新输出,关闭文件,调用 nam 回放 proc finish {} { global ns tracefile namfile $ns flush-trace close $tracefile close $namfile exec nam example1.nam & } $ns run

这段脚本注释已经很细了,我再挑三个容易看漏的点说:

  • duplex-link的参数顺序是节点1 节点2 带宽 延迟 队列类型。带宽和延迟同时设置才叫“链路参数完整”,很多人只写带宽不写延迟,仿真结果会跟真实网络差很远。
  • Agent/UDP和Agent/Null是配对出现的。Agent/Null作为接收端 sink,专门负责丢弃或统计数据包,它是 UDP 仿真里的标准收口,不理解这一点,后面读.tr文件时很容易犯迷糊。
  • $ns at是事件驱动的核心,它把 CBR 的启停和整个仿真结束时间挂到模拟时钟上。注意 5.0 秒的finish是必须的,否则ns run之后进程不会自己退出,会一直挂着。

至于class_example.tcl和common_example.tcl,它们解决的问题是“如何自定义一个协议或对象”。如果你只是跑现成例子,第三章可以先放一放,但当你打算把第七章 mflood 集成进来或者写新Agent子类时,这两个文件里的Class写法就是你最该模仿的模板。

2.3 跑起来:ns 执行、nam 回放、tr 落盘

文件都认识清楚了,就实际跑一遍。运行之前先确认环境变量,这个坑后面避坑章节会详细讲,这里直接用命令验证:

# 检查 ns 是否可用,能输出版本号就说明环境基本没问题 ns -version # 执行 example1.tcl,正常情况不会有多余输出 ns example1.tcl # 确认生成了轨迹文件和 nam 动画文件 ls -l example1.tr example1.nam

执行完你会得到example1.tr和example1.nam。.tr是纯文本的轨迹文件,后面 gawk 处理的就是它;.nam是给nam工具回放的动画文件。

# 用 nam 回放动画,观察数据包在节点之间流动 nam example1.nam &

nam 打开后慢放几遍,看清楚包从 n0 发出、经过链路队列、到 n1 被 Null 接收的过程。nam 里可以直接看队列占用、丢包位置,这些可视信息对后面你判断.tr统计结果是否合理非常有帮助。

这里有个习惯我从第一次跑 NS2 就养成了:拿到任何一个.tcl,先打开看有没有trace-all和namtrace-all,没有就自己不放心,因为后面awk和 gnuplot 全靠这两行生成的输出文件。

3. 读懂 .tr 轨迹文件:gawk 才是真正的数据分析入口

NS2 跑完只完成了一半,几乎所有实验报告需要的数字——吞吐量、延迟、丢包率——都要靠解析example1.tr得到。这一章我带你把这文件从头到尾读明白。

3.1 .tr 文件每一列到底写了什么

NS2 的默认 trace 格式是一行一个事件,空格隔开,字段含义基本固定。我把字段位置和含义列出来,这是后面所有统计脚本的基础,比去翻 Manual 快得多:

字段位置示例含义
$1r或+或-事件类型:r接收,+入队,-出队,d丢包
$21.000事件发生的时间戳,单位秒
$30源节点 ID
$41目的节点 ID
$5cbr流量类型或协议名
$6512数据包大小,单位字节
$72包在队列中时的标志位,一般传包时写-------
$80源节点.端口
$91.0目的节点.端口
$100包的 sequence number,TCP 里常用
$114包的全局唯一 ID,从 0 递增

用head命令直接看前几行,对照这份表印证一次,后面写 gawk 就顺手多了。

# 查看轨迹文件开头,对照字段说明 head -n 5 example1.tr

注意$5字段如果写的是cbr,说明这是 CBR 流量产生的包;如果你跑的是 TCP 脚本,这里会变成tcp,后面统计时不能写死流量类型,最好用变量传参。另外第 7 字段的-------和序号字段在 NS2.35 某些扩展模块里会变成 hex 值,遇到0x开头的值别慌,不影响按列取值。

3.2 用 gawk 统计吞吐量:一条命令算清楚

吞吐量的定义是单位时间内成功传输的数据量,接收端收到的字节数除以时间跨度就能算。这里“接收事件”看$1等于r,且$4是接收节点 ID。下面这条命令是我在实验室最常用的模板,从这份代码包的example1.tr上可以直接跑。

# 统计接收端累计字节,按 1 秒间隔分段计算吞吐量 gawk ' $1 == "r" && $4 == 1 { time[$2] += $6 } END { for (t in time) { print t, time[t] } } ' example1.tr | sort -n > received_bytes.dat

这段 gawk 的逻辑是按时间戳累计接收字节数,$4 == 1表示接收节点 ID 是 1。因为 CBR 包大小固定 512 字节,直接用字段 6。字段 6 也可能因为 TCL 里设了packetSize_而变动,所以统计时取$6而不是假设 512,写脚本时别写死,这是个非常隐蔽的翻车点。

# 把字节数转换成吞吐量(Mbps),并做时间对齐 gawk ' $1 == "r" && $4 == 1 { bytes += $6; last_time = $2 } END { print "总接收字节:", bytes print "最晚接收时间:", last_time if (last_time > 0) { throughput = bytes * 8 / (last_time * 1000000) print "平均吞吐量(Mbps):", throughput } } ' example1.tr

实际实验里你不会只看总吞吐量,更常看的是“随时间变化的吞吐量”。那么把上面第一条命令的时间精度从 1 秒改成 0.5 秒,用int($2 / 0.5) * 0.5做桶,把每一小段的字节数除以桶宽,就能得到一条吞吐量随时间变化的曲线,方便输入 gnuplot。这条思路和你这份代码包里第六章 gnuplot 脚本的数据准备方式是吻合的。

3.3 延迟和丢包率的近似算法

延迟在 NS2 的 trace 里没有现成字段,常见做法是对发送事件和接收事件按包的全局 ID 做配对:

# 通过包 ID 匹配发送和接收时间,求端到端延迟 gawk ' $1 == "+" && $3 == 0 { send_time[$11] = $2 } $1 == "r" && $4 == 1 { recv_time[$11] = $2 } END { for (id in recv_time) { delay = recv_time[id] - send_time[id] if (delay >= 0) print id, delay } } ' example1.tr

这里用$11作为包的全局唯一 ID,send_time和recv_time两个数组分别存两端的时刻。为什么用+而不是-?因为+是包刚进入队列的时刻,-是包离开队列的时刻,如果你想单独算排队延迟,就用-时刻减去+时刻;如果你想算端到端延迟,通常用接收时刻减去发送端应用层放包的时刻。说白了就看你关心的边界在哪里。

丢包率更简单,统计d事件数量除以总发送数量:

# 统计总发送数、总接收数和丢包数,计算丢包率 gawk ' $1 == "+" && $3 == 0 { sent++ } $1 == "r" && $4 == 1 { recv++ } $1 == "d" { dropped++ } END { printf "发送包: %d, 接收包: %d, 丢弃包: %d\n", sent, recv, dropped if (sent > 0) printf "丢包率: %.4f%%\n", dropped / sent * 100 } ' example1.tr

有一点必须说清楚:dropped的数量不完全等于sent - recv,因为仿真时间截止时,可能还有包滞留在队列里没被处理。所以更严谨的算法是丢包率 = dropped / sent,而不是拿差数算。如果你发现两个数对不上,大概率是队列里还有残留包,把仿真结束时间调长一点可以减小误差。我一开始写统计脚本时在这里栽过一次,后来养成了习惯:任何丢包统计脚本里,只信任d事件,不信任作差。

4. gnuplot 与 xgraph:把仿真结果画成论文级曲线

数据都从.tr文件里算出来了,接下来这张图决定你实验报告的上限。NS2 老用户基本不用 Excel 画仿真曲线,一是因为数据量大时卡,二是曲线风格跟论文要求的差别太大。这一章讲 xgraph 和 gnuplot 怎么配合这份代码包用。

4.1 为什么不用 Excel:gnuplot 和 xgraph 的适用场景

先分清这两个工具。xgraph 是 Tcl/Tk 自带或者 NS2 配套的轻量绘图程序,它的定位是快速预览——把.dat文件丢进去,几秒钟出一个可交互缩放的折线图,适合你在跑完仿真后扫一眼结果趋势是否正常。gnuplot 则是出版级绘图工具,支持批量出图、脚本化绘图、设置坐标轴范围、线型配色,最终论文里的图基本都是它画的。

对比项xgraphgnuplot
启动方式xgraph xxx.datgnuplot plot.plt
交互性支持缩放、多窗口偏批处理,出图后基本不交互
出图格式屏幕窗口为主PNG / PDF / EPS 都行
适合场景快速验证、调参最终曲线、对比图
与代码包关系第六章示例直接调用第六章脚本生成.dat后供其读取

我自己的习惯是两段式:先用 xgraph 把曲线扫一遍,确认没有数据异常,再写 gnuplot 脚本出正式图。这套代码包里第六章之所以同时涉及 gnuplot 和 xgraph-example.tcl,就是因为在不同阶段它们各有不可替代的位置。

4.2 把 gawk 结果导入 gnuplot:数据格式与 plot 命令

上一章我用 gawk 生成了received_bytes.dat,但这还不能直接喂给 gnuplot,gnuplot 需要的是清晰的两列或多列数据。这一步处理好,后面画图就很顺。

# 把上一步的字节累积量换算成吞吐量(Mbps),并输出两列数据 gawk ' $1 == "r" && $4 == 1 { bytes += $6; time = $2 } END { for (i = 1; i <= 10; i++) { # 这里按固定间隔输出,方便 gnuplot 直接画曲线 print i * 0.5, bytes * 8 / (i * 0.5 * 1000000) } } ' example1.tr | sort -n > throughput.dat

然后准备 gnuplot 脚本,这里的参数设置决定出图质量,我写一下最常用的一个模板:

set terminal pngcairo size 800, 600 set output "throughput.png" set title "CBR Throughput over Time" set xlabel "Time (s)" set ylabel "Throughput (Mbps)" set grid set key bottom right plot "throughput.dat" using 1:2 with linespoints pointtype 7 pointsize 0.8 title "UDP-CBR"

脚本里p但如果你的 NS2 是从源码手动编译的,ns可执行文件在ns-allinone-2.35/bin下,把这个路径和/usr/lib(或/usr/local/lib)一起写进环境变量就行。注意 LD_LIBRARY_PATH 别把bin路径加进去,那是给PATH` 用的,混了容易出怪问题。

5.2 错误二:一直报错 “invalid command name ns”

现象:你在某个目录下执行ns example1.tcl,终端提示“invalid command name ns”,换到另一个目录却正常。

原因:NS2 的 Tcl 解释器会到PATH里找ns,但在某些发行版里,ns这个命令不是个可执行文件,而是个 shell wrapper;且 wrapper 内部调用了绝对路径的ns可执行程序。如果你的PATH里同时存在其他版本的ns(比如某个 Python 包自带的ns),就会跑偏。

解决:用which ns查看当前解析到哪个路径,然后把这行加入~/.bashrc并source一下:

export PATH="/home/你的用户名/ns-allinone-2.35/bin:$PATH" export LD_LIBRARY_PATH="/home/你的用户名/ns-allinone-2.35/lib:$LD_LIBRARY_PATH"

我遇到过最离谱的一次是系统里有个 OpenBLAS 包自带的ns命令,导致我跑了半天“NS2”,实际在执行别的程序。从那以后我每换一台机器,第一步永远是which ns和ns -version,确认版本号再动手。

5.3 错误三:awk 统计结果全是 0,或者数字明显偏小

现象:gawk 命令照抄别人的,跑出来吞吐量是 0,或者延迟是负数。

原因:.tr 文件的列数不一定是 12 列。NS2.35 里如果打开了Mac/Wireless扩展,trace 行前面会多出几列,事件类型从第 1 列变成第 2 列;又或者你用的是namtrace-all生成的文件,格式和trace-all不同。此外,事件类型r的接收节点字段不一定是你想的那个。

解决:先head -20 example1.tr看实际列结构,再写 gawk。这里给个通用技巧:

# 打印前 3 行,并查看每行字段个数 head -3 example1.tr head -3 example1.tr | gawk '{print NF}'

如果字段数不是 12,说明这轨迹格式不是标准默认格式,你得按实际列数调整字段索引。比如有的无线 trace 格式里$2是时间戳,$1是事件类型,$3变成节点 ID,那你 gawk 里的$3 == 0就要改成$2 == 0这种,直接套模板必翻车。

5.4 错误四:gnuplot 输出 PNG 报错 terminal 不支持

现象:执行set terminal png时报错“unknown or ambiguous terminal type”,或者出图的 PNG 是空白。

原因:gnuplot 安装时没有编译 PNG 支持,或者版本太老,pngcairo需要单独安装。Debian/Ubuntu 上常见。

解决:先用gnuplot -e "set terminal"查看当前支持的终端类型。如果确定没有 png,安装扩展库:

sudo apt-get install gnuplot gnuplot-x11

或者干脆直接用set terminal pngcairo,因为这个终端类型在最新版本里几乎都默认支持。出图如果中文字体变方块,多半是缺少中文字体包,安装fonts-wqy-microhei并指定font "WenQuanYi Micro Hei,12"即可。

5.5 错误五:跑第七章 mflood 代码直接 make 报错

现象:把 mflood 源码解压到任意目录,进目录执行make,报一堆缺头文件的错。

原因:mflood 是 NS2 的一个 Agent 实现,它必须编译进 NS2 的源码树里,而不是单独编译。你需要把mflood.h和mflood.cc放到ns-2.35/mflood/或ns-2.35/mac/下,并在ns-2.35/Makefile的OBJ_CC里加上对应.o文件,才能一起编译。

解决:这一步容易劝退新手,但步骤很机械:

# 把 mflood 源码拷贝到 NS2 源码目录下的 agents 目录 cp mflood.cc mflood.h 你的NS2路径/ns-2.35/agents/ # 编辑 Makefile,找到 OBJ_CC 那行,在末尾加上 agents/mflood.o # 然后在 ns-2.35 目录下重新编译 ./configure make clean make

重新编译 NS2 可能要等一段时间,这期间要确保依赖库齐全,缺了otcl、tclcl会直接编译失败。编译通过后再用第七章的mflood测试代码里的 tcl 脚本跑测试,泛洪协议才能正常工作。

6. 最后一个小技巧:一套命令把多组仿真结果合成一张对比图

前面几章都是单次仿真,但你做实验不可能只跑一组参数。这个技巧是我在写课程设计时总结出来的,能让多组结果自动汇总到一张图里,省掉大量复制粘贴的体力活。方法就是:在 bash 里循环跑不同参数的 tcl,再用 gawk 抽取同一指标,拼进一个 dat 文件,最后 gnuplot 一次出图。

#!/bin/bash # 批量仿真脚本,参数按递减间隔生成,CBR interval 从 0.1 逐步调到 0.02 for interval in 0.1 0.05 0.02 do # 调用 tcl 脚本时通过环境变量传入 interval,这样不用改脚本内容 export CBR_INTERVAL=$interval ns example1.tcl # 把本次仿真的吞吐量曲线导出到一个带标记的临时文件 gawk -v tag="$interval" ' $1 == "r" && $4 == 1 { bytes += $6; time = $2 } END { print time, bytes * 8 / (time * 1000000), tag } ' example1.tr > temp_$interval.dat done # 合并所有临时文件到总 dat 文件 cat temp_*.dat > all_throughput.dat

这个脚本里用-v tag="$interval"把 interval 值传入 gawk,后面第三列就是这个标签,gnuplot 按标签分开画每条曲线。需要注意time变量在所有分组里都取最后一条接收事件的触发时间,如果你希望比较的是固定时间窗口(比如 0 到 5 秒)的平均吞吐量,就得加个时间条件,不然每组数据窗口不一致导致曲线无可比性。

set terminal pngcairo size 800, 600 set output "compare_throughput.png" set xlabel "Simulation Time (s)" set ylabel "Throughput (Mbps)" set grid set key left top plot "all_throughput.dat" using 1:2:3 with linespoints \ pointtype 7 pointsize 0.8 title sprintf("interval=%s", "0.1s"), "all_throughput.dat" using 1:2:3 with linespoints \ pointtype 5 pointsize 0.8 title sprintf("interval=%s", "0.05s")

注意这里using 1:2:3的第三列不会自动变成图例标题,你需要手动写 title。我一般会在合并 dat 前就把 tag 处理成可识别的后缀,或者直接在 gnuplot 里按行号范围取不同区间来画。这个方法不是万能的,但它能让你在调参阶段快速对比三四组数据,不用为每组数据单独写一个 gnuplot 脚本,省下的时间足够你多做两组仿真。从那以后我每次跑多组实验,都是强制走一遍“循环生成 dat → 合并 → 一张图对比”的流程,已经成了改不掉的习惯。希望这个流程也能帮你在 NS2 里少熬几个夜。

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

返回列表