ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Matlab的WSN动态路由仿真:Dijkstra与随机路点模型实践

基于Matlab的WSN动态路由仿真:Dijkstra与随机路点模型实践 简介针对无线传感器网络中的路由优化问题这份资源基于Djisktra算法实现路径规划并模拟随机路点运动模型提供完整Matlab源码适合通信工程、计算机等相关专业学生及算法研究人员学习参考。压缩包共33个文件大小约169KB其中含5个M脚本用于主程序、路由构建、能耗计算与吞吐量统计20个MAT数据文件保存了不同算法的仿真结果另有7张运行效果图及1份说明文档。目前已有138人浏览学习。通过源码与数据可对比DjisktraRoute与ModifiedLEACH、CORPACO等方案在平均能耗、节点剩余能量、存活节点数和吞吐量等方面的表现有助于理解无线传感器网络路由机制和Dijkstra算法的实际应用。资源结构清晰便于按需查看数据与图表可直接用于课程实验复现或算法效果验证。1. 无线传感器网络动态路由仿真为什么 Dijkstra 加随机路点模型是最好上手的组合做无线传感器网络WSN课程设计或毕业设计的人大概率卡在同一个地方想仿真动态路由但 NS3、OMNeT 那套环境太重装完依赖就到交作业的截止日期了。这份资源解决得很干脆——用 Matlab 把“路由”和“移动”两个模块拆开再合起来路由层用 Dijkstra 算法按最短路径建链移动层用随机路点运动模型Random Waypoint让节点满区域跑两者叠加就是一张能一帧帧播放的动态 WSN 路由图。这个组合对正在做路由协议对比、无线网络仿真验证、或者想在 Matlab 里快速复现经典路径规划算法的从业者和学生都合适。压缩包里是完整的 Matlab 源码解压后按下面章节的思路改参数就能跑出自己的仿真数据不用从零写框架。2. Dijkstra 路由的核心逻辑邻接矩阵、权重设计与动态重算2.1 为什么 WSN 路由场景里选 Dijkstra 而不是 Floyd 或 Bellman-FordWSN 路由本质上是图论里的最短路径问题但选哪个算法不是随便定的。Dijkstra 是单源最短路径算法一次运行就能得到从汇聚节点sink到所有普通节点的最短路径这正好匹配 WSN 的场景所有数据最终都流向 sink我只需要算 sink 到各节点这一个源点集合。Floyd 算的是全源最短路径复杂度 O(N³)在 N 超过 50 之后每次重算的耗时明显拖慢仿真节奏而我要的只是单源结果用 Floyd 属于杀鸡用牛刀。Bellman-Ford 能处理负权边但 WSN 链路的权重通常是距离、能耗或它们的组合天然非负Bellman-Ford 的价值体现不出来反而每次迭代都扫全边浪费算力。A* 算法在路径规划里很流行但它需要启发式函数而随机拓扑下很难设计一个既不出错又能加速的启发式用不好反而退化成 Dijkstra。相比之下 Dijkstra 的贪心策略配合优先队列或简单数组实现代码量小、行为可预期作为动态仿真里被反复调用的底层函数再合适不过。2.2 邻接矩阵构建通信半径决定链路权重模拟路径损耗路由计算的第一步是把节点位置转成邻接矩阵。WSN 里两个节点能不能通信取决于它们的欧氏距离是否在通信半径 R 之内。链路权重如果只用距离体现不出无线信道的路径损耗特征我一般用距离的 alpha 次幂作为权重alpha 取值 2 对应自由空间模型取值 3 或 4 对应多径衰落环境。下面是构建邻接矩阵的函数function adj build_adjacency(pos, R, alpha) % pos: N x 2 矩阵, 每行是一个节点的(x, y)坐标 % R: 通信半径, 距离小于等于R的两个节点之间才存在链路 % alpha: 路径损耗指数, 链路权重 distance^alpha n size(pos, 1); adj zeros(n, n); for i 1:n for j i1:n d sqrt(sum((pos(i,:) - pos(j,:)).^2)); if d R d 0 w d ^ alpha; % 权重随距离指数增长 adj(i, j) w; adj(j, i) w; % 矩阵必须对称 end end end end这里有一个约定要贯穿整个仿真adj(i,j) 0表示节点 i 和 j 之间没有链路正数表示链路权重。主对角线保持 0 没关系因为后文 Dijkstra 函数里会用adj(u,:) 0来筛选邻居对角线上的 0 不会被误当成链路。alpha 取 2 时链路权重是欧氏距离的平方这样做的物理含义是一条远距离链路即使能通信代价也远高于两跳短链路路由会自动偏向多跳。调参时如果发现路由总是绕远路先检查 alpha 是不是设置得太大了。2.3 Dijkstra 核心函数的 Matlab 实现Dijkstra 函数我分两个文件写一个负责算 dist 和 prev一个负责回溯路径。这样在仿真主循环里只需要调用一次前一个函数就能拿到 sink 到所有节点的最短距离需要某条具体路径时再单独回溯。function [dist, prev] dijkstra_route(adj, src) % adj: 邻接矩阵, 0表示无链路, 正数表示链路权重 % src: 源节点编号, 在WSN仿真里通常是sink节点 % dist: 长度N的向量, src到每个节点的最短距离, 不可达为inf % prev: 长度N的向量, 记录最短路径树中每个节点的前驱节点 n size(adj, 1); visited false(n, 1); dist inf(1, n); prev zeros(1, n); dist(src) 0; for k 1:n unvisited find(~visited); [min_val, idx] min(dist(unvisited)); u unvisited(idx); if isinf(min_val) break; % 剩余节点全部不可达, 提前退出 end visited(u) true; neighbors find(adj(u,:) 0 ~visited); for j neighbors alt dist(u) adj(u, j); if alt dist(j) dist(j) alt; prev(j) u; end end end end这个实现有两个新手容易踩的细节。第一min(dist(unvisited))返回的是筛选后数组里的位置不是原始节点编号所以必须先用unvisited find(~visited)拿到未访问节点的原始编号列表再通过u unvisited(idx)映射回去。第二isinf(min_val)的提前退出判断不能省——当网络因为通信半径太小而分裂成多个连通分量时某些节点永远不可达如果不判断循环会在 inf 上继续空转。路径回溯函数如下function path reconstruct_path(prev, src, dst) % prev: dijkstra_route返回的前驱数组 % src: 源节点编号, dst: 目标节点编号 % path: 从src到dst的节点序列, 不可达时返回空数组 path []; if isinf(prev(dst)) src ~ dst return; end node dst; while node ~ src path [node, path]; % 从目标一路往前插 node prev(node); if node 0 % 防止prev断裂导致死循环 path []; return; end end path [src, path]; end回溯逻辑不复杂但node 0的防御检查值得保留。正常 Dijkstra 运行后 prev 数组里除了源节点每个节点都有前驱但一旦网络不连通或者代码被改动破坏了 prev 的完整性这个检查能避免 while 循环卡死。路径用[node, path]的方式往前插入保持从源到目标的顺序。2.4 动态拓扑下的路由重算策略节点在移动拓扑每时每刻都在变但路由不可能每帧都重算。重算太频繁路由振荡剧烈链路在两条近乎等价的路径之间来回切换统计指标没法看重算太慢路由严重滞后于实际拓扑数据包大量丢失。我一般用重算周期 T_route 来控制经验值是让节点在一个周期内移动的距离不超过通信半径的一半到一倍T_route R / v_max其中 R 是通信半径v_max 是节点最大移动速度。这样设置的意义是路由更新速度跟得上拓扑变化速度又不会对微小位移过于敏感。比如 R 20v_max 5T_route 取 4 秒相当于每 4 秒全网络重算一次最短路径树仿真步长 dt 取 0.1 秒时每 40 帧算一次路由计算负担完全可以接受。3. 随机路点运动模型让节点按真实规律动起来的轨迹生成器3.1 RWP 的三个参数与典型取值随机路点模型Random Waypoint是 Ad Hoc 网络仿真用得最多的移动模型逻辑简单每个节点随机选一个目标点以随机速度直线移动过去到达后停留一段时间再选下一个目标点。核心参数只有三个速度范围、停留时间、仿真区域。参数取值直接影响网络形态给一组我常用的配置参数典型值说明v_min1 m/s速度下限不能太小否则节点长期滞留原地v_max5 m/s速度上限决定拓扑变化激烈程度t_pause2 s到达目标点后的最长停留时间实际值在其下随机area100 x 100 m正方形仿真区域节点的活动范围v_min 这个参数最容易被忽略。RWP 模型有一个已知问题如果允许 v_min 趋近 0节点到达目标点后可能以极慢速度慢慢挪导致仿真中后期节点平均速度持续下降网络看起来越来越“懒”。后面避坑章节会单独展开这个问题参数层面对策就是给 v_min 设置一个合理的下限。3.2 轨迹生成器的 Matlab 实现生成轨迹的核心是逐节点维护一个状态机每个节点要么在移动中要么在暂停中要么刚到达正在选新目标。下面这个函数直接输出完整的轨迹序列供后面路由仿真逐帧读取function traj generate_rwp_trajectory(n, sim_len, dt, area, vmin, vmax, t_pause) % n: 移动节点数量 % sim_len: 仿真总时长(秒) % dt: 仿真步长(秒) % area: 仿真区域边长, 区域为area x area正方形 % vmin, vmax: 移动速度范围(m/s) % t_pause: 到达目标点后的随机停留时间上限(秒) % traj: steps x n x 2 数组, steps floor(sim_len/dt) steps floor(sim_len / dt); pos rand(n, 2) * area; % 初始位置均匀撒点 traj zeros(steps, n, 2); v zeros(n, 2); % 当前速度向量, 全0表示暂停 dest zeros(n, 2); % 当前目标点 pause_left zeros(n, 1); % 剩余暂停时间 for s 1:steps for i 1:n if pause_left(i) 0 pause_left(i) pause_left(i) - dt; continue; % 暂停中不移动 end if all(v(i,:) 0) % 暂停结束或初始状态, 选新目标 dest(i,:) rand(1, 2) * area; speed vmin (vmax - vmin) * rand(); dir_vec dest(i,:) - pos(i,:); v(i,:) dir_vec / norm(dir_vec) * speed; end pos(i,:) pos(i,:) v(i,:) * dt; if norm(pos(i,:) - dest(i,:)) norm(v(i,:)) * dt % 防过冲 pos(i,:) dest(i,:); % 强制对齐到目标点 v(i,:) 0; pause_left(i) t_pause * rand(); end end traj(s,:,:) pos; end end这段代码里三个工程细节值得说明。第一到达判断用的是norm(到目标点距离) 速度 * dt而不是直接判断距离为 0因为数值计算下节点可能永远无法精确落在目标点上用步长判断可以防止节点反复穿越目标点。第二暂停期间节点位置完全不动只是倒计时归零后立刻选新目标这样生成的轨迹才符合 RWP 的定义。第三初始位置用rand(n,2) * area均匀撒点而不是全部放原点避免仿真初始阶段所有节点挤在一起导致路由退化成单跳。3.3 边界处理与轨迹分布的偏差问题上面的生成器没有处理边界碰撞。节点速度方向是随机选的目标点也在区域内但直线移动路径可能超出区域边界。我在实际项目中通常给 RWP 加一层反射边界处理节点坐标超出区域时把对应轴的速度分量取反并把位置拉回区域内。反射边界会让节点轨迹始终在区域内而且能保持速度方向随机性比直接环绕边界更接近真实移动设备的反弹行为。边界处理之外RWP 模型还有一个统计学层面的坑。节点在移动过程中会自然聚集到区域中心附近导致节点空间分布不均匀中心区域密度远高于边缘。如果路由仿真发现平均跳数随时间持续下降不要高兴得太早那可能是节点扎堆导致路由变短而不是算法优化效果好。对分布均匀性有要求的仿真可以考虑把移动模型换成随机方向模型Random Direction或者对速度按稳态分布做加权采样。4. 完整仿真流程把 Dijkstra 和移动模型串成一帧帧动态网络4.1 主循环骨架从轨迹到路由统计把前面两个模块拼起来主循环的思路是预先生成全部轨迹然后逐帧读取节点位置按重算周期调用 Dijkstra 计算路由最后把每一帧的路由统计信息汇总。完整的 Matlab 主脚本如下%% 参数配置 n 50; % 节点总数, 包含1个sink area 100; % 仿真区域 100x100 R 20; % 通信半径 alpha 2; % 路径损耗指数 vmin 1; vmax 5; % 移动速度范围 t_pause 2; % 暂停时间上限 dt 0.1; % 仿真步长 sim_len 100; % 仿真总时长 route_period 4; % 路由重算周期 sink 1; % sink固定在原点 %% 生成移动节点轨迹, sink不参与移动 rng(0); % 固定随机种子保证可复现 traj_mobile generate_rwp_trajectory(n-1, sim_len, dt, area, vmin, vmax, t_pause); steps size(traj_mobile, 1); pos_all zeros(steps, n, 2); pos_all(:, 2:end, :) traj_mobile; pos_all(:, 1, :) repmat([0 0], steps, 1); % sink固定在(0,0) %% 逐帧计算路由并统计 route_step round(route_period / dt); connect_rate_list zeros(steps, 1); avg_hop_list zeros(steps, 1); energy_list zeros(steps, 1); for s 1:steps pos squeeze(pos_all(s, :, :)); if mod(s-1, route_step) 0 || s 1 % 每隔route_step帧重算一次 adj build_adjacency(pos, R, alpha); [dist, prev] dijkstra_route(adj, sink); % 逐节点回溯, 统计连通率、平均跳数、能耗 hop_sum 0; reachable_cnt 0; for i 2:n if isinf(dist(i)) continue; % 该节点不可达 end path reconstruct_path(prev, sink, i); hop_sum hop_sum (length(path) - 1); reachable_cnt reachable_cnt 1; end connect_rate_list(s) reachable_cnt / (n - 1); avg_hop_list(s) hop_sum / max(reachable_cnt, 1); energy_list(s) sum(dist(~isinf(dist))); % 用距离和近似能耗 else % 未到重算周期, 沿用上一帧的统计结果 connect_rate_list(s) connect_rate_list(s-1); avg_hop_list(s) avg_hop_list(s-1); energy_list(s) energy_list(s-1); end end %% 汇总输出 fprintf(平均连通率: %.2f\n, mean(connect_rate_list)); fprintf(平均跳数: %.2f\n, mean(avg_hop_list)); fprintf(平均能耗(距离和): %.2f\n, mean(energy_list));主循环里一个容易被忽略的点是统计结果只在重算周期内更新其余帧直接沿用上一帧的值。这样做的原因是在两次重算之间路由表没有变化强行逐帧重新统计反而会把“路由未更新”这个事实掩盖掉。另外rng(0)固定随机种子这一步很关键——WSN 仿真里随机性来源太多不固定种子的话两次跑出来的结果差异可能比不同参数之间的差异还大后面做参数对比会做不下去。4.2 三个统计指标怎么解读connect_rate 反映网络的连通质量。这个值如果长期低于 0.9说明通信半径相对节点密度偏小网络碎片化严重Dijkstra 算出来的路径大部分都是不可达的先调 R 而不是调算法。avg_hop 反映路由长度理论上连通率稳定的情况下节点密度越高、通信半径越小平均跳数越大因为链路被切得更碎。energy_list 用所有可达节点的距离和来近似总能耗虽然粗糙但作为相对比较已经足够——同一组参数下谁更省能耗看这个值的相对大小就能判断。4.3 动态可视化的性能优化可视化环节最容易翻车的是每帧都调用plot重画。节点数量一上 50Matlab 的图形系统就会被反复创建对象拖垮仿真帧率掉到个位数。我习惯用句柄式更新figure; h_nodes scatter(pos_all(1,:,1), pos_all(1,:,2), 40, filled); hold on; h_links plot(nan, nan, k-, LineWidth, 0.5); hold off; xlim([0 area]); ylim([0 area]); for s 1:steps pos squeeze(pos_all(s, :, :)); set(h_nodes, XData, pos(:,1), YData, pos(:,2)); % 按需绘制路由链路, 用NaN分段避免连线交叉 if mod(s-1, route_step) 0 adj build_adjacency(pos, R, alpha); [~, prev] dijkstra_route(adj, sink); % 提取sink到各节点的路径线段, 拼成带NaN分隔的坐标序列 line_x []; line_y []; for i 2:n if ~isinf(dist(i)) i ~ sink path reconstruct_path(prev, sink, i); line_x [line_x; pos(path,1); nan]; line_y [line_y; pos(path,2); nan]; end end set(h_links, XData, line_x, YData, line_y); end drawnow limitrate; % 新版本Matlab支持限帧率刷新 end这里有两个技巧。第一scatter和plot的句柄只创建一次后续循环里只更新XData和YData图形对象数量不增长帧率能提升数倍。第二drawnow limitrate会把刷新率限制在约 20 帧每秒避免为了可视化白白烧掉 CPU 算路由。如果你的 Matlab 版本较老没有 limitrate 选项退回到drawnow也能跑只是慢一些。5. 避坑指南五个让 WSN 路由仿真翻车的细节5.1 网络不连通路由结果全是 inf现象仿真跑完连通率只有 50% 上下dist 数组里一多半是 inf画出来的网络图稀稀拉拉。原因节点随机撒点后通信半径 R 过小或者节点数量不足网络分裂成多个连通分量Dijkstra 对不可达节点返回 inf路由自然断掉。这本质上是图连通性问题不是算法问题。解决在跑正式仿真之前先单独做一次连通性预检。构建邻接矩阵后统计孤立节点比例如果不可达节点超过 5%直接调大 R 或增加节点数。另外节点初始位置用均匀随机分布时要保证平均每个节点的邻居数不低于 4经验上对应 R 与节点密度的关系是n * pi * R^2 / area^2 4。5.2 随机路点模型的平均速度陷阱现象仿真跑到 200 秒以后节点速度明显变慢平均跳数下降看起来路由算法“收敛”了其实是节点越来越不想动。原因RWP 模型的节点速度分布存在稳态偏移。高速节点很快到达目标点进入暂停低速节点还在路上慢慢走导致整个系统的平均速度随时间衰减并偏向低速。更麻烦的是节点的空间分布会向区域中心聚集边缘节点密度下降路由路径变短不是因为算法好而是因为节点扎堆。解决把 v_min 抬高到 1 m/s 以上缩小速度范围减小暂停时间的随机性。对分布均匀性敏感的场景换用随机方向模型或导入外部轨迹文件别再让 RWP 的统计特性干扰算法评估。5.3 邻接矩阵主对角线上的幽灵链路现象回溯出来的路径里出现连续重复节点例如 [1, 5, 5, 9]跳数计算也比预期偏大或偏小。原因我在最早版本的构建函数里用的是adj ones(n,n)再挖掉无链路位置导致主对角线全为 1节点到自己的链路被当成真实链路。Dijkstra 遍历邻居时把自身也纳入松弛集合路径中就会混入自环。解决构建邻接矩阵统一用全零初始化只对满足距离条件且 i≠j 的节点对赋值。Dijkstra 函数里查邻居时始终用adj(u,:) 0作为过滤条件并确保src节点的 dist 初始为 0这样从数学上排除自环进入最短路径的可能。5.4 路由重算太频繁引发链路振荡现象平均跳数和能耗曲线像锯齿相邻两个重算周期内路径频繁切换比如节点 3 到 sink 的路径一会儿走 8 跳一会儿走 7 跳来回横跳。原因重算周期 T_route 太短节点每移动一点点就把整棵最短路径树重算一遍Dijkstra 对权重微小变化过度敏感两条代价近似的路径会反复交替。解决把 T_route 设成节点穿越通信半径所需时间的量级也就是T_route R / v_max。这样设置后在两次重算之间拓扑结构变化显著路由切换的决策基于真实拓扑变化而不是数值抖动。实测下来这条经验值能把链路振荡频率降一个数量级。5.5 动态可视化卡成 PPT现象节点数超过 60 后每帧重画整个图仿真速度跌到每秒两三帧等它跑完人都想关电脑。原因在循环体内直接调用plot或scatter会反复创建和销毁图形对象Matlab 图形系统开销随对象数量线性增长。更隐蔽的是每帧还把所有链路用plot全量重画图形对象数量直接爆炸。解决按 4.3 节的方案做——提前创建图形对象句柄循环内只更新XData和YData。绘制链路时用 NaN 作为分段符把多条路径的坐标拼进一个数组一次画完不要每条路径调一次plot。配合drawnow limitrate限帧50 节点的动态仿真能保持肉眼流畅。6. 进阶玩法把单次仿真改造成参数扫描实验台单次仿真跑通只是第一步实际做论文或项目时你需要的是在不同节点数、不同通信半径、不同移动速度下反复对比。我的建议是把仿真拆成两段式结构先统一生成一批轨迹并保存然后针对不同参数只重跑路由评估部分。轨迹是随机过程如果每组参数都重新生成轨迹两次结果的差异里既有参数的影响也有随机性的影响说不清楚。轨迹固定后路由器部分变成确定性计算不同参数之间的差异就纯粹来自参数本身。批量扫描的代码如下所示核心在固定随机种子和复用轨迹node_counts [20 50 100 200]; results zeros(length(node_counts), 3); for i 1:length(node_counts) n node_counts(i); rng(10 i); % 不同规模对应不同种子, 但同规模可复现 traj_mobile generate_rwp_trajectory(n-1, sim_len, dt, area, vmin, vmax, t_pause); % 这里把轨迹预生成和路由评估分离, 轨迹存到traj_list中供后续复用 [connect, hop, energy] run_simulation_with_traj(traj_mobile, R, alpha, route_period); results(i, :) [connect, hop, energy]; end把路由评估部分封装成run_simulation_with_traj这样的函数输入只有轨迹和路由参数输出三个核心指标。这样每次扫描只改一个变量其他变量保持不变。做敏感性分析时可以固定轨迹、只改 R看通信半径对平均跳数和能耗的影响曲线用boxchart画每个参数档位的分布盒式图比单条曲线更有说服力。验证方面我习惯加一组静态网络基线对比——把 vmax 设成 0节点完全不移动其他条件不变。动态路由和静态基线的能耗差就是移动性带来的额外开销这个数字经常被忽略但评审老师很爱问。如果扫描时发现某些参数组合下动态结果反而优于静态先查是不是 RWP 轨迹的边界分布问题别急着下结论。关于能耗指标有一点提醒用sum(dist(~isinf(dist)))作为能耗只是粗略近似。真正严谨的做法是给每个节点分配初始能量每转发一次数据包按权重扣减节点能量耗尽后退出网络模拟网络生命周期。这个扩展做起来不难在 Dijkstra 返回的 dist 基础上乘一个单位能耗系数再累加到各节点的剩余能量数组里即可。我后来做网络生命周期仿真时就是在这个框架上改的改动量不超过 60 行。从那以后我每次跑这类动态路由仿真都会把“连通率预检、速度分布检查、重算周期合理性”三件事写成固定检查项跑正式参数扫描前先过一遍省掉了无数次半夜翻车的折腾。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进