做了这么多年的网络仿真,我越来越觉得ns-3好玩的地方不在跑通Demo,而在你把一条链路调得跟真实设备行为对得上。上一篇文章把框架概念过了一遍,这次直接进入正题:链路层与物理层。这一篇会从Wi-Fi信道模型、物理层丢包计算、MAC层退避机制到多节点仿真中常见的“静态节点漂移”问题挨个拆一遍,重点是参数背后的计算逻辑,以及我怎么把这些参数落到实际仿真脚本里的。内容偏实操,适合已经能跑通ns-3基础Script、想深挖协议行为细节的人。
1. 一个反直觉的现象:为什么改了发射功率,结果纹丝不动
先说一个我自己踩过的坑。有一阵子我在调一个无线链路仿真,目的是观察不同发射功率下的端到端丢包率。我拿到ns-3的默认Wi-Fi模型,直接改了YansWifiPhy的TxPowerStart,从20dBm一路调到5dBm,满心以为丢包率会有一个明显的爬坡过程。跑完之后我傻眼了:无论怎么调,所有节点的收包成功率都是100%,曲线平得像条直线。
当时第一反应是脚本写错了,后来一行行检查才发现问题不在脚本,而是出在物理层的默认配置上。
我用的信道模型是YansWifiChannel,它底层接的是PropagationLossModel。这个模型负责计算信号从发送端到接收端经过路径损耗之后还剩多少功率。我虽然在物理层设置了发射功率,但因为没有显式配置路径损耗模型,ns-3默认给的是PropagationLossModel这个空壳类,它不计算任何损耗,信号功率在信道上根本不会衰减。
这就好比你在真实世界里拿着对讲机喊话,我这边却没把空气的阻力算进去——信号满功率到达,任何距离都不丢包,物理层表现得像开了挂一样。
正确的做法是在信道模型上挂一个具体的有损传播模型。ns-3里最常用的是LogDistancePropagationLossModel,它会按照公式
[ PL(d) = PL(d_0) + 10 \cdot n \cdot \log_{10}\left(\frac{d}{d_0}\right) ]
计算路径损耗。其中(d_0)是参考距离,默认1米;(n)是路径损耗指数,室内环境通常取2.5到3.5,开阔空间取2.0左右。我把n设成3.0,TxPowerStart和TxPowerEnd都设成17dBm,再跑同样的场景,丢包率曲线立刻就有了反应——距离拉大到100米之后,接收功率跌到接收灵敏度以下,帧就开始丢了。
这里要给第一次接触ns-3的朋友提个醒:TxPowerStart和TxPowerEnd两个参数必须同时设置,如果只改了TxPowerStart,ns-3内部默认会在两个值之间做均匀随机取值,结果是每次发包的功率不一样,仿真结果就带上了随机波动,排查问题的时候很容易误判。
所以说,链路层和物理层的仿真,第一步就是把“信号会衰减”这个事实给建模出来。信道模型选对了,后面所有的调整才有意义。
2. 无线信道模型选择:除了对数距离,还有哪些要横向对比
信道模型是整个物理层仿真的地基。我在实际项目中一般会根据场景需求,从三个维度来判断该用哪种模型:距离范围、频率依赖、是否有遮挡物。下表是我常用的对照:
| 模型 | 损耗计算方式 | 适用场景 | 需要注意的地方 |
|---|---|---|---|
LogDistancePropagationLossModel | 固定指数对数距离 | 开阔空间、室内简单链路 | 单一指数不能反映近距离和远距离的差异 |
ThreeLogDistancePropagationLossModel | 三段距离不同损耗指数 | 分层场景,比如办公区隔墙 | 需要同时配置三个距离边界和三个指数 |
NakagamiPropagationLossModel | 随机衰落 | 移动场景、多径环境 | 每次仿真结果会带随机波动,需要多次取平均 |
FriisPropagationLossModel | 自由空间损耗 | 理论验证、短距离 | 只适合无遮挡的视线传播 |
我的建议是:如果你的仿真场景里节点相对静止、只关心距离带来的影响,优先用LogDistance;如果场景里有墙体或者分区,用ThreeLogDistance更合适,因为它允许你设置0到30米用指数2.0,30到80米用指数3.5,超过80米用4.0,这比单一指数更贴近真实室内环境;如果节点在移动,那一定要加Nakagami或者RandomPropagationLossModel,不然移动带来的多径衰落完全体现不出来。
还有一个小细节很多人不知道:ns-3的YansWifiChannel上可以同时挂多个PropagationLossModel。如果你既要路径损耗又要快衰落,不要自己写一个复杂模型,直接用PropagationLossModel的AddPropagationLoss方法把两个模型串起来,ns-3会按顺序先后计算。我一般会在LogDistance后面挂一个Nakagami,这样既保证了平均信号强度的衰减,又叠加了随机性,曲线看起来和真实环境更接近。
另外,PropagationDelayModel这个参数也值得留意。默认的ConstantSpeedPropagationDelayModel假设信号以光速传播,时延只跟距离有关。但如果你在仿真水下声学网络或者对时延极端敏感的场景,就得换成RandomPropagationDelayModel,给时延加上随机抖动,不然所有节点的时间同步行为会好得不像话,跟现实偏差很大。
3. 物理层丢包是怎么算出来的:从SNR到误码率
物理层丢包是链路仿真里最核心的一个指标,可很多人只是把DataMode改一改,看到丢包率变了就算交差,完全不清楚丢包是怎么算出来的。这一节我把计算链路完整捋一遍。
ns-3在接收端拿到一个帧之后,会先根据信道模型算出的接收功率和底噪,得到信噪比(SNR)。然后它会根据当前的调制编码方案(MCS),把SNR映射成误码率(BER),再根据帧长度算出误帧率(PER),最后用PER决定这一帧是不是被丢弃。
不同调制方式的BER计算公式不一样。比如BPSK在加性高斯白噪声信道下的误码率可以近似写成
[ P_b = Q\left(\sqrt{\frac{2 \cdot E_b}{N_0}}\right) ]
而QPSK把符号分成I/Q两路,误码率比BPSK要差一些。更高阶的16QAM、64QAM对信噪比的要求就更高,误码率曲线也更陡峭。这就是为什么仿真里把DataMode从OfdmRate6Mbps改成OfdmRate54Mbps之后,同样距离下的丢包率会急剧上升——不是信道变了,而是同一个SNR对应的BER变大了。
ns-3里默认由WifiPhy的子类来执行这个映射过程。对OFDM系统来说,InterferenceHelper会计算每个帧的SNR,然后交给WifiPhy::GetPayloadReceiveSuccessRate之类的函数,查表得到接收成功率。如果你用的是YansErrorRateModel,它内部预置了几张BER对照表,专门对应不同的调制编码方式。
这里有个实操技巧:如果你搞的是NIC仿真,或者信道环境很特殊,自带的误差模型可能不够准。ns-3允许你继承ErrorRateModel自己实现一个,只需要覆写GetChunkSuccessRate方法。我自己有段时间仿真窄带物联网场景,就按LoRa的BER公式写了一个自定义模型,核心就是把每个SF(扩频因子)对应的SNR门限重新定义一遍。写完挂到YansWifiPhy上,效果比默认模型贴合得多。
一句话总结:想让物理层丢包符合直觉,你至少要能回答三个问题——接收功率是多少、SNR是多少、这个调制方式在这个SNR下的误码率是多少。三者缺一,丢包率就只是凑出来的数字。
4. MAC层行为不是想当然:CSMA/CA的退避参数直接影响吞吐
链路层的另一大块是MAC协议。ns-3默认的Wi-Fi MAC是WifiMac,它采用CSMA/CA机制,核心逻辑就是信道空闲时才能发送,但发送前必须经过一段随机退避时间,如果信道忙就冻结退避计数器,等信道空闲再继续倒数。
很多人在仿真里遇到吞吐上不去,第一反应是改物理层速率,却忽略了MAC层退避参数的阻尼效应。我们来看实际数字:Slot时间在802.11a里是9微秒,SIFS是16微秒,DIFS是34微秒。退避窗口的初始值CWmin是15,意味着第一个退避时长是Random(0, 15) * 9us。如果发送失败,竞争窗口翻倍,最大到1023。
在ns-3里,这些参数都集中在WifiMac的配置属性里。最直接的做法是改CwMin和CwMax。我在一个高密度场景里做过测试:40个节点同时竞争信道,默认CwMin=15时,碰撞率非常高,整体吞吐只有11Mbps左右;我把CwMin改成31、CwMax改成1023之后,碰撞率下降,吞吐提升到19Mbps。
原因也很简单:节点越多,同一时刻落在同一个退避槽里的概率越大,碰撞概率跟退避窗口大小直接相关。窗口太小,大家退避时间都差不多短,碰撞多;窗口太大,信道浪费在空闲等待上,吞吐也上不去。所以这个参数需要在具体节点密度下反复试。
还有一个容易忽视的地方是RtsCtsThreshold。默认值是65535字节,等于关掉了RTS/CTS机制。在高干扰场景里,隐藏终端问题会导致大量碰撞。把阈值设成500字节之后,大帧会先走RTS/CTS握手,虽然多了两次控制帧开销,但碰撞减少带来净增益。我在一个多AP场景里实测,RTS/CTS开启后UDP吞吐提升了约23%。
另外,ns-3的MAC层还支持队列管理。默认WifiMacQueue的MaxSize是400个包,如果应用层灌包太快,队列满了之后新增的包会被直接丢掉,这不等于物理层丢包,但在吞吐统计里和丢包率混在一起,容易产生误判。建议做饱和吞吐测试的时候,把MaxSize调大,比如4000个包,保证瓶颈出现在信道上而不是队列上。
5. 多节点场景里的隐藏终端与仿真稳定性:我踩过的具体坑
多节点仿真和两节点之间完全是两种难度。两节点只要信道模型和物理层参数对,基本不会出大问题。节点一多,隐藏终端、竞争窗口、队列溢出这些因素全部叠加在一起,吞吐和丢包曲线就开始变得不可控。
我印象最深的一次是组一个星型拓扑:中心一台AP,周围30个站节点同时向AP发UDP流。第一次跑的时候,整体吞吐在5秒之后直接塌陷,终端里的接收包数量急剧减少。我一开始怀疑是物理层干扰,后来把PhyRxDrop的Trace挂上,看到丢包原因全是RXING,也就是接收过程中被其他传输中断。这明显就是CSMA/CA没能有效协调信道访问,隐藏终端导致的。
排查链路是这样的:先看WifiMac的日志,确认各节点的退避参数;然后把CwMin从15拉到31;再把RtsCtsThreshold从默认值降到500字节。改完重跑,吞吐就稳定住了。这里要强调一下,我跟踪丢包用的是ns-3的PhyRxDrop和PhyTxDrop两个Trace源,它们能把丢包事件和原因打印出来,远比靠应用层重传次数反推可靠。
前面讲了这么多参数,下面把我最常用的一个演示脚本核心部分放出来,方便你直接对照。
// 配置信道与传播模型 YansWifiChannelHelper channel = YansWifiChannelHelper::Default(); channel.AddPropagationLoss("ns3::LogDistancePropagationLossModel", "Exponent", DoubleValue(3.0)); channel.SetPropagationDelay("ns3::ConstantSpeedPropagationDelayModel"); // 配置物理层 YansWifiPhyHelper phy; phy.SetChannel(channel.Create()); phy.Set("TxPowerStart", DoubleValue(17)); phy.Set("TxPowerEnd", DoubleValue(17)); phy.Set("RxSensitivity", DoubleValue(-92)); // 接收门限 // 配置MAC层 WifiMacHelper mac; mac.SetType("ns3::AdhocWifiMac"); Config::SetDefault("ns3::WifiMacQueue::MaxSize", StringValue("4000p")); Config::SetDefault("ns3::WifiRemoteStationManager::RtsCtsThreshold", UintegerValue(500)); // 装配到节点 NetDeviceContainer devices = wifi.Install(phy, mac, nodes);这段配置看起来不复杂,但每一行都是踩过坑之后确认的。RxSensitivity设为-92dBm,意味着接收功率低于这个门限的帧直接被视为不可解调;RtsCtsThreshold设成500,就会让大于500字节的帧先走握手流程。最后用wifi.Install把物理层和MAC层一起装配到节点上,整个链路才算完整。
还有一个经常被忽略的问题是仿真步长与稳定性的关系。ns-3默认事件粒度很细,但当你调大节点数量之后,如果让所有节点在同一时刻启动发送,会产生初始碰撞峰值。我在脚本里习惯给每个节点加一个随机的启动偏移:
for (uint32_t i = 0; i < nodes.GetN(); ++i) { double offset = i * 0.001 + (double)rand() / RAND_MAX * 0.01; appStartTimes[i] = Seconds(0.5 + offset); }别小看这几十毫秒的偏移,它能把开局阶段的碰撞率降下来很多,而且不会影响稳态吞吐的测量。
静态节点漂移问题也值得单独讲一下。ns-3早期版本里,即使节点没有设置移动模型,由于物理层的默认移动模型可能带有微小随机速度,节点坐标会在仿真过程中缓慢漂移。这种情况在长时间仿真里会导致信号功率略微波动,表现就是同样的配置跑两次,结果有细微出入。解决办法是在安装完节点位置之后,显式设置一个ConstantPositionMobilityModel,把速度锁成零。这样不管跑多久,节点的位置计算都会保持稳定。
6. 仿真调试习惯:从Trace到可视化的一次性打通
到了最后一部分,我想分享几个调试习惯,这些习惯在真实项目中帮我省了大量排查时间。
第一,一定是先开Trace,再开图形界面。我见过很多人一上来就跑NetAnim,看到动画里包在乱飞,却感知不到物理层的失步。我更习惯的做法是先把PhyRxDrop、PhyTxDrop、MacRxDrop三个Trace源挂到回调函数里,把所有丢包事件打上时间戳、节点编号和原因,跑完直接看日志。等确认丢包行为合理了,再开NetAnim做演示动画,这样动画里展示的包流动才有可信度。
第二,配置参数尽量集中管理。我会在脚本头部维护一个Config结构体,把发射功率、传播模型指数、RtsCts阈值、队列大小等关键参数集中存放。做参数扫描分析的时候,直接写一个循环,每次只改一个参数,输出文件按参数值命名,避免一次跑完几十组数据却分不清哪组对应哪个参数。别问我为什么强调这个,我当年就是吃过乱改参数导致结果不可复现的亏。
第三,善用WifiPhy和WifiMac的日志级别。ns-3的日志系统非常强大,把NS_LOG_LEVEL_INFO组件名设置成WifiPhy或WifiMac,你就能看到每一帧的接收功率、SNR、是否成功解调的完整记录。缺点是输出量极大,适合小拓扑,不适合大规模仿真。调大场景之前一定要把日志关掉,不然磁盘空间先炸了。
第四,跑多组随机种子取平均值。ns-3使用RngRun来区分不同的随机种子,同样的参数、不同的RngRun,结果会有随机起伏。很多结论需要用5到10组RngRun取平均才能获得稳定曲线。我在论文数据采集阶段,每个参数组合至少跑10组种子,最后报告里只写均值和标准差。
最后补一句关于性能的:大规模仿真里,物理层和MAC层的计算开销占比相当高。如果你只是关心网络层的路由行为,可以通过关闭WifiPhy的误差模型计算来加速,但如果你要的结果恰恰是丢包率,那误差模型就是命根子,省不得。这就回到最开始说的那句话——链路层和物理层的仿真,核心不在API调得多溜,而在于你清楚地知道每个参数在真实系统中对应哪个物理事实。把这一层想通了,ns-3对你来说就不是一个黑盒工具,而是一台能随时拆开检查的精密仪器。