水下传感器网络仿真代码包:MAC与路由协议完整实现

发布时间:2026/10/18 12:48:53
水下传感器网络仿真代码包:MAC与路由协议完整实现 简介这份资源面向水下传感器网络UWSNs方向的研究生、科研人员与授课教师提供一套可直接运行的仿真代码与配套资料用于验证声波通信、路由协议、能量管理与数据融合等关键算法。压缩包共131个文件约22.23MB以C源码cc与h文件和Tcl仿真脚本为主体辅以29份PDF理论文档、doc说明、pptx讲稿以及throughput、energy、delay、loss等结果文件覆盖从建模、配置到性能统计的完整流程。其中statistics.awk等脚本可用于吞吐量、时延与丢包率的批量分析便于快速复现实验或改造为新的仿真场景。目前已有123人学习下载适合需要搭建水下网络实验环境、对比不同协议性能或开展课程项目的中高级读者参考。1. 水下传感器网络仿真一套能跑通 MAC 与路由协议的完整代码包水下传感器网络UWSN的仿真和陆地上那套 Wi-Fi、ZigBee 的仿真完全不是一回事。声波在水里的传播速度大约 1500 m/s比电磁波慢了五个数量级带来的直接后果就是传播时延大到能让你怀疑人生——几百毫秒起步多跳之后秒级延迟是常态。更麻烦的是水声信道带宽窄、误码率高、节点能量受限且往往随水流移动所以那些在陆地上跑得好好的 MAC 协议和路由协议搬到水下经常直接翻车。这个代码包就是冲着这个场景来的它把水下传感器网络里最核心的 MAC 层和路由层协议做成了可运行的仿真代码配套还带了说明资料适合做水声通信、海洋监测方向的研究生或者想快速验证协议想法的开发者。你拿到手之后不用从零搭信道模型直接改参数、换协议、看结果就行。2. 先看清这套仿真到底在仿什么模块划分与目录结构2.1 从压缩包到可运行工程目录里都有什么拿到一个仿真代码包我习惯先不急着跑而是把目录树扫一遍搞清楚哪些是核心逻辑、哪些是配置、哪些是结果输出。这个包的结构大致是这样的不同版本可能略有出入但核心目录跑不掉underwater-sensor-sim/ ├── src/ │ ├── channel/ # 水声信道模型 │ │ ├── acoustic_model.py │ │ └── noise_model.py │ ├── mac/ # MAC 协议实现 │ │ ├── aloha_mac.py │ │ ├── slotted_aloha.py │ │ └── handshake_mac.py │ ├── routing/ # 路由协议实现 │ │ ├── vbf_routing.py │ │ └── db_routing.py │ ├── node/ # 节点模型 │ │ └── sensor_node.py │ └── utils/ # 工具函数 │ ├── energy.py │ └── metrics.py ├── config/ │ └── sim_config.yaml # 仿真参数配置 ├── results/ # 仿真结果输出 ├── docs/ # 说明资料 └── main.py # 入口脚本这个划分方式很典型信道、MAC、路由、节点四层解耦每一层都可以单独替换。你想验证一个新的 MAC 协议只需要在mac/下加一个文件实现统一的接口然后在配置里切换就行不用动路由和信道的代码。这种设计对做协议对比实验特别友好也是我推荐大家拿到手之后重点理解的地方。2.2 水声信道模型为什么不能直接用自由空间损耗公式陆地上的无线仿真路径损耗通常用 log-distance 模型就够了。但水下不行声波在水中的吸收损耗和频率强相关而且不同频率的衰减差异巨大。这个包里acoustic_model.py用的是 Thorp 经验公式来计算吸收系数import math def thorp_absorption(freq_khz): Thorp 经验公式计算水声吸收系数 freq_khz: 频率单位 kHz 返回: 吸收系数单位 dB/km f2 freq_khz ** 2 # Thorp 公式三项分别对应不同频段的吸收机制 alpha 0.11 * f2 / (1 f2) \ 44 * f2 / (4100 f2) \ 2.75e-4 * f2 0.003 return alpha def transmission_loss(distance_m, freq_khz): 计算水声传播总损耗 distance_m: 传播距离单位米 freq_khz: 载波频率单位 kHz # 距离转换为 km d_km distance_m / 1000.0 # 吸收损耗 absorption thorp_absorption(freq_khz) * d_km # 扩展损耗水下通常用柱面扩展浅海或球面扩展深海 # 这里用球面扩展作为默认 spreading 20 * math.log10(distance_m) if distance_m 1 else 0 return spreading absorption这段代码的关键在于两个部分吸收损耗和扩展损耗。Thorp 公式里的三项分别对应低频弛豫、高频弛豫和纯水吸收你在 10 kHz 以下和 100 kHz 以上看到的损耗曲线斜率完全不同。扩展损耗那块代码默认用了球面扩展20log10但实际浅海环境更接近柱面扩展10log10这个参数在配置里可以改。如果你做的场景是浅海记得把扩展模型切过去否则算出来的通信距离会偏短仿真结果和实际对不上。2.3 节点模型与能量管理仿真里最容易被忽略的细节水下节点换电池的成本极高所以能量模型不是可选项是必选项。sensor_node.py里每个节点维护了自己的能量状态发送、接收、空闲三种状态的功耗各不相同class SensorNode: def __init__(self, node_id, position, initial_energy100.0): self.node_id node_id self.position position # (x, y, z) 三维坐标 self.energy initial_energy # 剩余能量单位焦耳 self.is_alive True # 不同状态的功耗参数单位瓦特 self.power_tx 2.0 # 发送功耗 self.power_rx 0.5 # 接收功耗 self.power_idle 0.01 # 空闲功耗 def consume_energy(self, state, duration): 根据状态和持续时间扣减能量 if state tx: cost self.power_tx * duration elif state rx: cost self.power_rx * duration else: cost self.power_idle * duration self.energy - cost if self.energy 0: self.is_alive False self.energy 0这里有个容易踩的坑很多仿真代码只算发送和接收的能量忽略了空闲监听的能量。但在实际水下网络中节点大部分时间都在空闲监听这部分累积起来相当可观。代码里power_idle设成了 0.01 W看起来很小但如果你仿真时间跑 10000 秒光空闲就消耗 100 焦耳比初始能量还多。所以跑长时间仿真的时候一定要把空闲功耗算进去否则节点存活时间的结论会过于乐观。3. 跑起来才算数环境搭建与第一个仿真场景3.1 依赖安装与配置检查这套代码是纯 Python 写的没有那些需要编译的 C 扩展所以环境搭建相对省心。我一般会先建一个虚拟环境避免和系统里的包打架# 创建虚拟环境 python -m venv uwsn_env source uwsn_env/bin/activate # Linux/Mac # uwsn_env\Scripts\activate # Windows # 安装依赖 pip install numpy scipy matplotlib pyyaml依赖就四个numpy 做数值计算scipy 做插值和信号处理matplotlib 画结果图pyyaml 读配置文件。版本上没有特别苛刻的要求numpy 1.20 以上、Python 3.8 以上都能跑。装完之后先别急着跑main.py打开config/sim_config.yaml看一眼这个文件决定了仿真的所有关键参数simulation: duration: 3600 # 仿真时长单位秒 time_step: 0.1 # 时间步长单位秒 area: x: 1000 # 仿真区域 x 方向长度单位米 y: 1000 z: 500 # 深度方向 num_nodes: 20 # 节点数量 channel: frequency_khz: 10 # 载波频率 spreading: spherical # 扩展模型spherical 或 cylindrical noise_level: -50 # 噪声功率谱密度dB mac: protocol: slotted_aloha # 可选aloha, slotted_aloha, handshake routing: protocol: vbf # 可选vbf, db energy: initial: 100.0 # 初始能量单位焦耳参数说明几个关键的time_step设成 0.1 秒是因为水声传播时延通常在百毫秒量级步长太大就会把时延细节抹掉frequency_khz设成 10 是水声通信的典型频段再高衰减就太大了spreading那个参数前面提过浅海场景改成cylindrical更准。3.2 运行第一个仿真从 ALOHA 开始配置检查没问题之后直接跑入口脚本python main.py --config config/sim_config.yaml --output results/跑完之后results/目录下会生成几个文件throughput.csv记录每个时间步的吞吐量energy.csv记录各节点剩余能量delay.csv记录端到端时延还有一张summary.png把关键指标画在一起。第一次跑建议就用默认的 slotted_aloha vbf 组合先看看基线性能长什么样。如果你想快速对比不同 MAC 协议不用改配置文件命令行直接覆盖就行# 跑纯 ALOHA python main.py --mac aloha --output results/aloha/ # 跑握手式 MAC python main.py --mac handshake --output results/handshake/跑完之后把两个目录下的throughput.csv拉出来对比你就能直观看到握手协议在负载高的时候吞吐量更稳但低负载时因为握手开销反而比 ALOHA 差。这种对比实验就是这套代码最核心的用法。3.3 结果解读吞吐量、时延和能量三个指标怎么看metrics.py里定义了三个核心指标的计算方式理解它们才能正确解读结果def compute_throughput(packets_delivered, packet_size_bits, duration): 计算网络吞吐量 packets_delivered: 成功送达的数据包数量 packet_size_bits: 每个包的比特数 duration: 仿真时长单位秒 返回: 吞吐量单位 bps return (packets_delivered * packet_size_bits) / duration def compute_end_to_end_delay(send_times, receive_times): 计算平均端到端时延 send_times: 各包的发送时间列表 receive_times: 各包的接收时间列表 返回: 平均时延单位秒 delays [r - s for s, r in zip(send_times, receive_times)] return sum(delays) / len(delays) if delays else 0 def compute_energy_efficiency(packets_delivered, total_energy_consumed): 计算能量效率每焦耳能量能传多少包 return packets_delivered / total_energy_consumed if total_energy_consumed 0 else 0看结果的时候有个经验先看吞吐量曲线有没有饱和再看时延是不是随负载线性增长最后看能量效率的拐点在哪。如果吞吐量早早饱和而时延飙升说明 MAC 层的冲突退避机制不够如果能量效率在低负载时就很低那多半是空闲监听功耗占比太高。这三个指标要连起来看单独看任何一个都容易得出片面结论。4. 核心协议拆解MAC 层和路由层的关键实现4.1 时隙 ALOHA 的时隙同步逻辑时隙 ALOHA 相比纯 ALOHA 的核心改进就是时间被划分成等长的时隙节点只能在时隙边界发送冲突窗口从整个包长缩短到一个时隙。代码里的实现是这样的class SlottedALOHA: def __init__(self, slot_duration, guard_time): self.slot_duration slot_duration # 时隙长度 self.guard_time guard_time # 保护间隔应对传播时延 self.current_slot 0 def can_transmit(self, node, current_time): 判断节点当前是否处于时隙边界 slot_index int(current_time / self.slot_duration) # 只在时隙开始的瞬间允许发送 time_in_slot current_time - slot_index * self.slot_duration return time_in_slot 1e-6 # 浮点比较留容差 def get_next_slot_time(self, current_time): 获取下一个时隙的起始时间 slot_index int(current_time / self.slot_duration) 1 return slot_index * self.slot_duration这里的关键参数是guard_time。水下传播时延大如果两个节点距离 500 米传播时延就是 0.33 秒左右。如果时隙长度是 1 秒保护间隔至少要留 0.33 秒否则远处节点的信号会漂移到下一个时隙里造成冲突。代码里guard_time默认设成最大传播时延的两倍这个值在配置里可以调。如果你把仿真区域改大了记得同步调大保护间隔不然结果会莫名其妙变差。4.2 基于向量的转发路由VBF位置信息怎么用VBF 是水下路由里比较经典的一个协议核心思想是利用节点的位置信息把数据包沿着从源到目的地的“路由向量”方向转发。只有那些位于路由管道内的节点才参与转发管道外的节点直接丢弃这样能大幅减少转发次数import numpy as np class VBFRouting: def __init__(self, radius, min_forwarding_delay): self.radius radius # 路由管道半径 self.min_delay min_forwarding_delay # 最小转发延迟 def is_in_pipe(self, node_pos, source_pos, sink_pos): 判断节点是否在路由管道内 vec_src_to_sink np.array(sink_pos) - np.array(source_pos) vec_src_to_node np.array(node_pos) - np.array(source_pos) # 计算节点到路由向量的垂直距离 cross np.cross(vec_src_to_sink, vec_src_to_node) distance np.linalg.norm(cross) / np.linalg.norm(vec_src_to_sink) return distance self.radius def compute_forwarding_delay(self, node_pos, source_pos, sink_pos): 根据节点到路由线的距离计算转发延迟 # 距离路由线越近延迟越小优先转发 vec_src_to_sink np.array(sink_pos) - np.array(source_pos) vec_src_to_node np.array(node_pos) - np.array(source_pos) cross np.cross(vec_src_to_sink, vec_src_to_node) distance np.linalg.norm(cross) / np.linalg.norm(vec_src_to_sink) # 距离越近延迟越小 return self.min_delay * (distance / self.radius)VBF 的精髓在compute_forwarding_delay这个函数里。它不是简单地让所有管道内节点同时转发而是根据节点到路由线的距离算一个延迟离路由线越近的节点延迟越小先转发远的后转发。后转发的节点听到前面已经转发过了就主动放弃从而减少冗余。这个机制叫“自适配转发”是 VBF 能省能量的关键。radius参数控制管道粗细设太小会导致管道内节点太少、路由断裂设太大又起不到过滤作用一般取通信半径的 0.3 到 0.5 倍比较合适。4.3 深度路由DB不依赖位置信息的备选方案VBF 需要每个节点知道自己的位置但在实际水下环境中GPS 信号在水下几米就衰减没了节点定位本身就是一个难题。DB 路由就是为了解决这个问题它只依赖节点的深度信息而深度可以通过压力传感器直接测量成本低得多class DBRouting: def __init__(self, depth_threshold): self.depth_threshold depth_threshold # 深度差阈值 def should_forward(self, packet_depth, node_depth): 判断节点是否应该转发 核心逻辑只有比上一跳更靠近水面的节点才转发 # 数据包从深往浅传节点深度必须小于包记录的深度 if node_depth packet_depth - self.depth_threshold: return True return False def compute_holding_time(self, packet_depth, node_depth): 计算持有时间深度差越大持有时间越短越早转发 depth_diff packet_depth - node_depth if depth_diff 0: return float(inf) # 不转发 # 深度差越大延迟越小 return 1.0 / depth_diffDB 路由的逻辑很简洁数据包从深水区往浅水区水面汇聚节点传每个收到包的节点看自己的深度是不是比包里记录的深度浅浅就转发不浅就丢。depth_threshold是为了避免深度差太小导致的无意义转发。compute_holding_time让深度差大的节点优先转发进一步减少冲突。这个协议的好处是完全不需要位置信息缺点是只能单向从深往浅传反向通信需要另外的机制。5. 避坑与排查仿真跑不通时先看这几条5.1 节点全部死亡导致仿真提前结束现象仿真跑了没几百秒就停了日志显示所有节点能量耗尽。原因通常是空闲功耗没配对或者时间步长太小导致能量扣减过于频繁。解决方法是检查sim_config.yaml里的power_idle参数如果仿真时长超过 1000 秒建议把空闲功耗降到 0.001 W 量级或者把时间步长从 0.1 秒放宽到 0.5 秒。另外确认一下initial_energy是不是设得太小默认 100 焦耳在 20 个节点、1 小时仿真的场景下是够用的。5.2 吞吐量始终为零或极低现象仿真正常结束但throughput.csv里全是零或者个位数。原因多半是通信距离设得太短节点之间根本连不上。检查transmission_loss函数里的扩展模型和频率设置10 kHz 频率下如果扩展模型用球面扩展1000 米距离的损耗大约是 60 dB 扩展加 3 dB 吸收总共 63 dB 左右。如果你的发送功率设成 10 dBm、接收灵敏度设成 -80 dBm那链路预算是够的。但如果频率设成了 50 kHz吸收损耗会飙升到 15 dB/km 以上链路预算就不够了。把频率降到 10 kHz 以下或者缩短节点间距问题一般能解决。5.3 时延数据出现负值现象delay.csv里出现了负数。原因是发送时间和接收时间的记录顺序搞反了或者时间同步没做好。检查compute_end_to_end_delay调用时传入的send_times和receive_times是不是一一对应的。另外确认仿真里所有节点用的是同一个全局时钟如果每个节点维护自己的本地时钟需要先做时间同步再计算时延。5.4 路由协议切换后结果完全不变现象把routing.protocol从vbf改成db跑出来的结果一模一样。原因通常是main.py里协议加载的逻辑有缓存或者配置文件的修改没被正确读取。检查main.py里读配置的部分确认routing字段被正确解析并传递给了路由模块的初始化函数。另一个可能是两个协议类实现了相同的接口但内部逻辑有 bug导致行为一致这种情况需要单独对路由模块做单元测试。5.5 仿真速度慢到无法接受现象20 个节点、1 小时仿真跑了半个小时还没结束。原因是时间步长太小加上 Python 循环效率低。优化方向有两个一是把time_step从 0.1 秒放宽到 0.5 秒仿真步数直接降到五分之一二是把节点间的信道计算向量化用 numpy 的广播机制一次性算所有节点对的损耗而不是用双重循环逐个算。如果还嫌慢可以考虑用 numba 对热点函数做 JIT 加速但那是后话了。6. 二次开发换协议、调参数、加指标6.1 新增一个 MAC 协议需要改哪些地方想加一个新的 MAC 协议最省事的方式是照着slotted_aloha.py的结构来。新建一个文件实现三个方法__init__接收配置参数can_transmit判断节点当前能否发送on_collision处理冲突后的退避。然后在main.py的协议注册表里加一行映射在配置文件的mac.protocol里写上你的协议名就行。接口统一是这个项目设计得最好的地方你不需要动信道和路由的任何代码。6.2 参数扫描用脚本批量跑配置组合做研究经常需要跑参数扫描手动改配置文件太慢。我一般会写一个批量脚本把关键参数做成笛卡尔积每个组合跑一次仿真最后把结果汇总成一张表import itertools import subprocess import yaml # 定义要扫描的参数空间 freqs [8, 10, 12, 15] # 频率 kHz macs [aloha, slotted_aloha, handshake] node_counts [10, 20, 30] results [] for freq, mac, n in itertools.product(freqs, macs, node_counts): # 生成临时配置 config { simulation: {num_nodes: n, duration: 1800}, channel: {frequency_khz: freq}, mac: {protocol: mac} } config_path ftmp_config_{freq}_{mac}_{n}.yaml with open(config_path, w) as f: yaml.dump(config, f) # 跑仿真 subprocess.run([python, main.py, --config, config_path, --output, fresults/scan_{freq}_{mac}_{n}/]) results.append((freq, mac, n))这个脚本跑完之后results/下会有一堆子目录每个对应一组参数。你可以再写个脚本把每个目录下的throughput.csv读出来画成热力图或者折线图一眼就能看出哪个参数组合最优。注意跑之前把duration调小一点1800 秒够看出趋势了跑太久浪费时间。6.3 加一个新指标从数据采集到可视化假设你想加一个“包投递率”指标需要改三个地方。第一在metrics.py里加一个计算函数def compute_delivery_ratio(packets_sent, packets_delivered): 计算包投递率 if packets_sent 0: return 0 return packets_delivered / packets_sent第二在main.py的仿真主循环里每次有包发送和送达时更新计数器。第三在结果输出部分把新指标写进 CSV并在summary.png里加一个子图。这三步做完你的新指标就和原有指标一样能自动输出了。我建议加指标的时候顺手写个单元测试用固定输入验证输出是否符合预期不然仿真跑了一大轮才发现指标算错了那才叫血亏。6.4 一个容易忽略的验证习惯最后说个我自己的教训。有一次我改了一个 MAC 协议的退避参数跑出来的吞吐量比基线高了 30%当时挺兴奋差点就直接写进报告了。后来不放心把随机种子固定下来重跑了一遍发现结果波动极大那 30% 纯粹是随机性带来的。从那以后我每次改完协议参数都强制用同一组随机种子跑三遍取平均值再下结论。这个习惯帮我省掉了好几次“假阳性”的坑。仿真这东西随机性带来的玄学波动比你想的大得多固定种子、多次重复、看方差这三步一个都不能少。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询