简介:面向 NS2 网络仿真初学者的一份入门代码包,适合希望快速上手 Network Simulator 2 并理解网络协议仿真流程的学生与研究人员,也适合课程实验和自学练习。压缩包共 28 个文件,大小约 623KB,以 tcl、cc、h、nam、tr、awk 等类型为主:tcl 脚本用于搭建拓扑与运行模拟,C++ 源码与头文件对应协议及节点行为实现,nam/tr 输出可进行可视化与轨迹分析,awk 脚本辅助统计吞吐量、延迟、丢包率等指标。整体规模精简,且目录按章节组织,便于逐文件研读和修改运行;目前已有 223 人学习,可作为 NS2 入门或课程配套参考。内容涵盖 TCP/IP 模拟、路由协议实现、流量与拥塞控制、移动性模型及 OOPSI 接口扩展等关键知识点,并包含 xgraph、gnuplot 可视化示例。通过学习这些代码,初学者既能掌握 Tcl 配置方式和事件驱动机制,也能获得动手修改与二次开发的实践基础。
1. 把 NS2 代码包跑起来:先搞清这份资源里到底有什么
NS2 是网络仿真领域绕不开的经典工具,尤其在做无线传感器网络、MANET 路由协议、TCP 拥塞控制验证时,很多论文的仿真数据都出自它。这份 ns2.rar 并不是一个完整的 NS2 安装包,而是围绕《NS 与网络仿真》教材整理的代码集合,按章节拆好了第二章的 example1.tcl、example1.tr、example2.tcl、example2.tr、example2.nam,第三章的 common_example.tcl 和 class_example.tcl,第六、七章的 gnuplot、xgraph-example.tcl、nam-example.tcl 以及 mflood 测试代码和源码。也就是说,你拿到手的是 NS2 的练习代码和配套分析脚本,适合刚装好 NS2、正愁“照着书写完 Tcl 脚本却不知道怎么调、怎么出图、怎么分析 trace 文件”的人。我的建议是:先不要急着改代码,把这套包里的 example 系列从 tcl 跑到 nam、再从 trace 跑到 gnuplot,完整走通一遍,NS2 的工作流程基本就印在脑子里了。
2. 环境与第一次仿真:从 example1.tcl 到可视化输出
2.1 确认 NS2 装到了能用状态
不管你是用 Ubuntu 上的 apt 装的 ns2、还是源码编译安装,第一步先确认三个可执行文件在不在:ns、nam、xgraph。我在 Ubuntu 20.04 上的检查命令一般是:
which ns nam xgraph gawk正常输出会类似:
/usr/local/bin/ns /usr/local/bin/nam /usr/bin/xgraph /usr/bin/gawk这里有个容易翻车的地方:有些发行版只装了 ns 核心,nam 和 xgraph 是独立包。Debian/Ubuntu 系可以用sudo apt install nam xgraph gawk补齐,但如果你是从源码编译安装的 NS2,nam 需要在ns-allinone-2.35目录下单独make。我遇到过 nam 缺失导致 example1.nam 打不开的情况,症状是ns example1.tcl跑完没有报错,但文件生成了双击 nam 文件没反应。检查顺序建议是:先跑一个最简单的 tcl 脚本,生成 nam 文件,再确认 nam 能打开它。
确认环境没问题后,进入第二章代码目录,直接跑第一个例子:
cd ns2.rar解压目录/第二章 ns example1.tcl跑完后目录下应该多出 example1.nam 和 example1.tr 两个文件。前者是给 nam 看的动画轨迹文件,后者是文本格式的 trace 文件,后续 awk 统计全靠它。这里补充一个判断标准:如果 ns 执行后终端只在最后输出一行Scheduler: number of events之类的信息、没有抛异常,基本就是成功的,不需要等到出现窗口。
2.2 用 nam 回放仿真过程
nam 是 NS2 自带的网络动画播放器,它读取 .nam 文件后会把数据包的流动过程可视化出来。执行:
nam example1.nam &你会看到两个带数字的方形节点,中间一条链路,然后开始有数据包从节点 0 发向节点 1。如果节点没有动起来、画面是静止的,在 nam 窗口里点一下 Play 按钮。这是一个经典的双节点 TCP 传输例子,节点 0 是 TCP 源端,节点 1 是 TCP 接收端,链路的带宽和延迟在 example1.tcl 里用$ns duplex-link设置。
我在第一次跑这个例子时犯过一个低级错误:以为 nam 文件是跑完 tcl 后自动弹出窗口的,结果干等半天。实际上 NS2 的默认行为是把轨迹写入文件,是否打开 nam 窗口取决于 tcl 脚本里有没有$ns namtrace-all加exec nam之类的语句。example1.tcl 里没有自动弹出的逻辑,需要手动运行 nam 命令。所以如果你忘了nam example1.nam这一步,就会误以为“仿真没跑出结果”。
2.3 example2.tcl 对比:UDP/CBR 与 TCP 的行为差异
第二个例子 example2.tcl 通常是与 example1 形成对比的场景——一端用 UDP 连接,业务流量模型用 CBR(Constant Bit Rate,恒定比特率),另一端是 TCP。先跑起来再说:
ns example2.tcl这一步会生成 example2.tr 和 example2.nam。打开 trace 文件对比一下,能看到两类数据包的事件记录格式不同:TCP 的包事件会带上 ACK 相关的标志,UDP/CBR 则纯粹是发送和接收。
这里注意一个细节:example2.tr 的每一行是事件记录,常见字段格式是事件类型 时间 源节点 目的节点 包类型 包大小 标志 流ID 源端口 目的端口 序号 包ID。新手最容易犯的错是把 packet ID 当作序号去统计丢包,导致统计结果和 nam 里看到的丢包位置对不上。我一般是这样看的:
+ 1.0 0 1 cbr 1000 ------- 2 0.0 1.0 0 0 r 1.0024 1 0 ack 40 ------- 0 1.0 0.0 1 1+表示入队,r表示接收。中间有一长串-是协议标志位,对应 trace 文件格式里头的标志列。真正要做丢包统计,需要数的是某个特定流 ID 下、某对节点之间的发送条数和接收条数差值,不是看所有行的和。后面第四章会展开讲 awk 怎么写。
3. 读懂 trace 文件格式:统计延迟、吞吐和丢包的基线
3.1 trace 文件各字段含义与事件类型
NS2 的 trace 文件是分析仿真的核心原料。第二章的 example1.tr 和 example2.tr 规模很小,正好拿来逐行拆解。标准 trace 行的结构是:
字段1 事件类型:+ 入队 / - 出队 / r 接收 / d 丢弃 字段2 发生时间 字段3 源节点 ID 字段4 目的节点 ID 字段5 包类型(tcp / ack / cbr 等) 字段6 包大小(字节) 字段7 标志位(一般为一串 - 或具体标志,供协议用) 字段8 流 ID(即 connection ID,区分不同连接) 字段9 源端口 字段10 目的端口 字段11 序列号 字段12 包唯一 ID注意这里说的字段编号是“语义上”的,实际文本里一行是空格分隔的 12 列。用 example1.tr 抽查一份真实行:
+ 0.1 0 1 tcp 1000 ------- 0 0.0 1.0 0 0 - 0.1 0 1 tcp 1000 ------- 0 0.0 1.0 0 0 r 0.1104 1 0 ack 40 ------- 0 1.0 0.0 1 1入队和出队时间几乎相同,说明队列没有堆积;ack 从节点 1 回到节点 0,说明接收端正常回复。如果你看到大量d事件,说明发生了丢包——要么队列溢出、要么无线场景下信号冲突,这就是后面排查仿真问题的入口。
3.2 吞吐量统计:用 gawk 一行算出来
吞吐量是最常用的性能指标,计算公式很简单:接收端收到的总字节数除以最后一个包到达时间与第一个包到达时间之差。用 gawk 对 example2.tr 做统计:
gawk ' BEGIN { start = 0; stop = 0; bytes = 0 } { if ($1 == "r" && $5 == "cbr" && $4 == 1) { bytes += $6 if (start == 0) start = $2 stop = $2 } } END { if (stop > start) printf "吞吐量 %.2f kbps\n", bytes * 8 / (stop - start) / 1000 } ' example2.tr逻辑说明:只统计r事件,而不是+或-,原因是从r事件才能确认包真正到达了接收端;$5 == "cbr"过滤出 CBR 流,避免把 ack 这类控制包算进业务吞吐量;$4 == 1限定目的节点为 1。如果你在无线场景下做统计,`.
这段 awk 里 start 和 stop 的赋值有一个坑:start 只在第一次满足条件时赋值一次,但如果第一个 r 事件出现之前就有其他事件,start 取的并不是仿真开始时间,这对吞吐量计算没有负面影响,因为吞吐量只关心“第一次收到到最后一次收到的数据量”。但如果你要做“端到端延迟”统计,就不能用这个思路,需要按包的 ID 去匹配发送时间和接收时间。
3.3 延迟计算:按包 ID 匹配发送与接收时间
延迟要的是一个包从发出到到达的时间差。做法是用数组记录每个包 ID 的发送时间,遇到对应接收事件时做差:
gawk ' { if ($1 == "+" && $5 == "cbr") send[$12] = $2 if ($1 == "r" && $5 == "cbr" && $4 == 1) { if ($12 in send) { delay = $2 - send[$12] sum += delay count++ } } } END { if (count > 0) printf "平均延迟 %.4f s\n", sum / count } ' example2.tr这里用$12作为包的唯一 ID,注意前面提过$11是序列号,它在一个流内会重复,不能全局唯一标识包。我见过有人在延迟统计里把序列号当 key,结果两条连接的包 ID 冲突,算出的延迟数据完全不可信。如果你发现延迟偶尔出现负值,基本都是 key 选错了。这条命令统计的是接收事件里,CBR 包从入队到接收的时延,也就是排队时延加传输时延之和。
3.4 丢包统计与事件筛选的边界
丢包统计最简单直接的指标是:发送端发出的包数减去接收端收到的包数。但要注意 TCP 有重传,不能简单地把发送事件数减接收事件数,因为重传的包会重复计数。对于有重传的流,更稳妥的做法是统计唯一的包 ID:
gawk ' { if ($1 == "d") drop[$12] = 1 } END { n = 0 for (k in drop) n++ printf "丢包数 %d\n", n } ' example2.tr这个统计只关心有没有发生过丢弃事件,不去重发送端数量,避免 TCP 重传带来的干扰。如果你的场景里只有 UDP/CBR 流量,那直接数发送事件数和接收事件数相减也可以,两者结果应该一致。
这里有个非常常见的误用:直接把$1 == "d"当成丢包率的分子,把$1 == "r"当分母。这在有路由协议控制的无线仿真里会失真,因为一个包可能被中转节点转发多次、产生多条记录。真正丢包率的定义必须和你要回答的问题绑定:是链路层丢包、还是网络层丢包、还是应用层丢包?不同层定义对应不同的 trace 行筛选条件。我在做 AODV 仿真时吃过一次大亏,统计出的丢包率高达 60%,后来发现是把自己发给自己转发的包也算进去了。
4. 用 xgraph 与 gnuplot 出图:从 trace 到论文曲线的完整链路
4.1 xgraph 快速看图:适合验证仿真结果再深究
第六、七章的文件里出现了 xgraph-example.tcl 和 gnuplot。xgraph 是 NS2 自带的简易绘图工具,它读取文本数据文件,格式是“X 值 空格 Y 值”两列,一行一个点。xgraph-example.tcl 这个脚本的作用通常是生成一个 .dat 或 .xgr 文件,提供绘图所需的数据点。手动生成完可以这样看:
xgraph output.xgrxgraph 的优点是零配置,适合仿真中途快速看一眼曲线趋势。缺点也很明显:不能定制坐标轴标签、线型有限、导出的图基本没法直接用进论文,字体粗糙。所以我一般把 xgraph 当“调试眼”,正式出图用 gnuplot。
如果 xgraph 打开后显示空白,先检查 .xgr 文件里是不是只有一行数据或者根本没有数据,最常见的原因是前面的 tcl 脚本没有运行完就中断了,数据没写全。还有一种情况是 xgraph 报错说“can't parse”某一行,那多半是文件里混入了 tab 键,xgraph 只认空格。
4.2 gnuplot 出图:把 trace 统计结果变成论文级曲线
gnuplot 才是真正把数据变成可发表曲线的工具。与 xgraph 相比,它支持 EPS/PDF 输出、自定义坐标轴、多曲线对比,并且可以脚本化批量出图。
先假设你统计得到了吞吐量随时间变化的数据文件,每行两列:时间、吞吐量(kbps)。用 gnuplot 脚本出图的做法是新建一个plot_throughput.plt:
set terminal pngcairo enhanced font "Arial,12" size 800,500 set output "throughput.png" set xlabel "仿真时间 (s)" set ylabel "吞吐量 (kbps)" set grid plot "throughput.dat" using 1:2 with lines title "UDP 吞吐量" linewidth 2命令解释:set terminal pngcairo指定 PNG 渲染器,enhanced允许下标和特殊符号;set output决定保存路径;using 1:2表示取第一列作 X、第二列作 Y。如果你要对比 TCP 和 UDP 两条曲线,可以这样:
plot "tcp_tput.dat" using 1:2 with lines title "TCP" lw 2, \ "udp_tput.dat" using 1:2 with lines title "UDP" lw 2生成的方式是:
gnuplot plot_tcp_udp.plt关键点在于:throughput.dat不是 trace 文件直接来的,它必须是由 awk 按时间窗口计算得到的序列。我在第四章给的 per-flow 统计命令是一整条均值,如果要画出随时间变化的曲线,需要按固定窗口(比如每 0.5 秒)切分 trace 再做聚合。这也就是第二章的 example2.tr 配合第六、七章 gnuplot 的典型用法——先 awk 分窗汇总,再喂给 gnuplot。
4.3 时间窗口聚合:从平均到时间序列的关键转换
下面这段 awk 把 trace 按 0.5 秒时间窗聚合出吞吐量序列:
gawk ' BEGIN { window = 0.5; start = 0 } { if ($1 == "r" && $5 == "cbr" && $4 == 1) { idx = int($2 / window) bytes[idx] += $6 } } END { for (i = 0; i < idx; i++) { printf "%.2f %.2f\n", (i + 0.5) * window, bytes[i] * 8 / window / 1000 } } ' example2.tr > throughput.dat逻辑说明:idx = int($2 / window)把时间轴切成多个半秒窗口,每个事件落入对应窗口累加字节数;输出时时间取窗口中间值。这样得到的throughput.dat就是给 gnuplot 用的标准两列数据。如果你改变window的值,曲线平滑度会变,窗口越小曲线锯齿越明显、窗口越大越平滑但细节丢失,论文里一般用 0.5 秒或 1 秒。
注意这里的> throughput.dat是 shell 重定向,不是 gawk 的参数。如果你忘了重定向,数据会直接打到屏幕上。还有一个细节:END块里用了idx,如果最后一个窗口没有收到任何包,它的累计值是 0,gnuplot 画出来会掉到 0 而不是显示空缺,这在视觉上会误导读者。处理办法是输出前判断if (bytes[i] > 0)。
5. 避坑手册:NS2 跑仿真最常见的五个问题
5.1 装了 NS2 但ns命令找不到
现象:终端输入ns提示command not found,但 NS2 确实装了。
原因:ns 可执行文件所在的路径没有加入 PATH。源码安装时默认装到/usr/local/bin,但如果你指定了别的安装前缀,比如./install到用户目录,路径就变了。
解决:
find / -name "ns" -type f 2>/dev/null找到实际路径后,把它写进.bashrc:
export PATH=$PATH:/usr/local/ns-allinone-2.35/bin然后source ~/.bashrc。从源码编译的朋友最容易漏掉这一步,因为安装脚本本身不一定帮你配置 PATH。
5.2 tcl 脚本跑完没有任何输出文件
现象:ns example1.tcl执行完成,终端也没报错,但目录下没有 .tr 和 .nam。
原因:脚本里缺少 trace 初始化语句。NS2 只有在执行了$ns trace-all和$ns namtrace-all之后才会生成对应文件,不是跑完就自动有。
解决:打开 tcl 脚本,检查是否有这两行:
set tracefile [open example1.tr w] $ns trace-all $tracefile set namfile [open example1.nam w] $ns namtrace-all $namfile如果没有,补上后重新运行。这里有个新手常犯的错:namtrace-all需要知道坐标范围,标准写法是$ns namtrace-all-wireless $namfile $x $y用于无线场景,有线场景用$ns namtrace-all $namfile就行。
5.3 nam 动画是灰屏或节点都是重叠的
现象:nam 能打开文件,但画面里节点堆叠在一起,或者链路是乱的。
原因:tcl 脚本里没有给节点设置坐标。NS2 有线场景默认会把节点放在同一个位置,需要$node set X_、$node set Y_指定坐标。
解决:在 tcl 脚本里加上:
$node0 set X_ 10 $node0 set Y_ 10 $node1 set X_ 100 $node1 set Y_ 10设置完坐标后重新跑一遍再打开 nam 就能看到正常的拓扑布局。
5.4 awk 统计出的丢包数为 0,但 nam 里明明有丢包
现象:nam 动画里清晰看到数据包消失,但 gawk 统计d事件为零。
原因:无线场景下丢包事件不一定记录为d,MAC 层冲突或者信号衰减造成的丢包可能只表现为发送失败、没有进入队列,trace 里对应的是其他标记。如果你统计的是有线场景却出现这个问题,说明你过滤条件写错了,比如丢包发生在某条中间链路上而未经过你筛选的目的节点。
解决:先用grep " d " example2.tr看一眼原始数据里有没有d行,确认有没有该事件类型。如果有但统计为零,检查 awk 的条件是否限定错了$4或$5。我在一个 ad hoc 网络仿真里统计丢包永远为零,后来把所有字段打印出来才发现 MAC 层丢包用的是D大写,和网络层的d区分开了,我一开始只查小写d。
5.5 gnuplot 输出中文乱码或字体僵硬
现象:gnuplot 生成的图片里坐标轴中文显示为方块,或者英文和数字挤在一起很难看。
原因:gnuplot 默认字体对中文支持不好,pngcairo终端如果没指定中文字体就会出方块。
解决:指定系统已有的中文字体,比如在 Ubuntu 上用文泉驿:
set terminal pngcairo enhanced font "WenQuanYi Micro Hei,12"如果你想完全避免字体问题,坐标轴标签就都用英文,论文插图也普遍不接受中文,这是投稿习惯问题。此外linewidth在 eps 输出里建议不低于 2,否则印刷出来线条太细。
6. mflood 源码研读:从跑通例子到看懂一个协议实现
6.1 mflood 在 NS2 里的角色与场景定位
mflood 是一个泛洪式路由协议,全称是 Multi-hop Flooding,常用于 MANET(移动自组织网络)的仿真对比。它不维护路由表,而是直接把数据包向所有邻居广播,收到包的节点如果不是目的节点就继续转发,直到到达目的地或生命周期结束。第七章的 mflood 测试代码和源码放在一起,正好能零距离看到 NS2 协议模块是怎么嵌入到整个框架里的。
跑 mflood 测试代码之前要确认一件事:你安装的 NS2 有没有编译过 mflood 模块。很多预编译版本的 NS2 没有把 mflood 加进去。检测方法很简单:
ns -version如果这个可以,再跑:
ns mflood_test.tcl如果报错提示unknown agent type mflood之类,说明 NS2 的 C++ 源码里缺少 mflood 的 otcl 映射,需要重新编译或者检查ns-lib.tcl里是否注册了相关类。
6.2 mflood 源码的核心结构拆解
mflood 的源码一般包括mflood.h和mflood.cc两个文件。mflood.h里定义了协议类,比如:
class Mflood : public Agent { public: Mflood(); void recv(Packet* p, Handler* h); void send(Packet* p); int command(int argc, const char*const* argv); private: int seqno_; // 本节点当前序列号 struct hdr_mflood* hdr_; };recv是 NS2 协议模块最核心的入口,所有到达这个代理的数据包都会进到这里。典型逻辑是:如果是广播包且之前没收到过同样的包,就转发;如果目的节点是自己,则交给上层处理。mflood 用序列号做去重,避免一个包在网络里无限循环泛洪,这是泛洪协议的关键设计。你可以在recv里加打印语句观察数据包生命周期:
printf("node %d recv packet %d seq %d\n", here_.addr_, p->uid_, seqno_);改完需要重新编译 NS2,编译过程不是一两句话能说清楚的,简单做法是在 ns-allinone 目录下直接跑make。如果你只改协议代码,编译时间可控在几分钟内。
6.3 用 mflood 源码学习 NS2 协议扩展的通用套路
NS2 里增加一个协议的核心套路是三步:C++ 类实现、OTcl 绑定、Tcl 脚本调用。mflood 正好展示了完整链路。mflood.cc里有一行绑定代码:
static class MfloodClass : public TclClass { public: MfloodClass() : TclClass("Agent/Mflood") {} TclObject* create(int, const char*const*) { return (new Mflood()); } } class_mflood;这个类注册了 Tcl 侧可以调用的类名Agent/Mflood。之后在 tcl 脚本里创建协议节点就是:
set mflood [new Agent/Mflood] $ns attach-agent $node $mflood命名保持分层结构是必须的,Agent/Mflood的Agent/前缀表明它是继承自 Agent 的协议代理,NS2 的 OTcl 调度器会按这个层级关系来调用 C++ 的command方法。command函数则是 Tcl 侧和 C++ 侧沟通的桥梁,mflood 里通常用它处理attach-agent、set-flag等指令。
6.4 验证自定义协议的正确姿势
改完 mflood 或自己写了协议,怎么验证行为对不对?我的习惯是从三个层面对齐结果:第一,单节点收发计数。在recv里加计数器,跑一个小拓扑,人工手算预期值比对。第二,用 trace 文件做宏观统计,比如观察洪泛场景下每个节点收到的重复包数量,泛洪协议正常的话,网络中离源节点越远的节点收到的副本计数越高。第三,用 nam 动画观察包的扩散过程,确认没有出现单节点瞬间收到大量包导致的队列溢出,这种溢出在 trace 里表现为连续的d事件。
我实际调 mflood 时遇到过一个最隐蔽的问题:转发时没有设置 MAC 层广播地址,导致包只发给了默认的下一跳,整个网络根本泛洪不起来,但 trace 文件看不出异常。排查了半天,最后是在send函数里打印Packet::head相关的标志才发现广播标志位没有置位。从那以后我每次写基于广播的协议,都会先在 tcl 脚本里加一个puts打印发送前包的信息,强制走一遍“源码编译 → 跑通 → 加打印 → 对比 trace”的完整流程。这套方法说起来简单,但真能省下大量对着黑匣子猜原因的时间,希望帮到你。
本文还有配套的精品资源,点击获取