
NS-3和Aqua-sim-ng这套组合最近在水下传感器网络的研究圈子里出现频率非常高。随便翻几篇水下MAC协议或路由协议的论文实验部分基本都能看到它们的身影。NS-3是当前主流的开源离散事件网络模拟器Aqua-sim-ng则是跑在NS-3上的水声通信网络仿真模块专门用来模拟水声信道、水下节点通信和各类协议的性能。我最近把这两个工具从零到一完整搭了一遍踩了不少坑也把源码结构大致过了一遍。这篇文章是第一篇学习笔记重点讲三件事为什么要用这套组合做水下网络仿真、怎么把环境快速搭起来、以及如何手写第一个完整的水声网络仿真脚本。内容定位偏入门适合刚接触水下传感器网络、或者只听过NS-3但没实际动手写过脚本的朋友。1. 为什么研究水下网络绕不开NS-3和Aqua-sim-ng1.1 水下通信场景的特殊性决定了“仿真优先”水下传感器网络UWSN和陆地无线传感器网络最大的区别就是通信介质从电磁波换成了声波。这个改变带来的是一连串连锁反应声波在水中的传播速度只有约1500 m/s比无线电慢了五个数量级可用带宽通常只有几千赫兹到几十千赫兹数据速率低得可怜多径效应严重信道时变性强再加上海水对声信号的吸收与频率强相关导致通信距离和速率之间存在根本矛盾。更麻烦的是水下做真实实验的成本极高。租船、布放节点、防水封装、能源补给、数据回收每一项都是真金白银。而且海洋环境不可控实验可重复性差今天测的结果明天换个海域可能就不成立。所以在研究早期用仿真来验证协议思想和参数设计几乎是必经之路。仿真环境可以精确控制节点位置、信道条件、流量模型各种方案在同样的基准下公平对比这也是学术界普遍接受的做法。1.2 为什么选择NS-3而不是NS-2或自研仿真器在水声网络仿真这个领域很多老论文用的是NS-2加Aqua-Sim插件。Aqua-Sim最初就是康涅狄格大学在NS-2上开发的一套水声仿真插件做了很多年有不少协议实现。但NS-2本身的问题摆在那里架构老、对象模型混乱、内存管理全凭自觉、扩展一个新协议要翻很多代码。而且NS-2已经停止大规模维护了新研究者接手老代码的学习成本非常高。NS-3虽然名字只差一代但架构上是彻底重写的。它借鉴了ns-2的很多思想但底层用了更现代的C设计有清晰的对象模型、事件调度器、节点和设备抽象。而Aqua-sim-ng就是把Aqua-Sim的核心设计移植到NS-3上的版本保留了水声信道模拟、物理层、MAC层、路由层等完整协议栈同时继承了NS-3的模块化优势。还有一个现实原因现在很多高水平论文的仿真环境已经整体迁移到NS-3如果还用NS-2复现别人的实验数据可能根本没有可比性。选择Aqua-sim-ng至少能保证和主流文献站在同一套工具链上。1.3 Aqua-sim-ng到底能做什么简单说Aqua-sim-ng提供了一整套水声网络仿真组件。它模拟了水声信道中的信号传播损耗、延迟、碰撞提供了多种MAC协议和路由协议实现还内置了节点移动模型和流量生成器。你可以在它上面做的工作包括但不限于对比不同MAC协议在相同水下场景下的吞吐量和时延、验证新设计的路由协议在不同节点密度下的表现、分析数据包大小和发送间隔对网络性能的影响。而且因为整个模块跑在NS-3里NS-3自带的各种工具也都能用比如能量模型、数据统计框架、可视化界面。这意味着你不需要自己从零搭一套仿真框架只要把注意力集中在协议实现和场景设计上。2. 从零搭建仿真环境5步编译出带Aqua-sim-ng的NS-32.1 环境准备和依赖安装我用的环境是Ubuntu 22.04 LTS这套步骤在其他Linux发行版上也通用macOS可能需要稍微调整一下依赖包名称。在开始之前先把NS-3编译需要的东西装上sudo apt update sudo apt install gcc g python3 python3-dev cmake ninja-build ccache \ libboost-all-dev libeigen3-dev libgsl-dev libgtk-3-dev \ libssl-dev libxml2-dev libpcap-dev libsqlite3-dev这里值得多说一句boost库和python3-dev是NS-3编译的硬依赖缺了会在configure阶段直接报错。libpcap-dev用于生成pcap抓包文件libgtk-3-dev用于NetAnim可视化。如果你只是跑命令行仿真不打开界面后面的图形相关依赖可以先不装但建议一次性装齐省得到时候想用了又得回来补。我个人的习惯是先确认cmake版本不低于3.16太老版本的cmake在处理NS-3的构建配置时会有兼容问题。用命令cmake --version查一下就行不满足就加apt源或者手动安装新版。2.2 下载NS-3源码并锁定版本Aqua-sim-ng对NS-3的版本敏感不是随便拿一个最新版都能编译过。我实测下来aqua-sim-ng的官方仓库对NS-3.36的支持是最稳的所以建议直接锁定这个版本。用git拉取源码git clone https://gitlab.com/nsnam/ns-3-dev.git cd ns-3-dev git checkout ns-3.36如果你直接clone的是ns-3-dev的master分支后面编译时会遇到很多API对不上的问题因为aqua-sim-ng的维护节奏跟不上NS-3主线的迭代速度。这一点我在后面“常见问题”里还会细说这里记住一个原则用发行版tag不要用主线。2.3 把Aqua-sim-ng放进contrib目录NS-3的源码目录结构里有一个contrib文件夹专门用来放外部扩展模块。Aqua-sim-ng就放在这里面cd contrib git clone https://github.com/rmartin5/aqua-sim-ng.git需要注意clone下来之后看一它的分支确认当前默认分支对应的是ns-3.36版本。我拉取的时候默认分支是可以直接用的但如果你的网络环境拉到的代码版本比较新建议执行cd aqua-sim-ng git checkout ns-3.36如果仓库里没有这个分支那就以master分支为准把NS-3版本对齐到master所对应的那个release。这里最核心的一点就是NS-3和aqua-sim-ng的版本必须配套否则编译报错能让人崩溃。2.4 configure和build回到NS-3根目录执行配置和编译。NS-3.36开始使用CMake作为构建系统但保留了ns3命令封装。官方推荐的命令是cd .. ./ns3 configure --enable-examples --enable-testsconfigure阶段会检测系统依赖并生成构建配置。如果一切顺利你会看到一大片模块列表里面应该包含aqua-sim-ng相关的内容。如果这个阶段报错先看缺少哪个依赖库补装后重新运行configure。configure完成后开始编译./ns3 build第一次编译会非常久取决于CPU核数一般二十分钟到一小时不等。可以带上并行参数加快速度比如./ns3 build -j 8这里的数字建议设为你CPU物理核心数的1到2倍。注意不要盲目设太大内存不够会直接OOM。我第一次编译时用了-j 16结果16GB内存被吃满卡在某个文件上像死机一样后来老老实实改回8。2.5 跑通自带example验证安装编译完成后验证工作是否正常的最快方式是跑一个自带的示例脚本。Aqua-sim-ng源码里自带了一些example比如aqua-sim-ng-example.cc。运行命令./ns3 run aqua-sim-ng-example如果环境搭好了你会看到类似这样的输出Program arguments: Simulation started ... Simulation finished同时目录下会生成trace文件或者anim格式文件说明整个环境已经能正常运转了。我第一次跑通这个example的时候心里那块石头才算落地因为这意味着后面的所有学习和实验都真正有了一套可运行的“床”。3. 核心架构拆解从信道到应用层的模块化设计3.1 整体分层结构Aqua-sim-ng的架构和网络协议栈的分层思想完全对应。从底到顶依次是信道与传播模型、物理层、MAC层、路由层、应用层。此外还有移动模型和流量生成器作为支撑组件。这套分层的好处很直接你可以单独替换某一层的实现而不需要动其他层。比如你想测试一种新的MAC协议在原有路由协议下的表现只需要把MAC层的对象换成自己的实现路由层完全不用改。这与真实网络设备的设计思路一致也是做协议对比实验时效率最高的方式。为了方便刚接触的朋友理解我用一张简化的分工表来展示各层职责层次核心作用对应常见组件信道模型模拟声信号的传播延迟、衰减、碰撞AquaSimChannel物理层模拟调制、误码率、捕获效应AquaSimPhyMAC层控制节点接入信道的时机AquaSimMacAloha、AquaSimMacCsmaAloha等路由层决定数据包从源到宿的转发路径AquaSimVBF、AquaSimHHVBF等应用层生成流量、模拟采集业务AquaSimTrafficGen等移动模型控制节点在三维水域中的位置变化AquaSimMobility3.2 信道模型和传播模型是水声仿真的灵魂水声信道和陆地无线信道最大的差异可以归结为三个字慢、窄、差。慢指声速低带来的传播时延非常显著窄指可用带宽极小高速率传输不现实差指信道衰减严重且时变。Aqua-sim-ng里的AquaSimChannel负责模拟数据帧在水下的传播过程。它会计算发送节点和接收节点之间的距离结合声速换算传播时延同时根据传播模型计算接收信号强度。AquaSimPropagation模块里实现了Thorp吸收模型等经典公式用来估算不同频率下声信号的吸收损耗。频率越高吸收越严重这就是为什么水下通信通常使用较低的载波频率。我刚开始看这块代码的时候有个很深的感受在水声仿真里“位置”和“时间”的关系比陆地无线仿真敏感得多。因为声速只有1500 m/s一个节点移动几米传播时延就显著变化。所以Aqua-sim-ng的移动模型和历史轨迹记录设计都不是可有可无的花架子而是直接影响仿真精度的核心组件。3.3 物理层和MAC层关注碰撞与能耗物理层AquaSimPhy的主要任务是判断一个数据帧能不能被正确接收。它结合接收到信号的强度、噪声功率、调制方式和数据速率计算误码率然后决定这一帧是丢还是收。水声信道里信号碰撞是常态因为传播时延大“发送前先听信道”这种无线电里的假设在水下常常失效听的时候信道上没有信号等发出去之后另一个节点的信号才到碰撞就这样发生了。Aqua-sim-ng提供了多种MAC协议实现。BroadcastMAC是最简单的把接收到的帧逐个向上层送不做调度。AquaSimMacAloha实现的是经典ALOHA协议节点什么时候有数据什么时候发不监听信道。AquaSimMacCsmaAloha是在CSMA基础上结合了ALOHA的思路加入了一定的监听机制。还有T-Lohi这种针对水下环境设计的高能效MAC协议。不同MAC协议在吞吐量、时延、能耗三个维度上的取舍是很多论文的选题方向。我建完环境跑的第一个实验就是对比Aloha和CSMA-ALOHA在不同节点数下的吞吐量。你会发现简单粗暴的ALOHA在轻负载下表现还行但节点一多、流量一大碰撞率陡增吞吐量急剧恶化。这个结果本身不意外但把曲线画出来的过程让我把协议行为真正记住了。3.4 路由层和应用层决定数据怎么走、业务怎么来水下路由协议的设计也很有特殊性。因为节点能量有限且难以充电路由协议需要重点关注能耗均衡和避免空洞。Aqua-sim-ng实现了VBFVector-Based Forwarding和HH-VBF等经典基于向量转发的协议也提供了一些基础的路由基类供扩展。VBF的思路很有意思源节点到汇聚节点之间划出一条虚拟的“路由管道”只有距离这条管道足够近的节点才有资格参与转发。这样比洪泛更节能也能在一定程度上减少重复包。但性能受管道半径参数影响很大半径设小了可能找不到转发节点设大了又退化成近似洪泛。这种参数敏感性正是做研究时可以利用的切入点。应用层方面AquaSimTrafficGen是内置的流量生成器可以按固定速率或者泊松过程产生数据包。你还可以用NS-3自带的UDP或TCP应用来灌流量。不过在水声场景下TCP基本没人用因为巨大的传播时延会让拥塞控制完全失效。我看到的绝大多数文献都是跑UDP或自定义的间歇性业务这样做也更贴近传感器网络的真实情况。4. 第一个仿真脚本搭建一条水下链路4.1 场景设计为了把整个流程走通我设计了一个最简单的三节点场景一个源节点、一个中继节点、一个汇聚节点直线排布。源节点周期性发包中继节点收到后转发汇聚节点记录收到的数据。这个场景虽然简单但已经覆盖了节点创建、信道绑定、协议配置、流量生成、trace输出等所有核心步骤。把逻辑捋清楚后再写代码心里会很有数。这个习惯我强烈建议你保留NS-3是一门“先想后写”的功夫脚本越长越需要清晰的场景边界。4.2 完整脚本解析下面是我实际跑通的一个简化版脚本。这里以NS-3.36加对应版本的aqua-sim-ng为准API以你拉取的源码头文件为最终依据因为不同小版本之间函数签名可能会有细微差异。#include ns3/core-module.h #include ns3/network-module.h #include ns3/mobility-module.h #include ns3/aqua-sim-ng.h using namespace ns3; NS_LOG_COMPONENT_DEFINE(FirstAquaSimExample); int main (int argc, char *argv[]) { // 仿真参数 double simTime 600.0; uint32_t numNodes 3; double txRange 500.0; CommandLine cmd(__FILE__); cmd.AddValue(simTime, 模拟时间(秒), simTime); cmd.AddValue(numNodes, 节点数量, numNodes); cmd.AddValue(txRange, 通信范围(米), txRange); cmd.Parse(argc, argv); // 步骤1创建节点 NodeContainer nodes; nodes.Create(numNodes); // 步骤2创建水声信道 PtrAquaSimChannel channel CreateObjectAquaSimChannel(); // 步骤3为每个节点配置协议栈和设备 for (uint32_t i 0; i numNodes; i) { PtrNode node nodes.Get(i); // 布置节点位置三个节点在一条直线上间隔100米 Vector pos(i * 100.0, 0.0, 0.0); PtrAquaSimMobility mobility CreateObjectAquaSimMobility(); mobility-SetPosition(pos); node-AggregateObject(mobility); // 路由协议中间节点用洪泛源和汇聚也挂上路由组件 PtrAquaSimRouting routing CreateObjectAquaSimFloodingRouting(); // MAC协议先用最简单的广播MAC PtrAquaSimMac mac CreateObjectAquaSimMac(); // 物理层设置信道和通信范围 PtrAquaSimPhy phy CreateObjectAquaSimPhy(); phy-SetChannel(channel); phy-SetTransRange(txRange); // 组合成水声网络设备 PtrAquaSimNetDevice device CreateObjectAquaSimNetDevice(); device-SetRouting(routing); device-SetMac(mac); device-SetPhy(phy); device-SetChannel(channel); node-AddDevice(device); } // 步骤4配置流量 // 源节点(0号)每2秒向汇聚节点(2号)发送一个64字节的数据包 PtrAquaSimTrafficGen traffic CreateObjectAquaSimTrafficGen(); traffic-SetPacketSize(64); traffic-SetInterval(Seconds(2.0)); traffic-SetDestinationAddr(AquaSimAddress::ConvertFrom(2)); nodes.Get(0)-GetApplication(0); // 占位具体挂载方式见说明 PtrApplication app traffic; nodes.Get(0)-AddApplication(app); // 步骤5开启Trace AquaSimPhy::EnableTrace(); // 步骤6运行仿真 Simulator::Stop(Seconds(simTime)); Simulator::Run(); Simulator::Destroy(); return 0; }需要说明的是上面脚本里流量生成器的挂载方式不同版本有不同的写法有的版本通过Device的Application容器挂载有的版本直接AddApplication。我在实际运行中遇到过API不一致的情况最终是参考aqua-sim-ng自带example里的写法调整的。所以这份代码更多是给你一条思路框架建议复制下来后先对照你本地example源码修正API再编译。4.3 关键参数选择背后的物理含义脚本里几个参数看着简单其实每个都有物理意义。simTime设成600秒是因为水下网络是慢网络传播时延动辄几十毫秒到几百毫秒跑几十秒根本看不出协议真实行为。我试过只跑60秒结果吞吐量曲线都是毛刺完全没法分析。txRange通信范围设成500米这是参考了实际水声通信设备的典型能力。低频长距离水声Modem的通信距离可以到几公里但速率很低短距离的高频设备通信范围就小。500米算是一个做实验的折中值既不会让拓扑完全连通失去路由意义也不会让数据传不出去。packetSize用64字节也是符合水下传感器网络的实际业务特点。水下传感器传的多是温度、盐度、深度这类环境数据单条数据量很小。如果专门做流媒体传输才会用到几百字节甚至更大的包。但那种场景仿真时延和能耗都会显著变大建议入门阶段先从小包开始。4.4 运行脚本和分析输出编译脚本和运行./ns3 run scratch/first-aqua-sim-example跑完后aqua-sim-ng会生成trace日志。打开trace文件你会看到类似这样的内容 123.456789 0 2 UDP 64 - 123.456789 0 2 UDP 64 r 123.789012 2 0 UDP 64每一行的含义分别是时间戳、事件类型、源节点、目的节点、协议类型、包大小。表示发放入队-表示发送r表示接收。从这些记录里你可以统计出端到端时延、吞吐量、丢包率等关键指标。我习惯用awk或者Python写个小脚本批量处理trace文件把数据画成图。这一步虽然琐碎但对判断仿真结果合不合理至关重要。比如如果丢包率超过90%那就不是协议问题可能是参数设置把网络压垮了。5. 常见问题与排查技巧实录5.1 编译和运行阶段的高频报错这部分是我踩坑最多的地方也是我写这篇笔记时最想分享的内容。我把遇到过的典型问题整理成了一张速查表问题现象可能原因处理办法configure时找不到aqua-sim-ngcontrib目录路径不对或未clone成功检查aqua-sim-ng文件夹是否在contrib下编译报错undefined reference to AquaSimPhy模块没被编译进NS-3确认contrib模块被build启用重新configure编译报错API不匹配如SetTransRange不存在NS-3版本和aqua-sim-ng版本不配套检查NS-3版本切换到aqua-sim-ng对应的release运行时提示Attribute not found配置的属性名拼写错误或该版本未定义用grep -r 属性名 contrib/aqua-sim-ng查头文件仿真时间很长但trace为空白流量生成器未正确挂载或目的地址配置错误用NS_LOG启动日志检查流量是否产生command not found: ns3不在NS-3根目录执行先回到ns-3-dev文件夹5.2 版本不兼容的终极解法如果你在编译时遇到一大堆报错而且内容类似“没有匹配的成员函数”“未声明的标识符”那大概率是版本不匹配。我试过用NS-3.37配默认的aqua-sim-ng结果编译到一半直接退出改回3.36马上就好了。真遇到这种情况不要硬碰先把报错文件路径和API名记下来然后去aqua-sim-ng的GitHub仓库issues页面搜索大概率已经有人报过类似问题。如果版本相差太大最简单的方式就是切换到推荐版本。有时候换个版本比自己改代码快得多。5.3 如何用日志调试仿真脚本NS-3内置了关注等级的日志系统用NS_LOG环境变量就能开启。比如export NS_LOGAquaSimPhylevel_info ./ns3 run scratch/first-aqua-sim-example这会输出每个物理层事件。如果你想看更细的流程把日志级别改成level_debug。我调试时最常用的组合是开AquaSimPhy、AquaSimMac、AquaSimChannel三个模块的日志基本能看到一帧数据从发送到接收的完整路径。这个方法处理“数据包发出去对方就是收不到”这类问题时特别有效一眼就能看出是物理层没收到还是MAC层没上交。5.4 如何判断仿真结果“合理”很多新手第一次跑通仿真后面对一堆数据不知道结果对不对。我给一个参考思路先构造一个极端简单的场景比如两节点一对一周期发包预期端到端时延约等于传播时延加上处理时延。如果仿真结果和理论值差出一个数量级说明配置大概率有问题。我在测试三节点中继场景时先用两节点直连验证过基本时延。两节点距离100米声速1500 m/s理论传播时延约66.7毫秒。看trace实际收到的时刻差确实在66到70毫秒左右符合预期。这个结果让我对后续实验数据的可信度有了底。做仿真实验永远要把“和理论对得上”放在第一位不然跑出来的曲线再漂亮也没法写进论文。6. 写在最后的一些经验这套环境我前后折腾了两三天核心时间都花在编译和API适配上了。如果你也想快速上手我的建议是第一严格按推荐版本组合安装别贪新第二先跑通自带example再改自己的场景不要一上来就写复杂脚本第三多翻aqua-sim-ng源码里的example目录那里面藏着最原汁原味的使用方式。Aqua-sim-ng这块还有很多可以深入的方向比如VBF路由的参数分析、MAC协议能耗对比、移动模型的影响等等。我后面应该会再写几篇把具体的协议实验和数据整理方法分享出来。仿真这条路最怕的就是“跑出结果但不知道为什么”和“结果错了但不知道去哪里排查”。希望这篇笔记能帮你把前一个问题变成“有依据”把后一个问题变成“有路径”。实际上手跑一遍你会发现水声网络仿真的门槛没有想象中那么高真正花时间的是理解每个模块背后的物理含义。