资讯详情 网络拓扑图源码:可联动、可二次开发的动态拓扑底座
📅 2026/10/14 12:55:39
简介本资源是一套可二次开发的网络拓扑图可视化源码面向中高级前端与全栈开发者、网络运维工程师及高校计算机专业学生用于快速构建自定义网络监控界面、教学演示系统或IT资产可视化管理平台。项目已过实际优化涵盖图数据建模、力导向布局算法、动态节点交互、实时状态渲染等核心能力支持基于D3.js或类似图形库的深度定制。压缩包共507个文件主体为71个XML配置与元数据文件定义拓扑结构与设备属性、60个PNG/GIF图标资源含设备与连接样式、15个JS脚本实现拖拽、增删节点、边关系绑定等交互逻辑及8个JSP/Java后端接口示例整体仅2.65MB轻量易集成。目前已有1121人学习下载代码结构模块清晰CSS样式分离含IE6兼容适配并保留SVN版本痕迹便于理解迭代过程与工程演进路径。1. 网络拓扑图源码不是画图工具而是可嵌入、可联动、可二次开发的拓扑底座你手头有一套设备清单、几台交换机的 SNMP 输出、一段 NetFlow 聚合数据但每次想看“当前流量从哪来、往哪去、卡在哪”还得手动拖拽 Visio 或在线绘图工具——改个 IP 就要重连线加个新节点就得调布局导出 PNG 后再也没法点击查端口状态。这不是拓扑图这是静态快照。真正的网络拓扑图源码是把“节点-连接-状态”三要素全部代码化节点位置可由坐标或力导向算法动态计算连线能绑定真实接口指标如ifInOctets增量点击设备能触发 API 请求获取实时 CPU/内存甚至支持右键菜单下发 CLI 命令。它不依赖特定网管平台也不绑定某家厂商 SDK而是一套用标准 Web 技术栈SVG D3.js / Vue ECharts WebSocket组织的、带完整数据驱动逻辑的前端工程。适合正在做自研运维平台、需要对接 Zabbix/Prometheus/Nagios 的 DevOps 工程师或是高校网络实验课中要求学生理解“拓扑即状态”的教学项目开发者。它解决的从来不是“怎么画得好看”而是“怎么让图真正活起来”。2. 拓扑图源码结构解析从目录树到核心渲染链路拓扑图源码不是单个 HTML 文件而是一个具备明确分层职责的工程结构。我拆过不下二十个开源拓扑项目真正能落地进生产环境的基本都遵循“数据层 → 模型层 → 渲染层 → 交互层”四层分离。下面以一个典型基于 Vue 3 TypeScript D3-force 的拓扑图源码为例逐层说明其设计意图与关键文件作用。2.1 目录结构与各层定位提示不要一上来就跑npm run serve。先看清目录骨架否则后续改配置、加节点类型时会反复踩坑。src/ ├── assets/ # 静态资源设备图标 SVGrouter.svg, firewall.svg、背景纹理 ├── components/ │ ├── TopologyCanvas.vue # 核心渲染容器挂载 D3 force simulation监听 zoom/pan 事件 │ ├── NodeItem.vue # 单节点组件接收 nodeData渲染图标标签状态环绿色/黄色/红色 │ └── LinkLine.vue # 连线组件根据 source/target 坐标绘制贝塞尔曲线绑定带宽 tooltip ├── stores/ │ ├── topologyStore.ts # Pinia store管理 nodes[]、links[]、selectedNode、zoomLevel 等响应式状态 │ └── dataService.ts # 数据服务封装 fetchTopologyData()、updateNodeStatus()、addLink() 等方法 ├── utils/ │ ├── forceLayout.ts # 力导向算法封装配置 charge、linkDistance、gravity 参数暴露 tick() 回调 │ └── svgHelper.ts # SVG 工具函数生成设备 icon path、计算连线控制点、处理文字换行 └── main.ts # 入口注册全局指令 v-tooltip初始化 WebSocket 连接这个结构的关键在于所有业务逻辑与 UI 渲染解耦。比如你要把“路由器节点”从圆形改成机架式图标只需替换assets/router-rack.svg并在NodeItem.vue中修改:icon-src绑定若要切换为自动布局非力导向只需重写utils/forceLayout.ts中的initSimulation()方法TopologyCanvas.vue完全不用动。2.2 数据模型nodes 与 links 的字段语义必须对齐监控系统拓扑图能否“活起来”第一道门槛是数据格式。源码里stores/topologyStore.ts定义的Node和Link类型不是随意写的而是直接映射真实网络对象// src/stores/topologyStore.ts export interface Node { id: string; // 必须唯一建议用设备 sysObjectID 或 hostname name: string; // 显示名称如 Core-SW-01 type: router | switch | firewall | server; // 决定图标和样式 ip: string; // 用于 SSH/API 调用非显示用 status: up | down | warning; // 影响状态环颜色 cpu?: number; // 0-100用于 tooltip 显示 memoryUsage?: number; // 同上 x?: number; // 初始 x 坐标力导向下可为空 y?: number; // 初始 y 坐标 } export interface Link { id: string; // 如 Core-SW-01:Gig1/0/1 - Access-SW-02:Gig1/0/24 source: string; // 源 node.id target: string; // 目标 node.id bandwidth: number; // 当前 Mbps用于连线粗细映射 utilization: number; // 0-100用于连线颜色绿→黄→红 portName?: string; // 源端口名显示在 tooltip 中 }注意id字段它必须与你的监控系统如 Prometheus 的node_exporterjob 实例、Zabbix 的 hostid保持一致。否则dataService.ts中的fetchTopologyData()返回的数据即使字段名对得上store.nodes.find(n n.id apiResponse.id)也会失败——这是新手最常翻车的第一步。2.3 渲染层核心D3-force 的三个关键参数如何影响拓扑“呼吸感”TopologyCanvas.vue内部的 D3 force simulation 不是黑匣子。它的三个基础参数直接决定拓扑图是“挤成一团”还是“散作满天星”参数名典型值作用说明修改建议forceManyBody().strength(-300)-100 ~ -500节点间斥力。值越负排斥越强节点越分散。设为 -50 会导致节点紧贴设为 -800 会让图疯狂抖动。生产环境建议 -200 ~ -350教学演示可设 -150 增强稳定性forceLink().distance(180)120 ~ 250连线“理想长度”。实际效果是有连线的节点会被拉近无连线的被推远。值过小100导致节点堆叠过大300使连线拉成直线失去层次感。优先匹配你网络的逻辑层级核心层设 200接入层设 150forceCenter( width/2, height/2 )固定中心坐标所有节点受向心力牵引。没有它图会整体漂移出视口。必须存在且 center 坐标需与 SVG 容器 width/height 动态绑定这些参数不是写死在代码里的魔法数字。我在某高校网络实验课项目中把它们做成运行时可调的调试面板!-- TopologyCanvas.vue 内 -- template div classdebug-panel label斥力强度: input v-model.numberforceParams.charge typerange min-500 max-50 //label label连线距离: input v-model.numberforceParams.linkDistance typerange min100 max300 //label /div /template script setup const forceParams ref({ charge: -250, linkDistance: 180 }); watch(forceParams, () { simulation.force(charge, d3.forceManyBody().strength(forceParams.value.charge)); simulation.force(link, d3.forceLink().distance(forceParams.value.linkDistance)); }); /script这样学生调试时拖动滑块就能直观理解“为什么我的核心交换机总被弹到右上角”——不是代码 bug是forceCenter坐标没随容器 resize 更新。3. 快速启动与数据对接三步注入你的真实网络数据源码下载解压后不能直接npm install npm run serve就完事。它默认加载的是public/mock-topology.json这个模拟数据。要让它显示你机房里的真实设备必须完成数据管道的“三步对接”准备数据源 → 改写数据获取逻辑 → 验证状态映射。下面以对接 Prometheus SNMP Exporter 的典型场景为例。3.1 准备数据源用 PromQL 构建拓扑元数据查询拓扑图需要两类数据静态元数据设备有哪些、怎么连和动态状态CPU、端口 up/down。Prometheus 本身不存拓扑关系需靠外部脚本生成。常见做法是用 Python 脚本定期扫描网络并写入 Prometheus 的pushgateway# generate_topology.py —— 每5分钟执行一次 import requests from pysnmp.hlapi import * def get_device_info(ip): # 通过 SNMP 获取 sysName, sysDescr, ifIndex/ifDescr errorIndication, errorStatus, errorIndex, varBinds next( getCmd(SnmpEngine(), CommunityData(public), UdpTransportTarget((ip, 161)), ContextData(), ObjectType(ObjectIdentity(SNMPv2-MIB, sysName, 0)), ObjectType(ObjectIdentity(IF-MIB, ifDescr, 1))) ) return {name: str(varBinds[0][1]), interfaces: [str(v[1]) for v in varBinds[1:]]} # 构建 nodes 列表简化版 nodes [] for ip in [192.168.1.1, 192.168.1.2, 192.168.1.3]: info get_device_info(ip) nodes.append({ id: ip, name: info[name], type: switch if Cisco in info[name] else router, ip: ip, status: up }) # 构建 links 列表需额外端口映射表 links [ {source: 192.168.1.1, target: 192.168.1.2, id: sw1-g1/0/1-sw2-g1/0/24}, {source: 192.168.1.2, target: 192.168.1.3, id: sw2-g1/0/1-sw3-g1/0/1} ] # 推送到 Pushgateway供前端 API 调用 requests.post(http://pushgateway:9091/metrics/job/topology, dataftopology_node_count {len(nodes)}\n \n.join([ftopology_link{{source{l[source]},target{l[target]}}} 1 for l in links]))这个脚本输出的不是 JSON而是 Prometheus 的文本格式。前端要读它需另起一个轻量 API 服务如 Express做转换// backend/api/topology.js app.get(/api/topology, (req, res) { // 从 Pushgateway 拉取原始指标 const raw await axios.get(http://pushgateway:9091/metrics); // 解析出 nodes 和 links 数组此处省略解析逻辑 const topologyData { nodes: [...], links: [...] }; res.json(topologyData); });3.2 改写数据获取逻辑替换 mock 数据为真实 API 调用打开src/stores/dataService.ts找到fetchTopologyData()方法。原始代码类似// src/stores/dataService.ts原始 export async function fetchTopologyData(): Promise{ nodes: Node[], links: Link[] } { const response await fetch(/mock-topology.json); // ← 这行必须改掉 return response.json(); }改为调用你自己的后端 API// src/stores/dataService.ts修改后 export async function fetchTopologyData(): Promise{ nodes: Node[], links: Link[] } { try { const response await fetch(/api/topology); // ← 指向你的 Express 服务 if (!response.ok) throw new Error(HTTP ${response.status}); const data await response.json(); // 关键校验确保返回字段符合 Node/Link 接口定义 data.nodes.forEach((n: any) { if (!n.id || !n.name || ![router,switch,firewall].includes(n.type)) { console.warn(Invalid node data:, n); } }); return data; } catch (err) { console.error(Failed to fetch topology data:, err); // 降级到 mock 数据避免白屏 const mock await fetch(/mock-topology.json).then(r r.json()); return mock; } }注意/api/topology是相对路径需在vue.config.js中配代理避免跨域// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:3000, // 你的 Express 服务地址 changeOrigin: true } } } };3.3 验证状态映射让“红色告警”真正反映设备宕机光有节点和连线不够拓扑图的灵魂在于状态联动。dataService.ts中通常还有pollStatus()方法它定时拉取设备状态并更新 store// src/stores/dataService.ts export function startStatusPolling() { const interval setInterval(async () { try { const statusRes await fetch(/api/device-status); // 返回 { 192.168.1.1: { cpu: 85, memory: 62, status: up } } const statusData await statusRes.json(); // 关键遍历 store.nodes按 id 匹配并更新 topologyStore.nodes.forEach(node { if (statusData[node.id]) { node.cpu statusData[node.id].cpu; node.memoryUsage statusData[node.id].memory; node.status statusData[node.id].status; // ← 这个字段驱动状态环颜色 } }); } catch (e) { console.error(Status polling failed:, e); } }, 10000); // 每10秒轮询一次 }验证是否生效打开浏览器开发者工具在 Console 中执行// 查看 store 中某个节点的状态 console.log(topologyStore.nodes.find(n n.id 192.168.1.1)); // 应看到 cpu/memory/status 字段已更新 // 再检查 DOM对应节点的 circle classstatus-ring 是否添加了 status-up / status-down 类如果状态没变90% 是node.id与 API 返回的 key 不一致比如 API 返回hostname而 store 用ip作为 id。此时必须统一标识符——这是血泪经验永远用设备唯一标识符如 sysObjectID做 id而不是 IP 或 hostname因为 IP 可能变hostname 可能重复。4. 避坑指南五个让拓扑图“动不起来”的高频问题拓扑图源码最大的陷阱不是功能缺失而是“看起来跑起来了但数据就是不联动、状态就是不刷新、点击就没反应”。以下是我在三个不同客户现场实测总结的五大避坑点每一条都对应真实翻车现场。4.1 现象节点全部挤在左上角拖拽后立刻弹回原因D3 force simulation 的forceCenter()坐标未随 SVG 容器尺寸变化而更新。常见于使用resizeObserver但未正确绑定或容器初始width/height为 0。解决在TopologyCanvas.vue的onMounted中先获取容器尺寸再初始化 simulation并在onBeforeUnmount中清理 resize listener。关键代码const container refHTMLElement | null(null); let resizeObserver: ResizeObserver | null null; onMounted(() { if (container.value) { const rect container.value.getBoundingClientRect(); simulation.force(center, d3.forceCenter(rect.width / 2, rect.height / 2)); resizeObserver new ResizeObserver(entries { for (let entry of entries) { const rect entry.contentRect; simulation.force(center, d3.forceCenter(rect.width / 2, rect.height / 2)); } }); resizeObserver.observe(container.value); } }); onBeforeUnmount(() { if (resizeObserver) resizeObserver.disconnect(); });4.2 现象点击节点无反应控制台报Cannot read property dispatch of undefined原因NodeItem.vue中绑定了clickhandleClick但handleClick方法试图调用this.$emit(node-click, node)而该组件未被父组件正确监听。更常见的是TopologyCanvas.vue未在NodeItem上声明v-on$listeners导致事件无法冒泡。解决Vue 3 中应使用defineEmits显式声明并在父组件中监听!-- TopologyCanvas.vue -- NodeItem v-fornode in nodes :keynode.id :node-datanode node-clickonNodeClick !-- 显式监听 -- /!-- NodeItem.vue -- script setup const emit defineEmits([node-click]); // 显式声明 const props defineProps([nodeData]); const handleClick () { emit(node-click, props.nodeData); // 触发事件 }; /script4.3 现象连线显示为直线且不随节点移动而重绘原因LinkLine.vue中d属性绑定的路径字符串未设置为响应式。常见错误写法:dgetLinkPath(link)而getLinkPath()是一个普通函数不依赖 reactive 数据。解决将路径计算逻辑移到computed确保依赖link.source.x、link.target.y等响应式字段script setup const props defineProps([link]); const sourceNode computed(() topologyStore.nodes.find(n n.id props.link.source)); const targetNode computed(() topologyStore.nodes.find(n n.id props.link.target)); const linkPath computed(() { if (!sourceNode.value || !targetNode.value) return M0,0L0,0; const sx sourceNode.value.x || 0; const sy sourceNode.value.y || 0; const tx targetNode.value.x || 0; const ty targetNode.value.y || 0; // 贝塞尔曲线控制点取中点偏移 const cx (sx tx) / 2; const cy (sy ty) / 2 - 50; return M${sx},${sy} Q${cx},${cy} ${tx},${ty}; }); /script template path :dlinkPath stroke#999 stroke-width2 fillnone/ /template4.4 现象WebSocket 连接成功但设备状态不更新控制台无报错原因后端推送的 JSON 消息体格式与前端onmessage处理逻辑不匹配。例如后端发{ type: node_update, data: { id: sw1, cpu: 92 } }但前端只写了event.data未做 JSON.parse或解析后未按type分发。解决在main.ts或dataService.ts中建立健壮的消息分发器// src/stores/dataService.ts const ws new WebSocket(ws://localhost:8080/topology); ws.onmessage (event) { try { const msg JSON.parse(event.data); switch (msg.type) { case node_update: updateNodeStatus(msg.data); // 自定义更新函数 break; case link_utilization: updateLinkUtilization(msg.data); break; default: console.warn(Unknown message type:, msg.type); } } catch (e) { console.error(Failed to parse websocket message:, event.data); } };4.5 现象部署到 Nginx 后拓扑图空白Network 面板显示 404 加载/mock-topology.json原因Vue CLI 默认打包为dist/目录但 Nginx 配置未正确处理前端路由history 模式和静态资源路径。/mock-topology.json404 说明public/目录下的文件未被 Nginx 服务。解决检查 Nginx 配置确保location /块包含try_files $uri $uri/ /index.html;且public/下的文件放在dist/同级或正确复制到dist/# nginx.conf location / { root /var/www/my-topology/dist; try_files $uri $uri/ /index.html; # ← 关键让前端路由 fallback 到 index.html } # 确保 dist 目录下有 mock-topology.json构建时需手动 cp 或用 copy-webpack-plugin5. 进阶技巧给拓扑图加上“时间旅行”能力——回溯任意时刻的网络状态拓扑图的价值不仅在于“此刻”更在于“刚才”。当告警发生时运维人员第一反应不是看当前图而是问“5分钟前这根线是不是就红了”、“这台设备是什么时候掉线的”。源码本身不带历史回溯但利用其数据驱动架构只需增加一个时间选择器 历史数据 API就能低成本实现“时间旅行”功能。我在某运营商省级网管项目中落地了该方案效果远超预期。5.1 架构设计三层时间数据支撑要支持回溯数据层必须提供三种时间粒度的数据时间粒度数据来源更新频率用途实时秒级WebSocket 流每 5 秒驱动当前拓扑状态环、连线颜色近期分钟级Prometheus 查询每 1 分钟生成最近 1 小时的状态热力图历史小时级TimescaleDB 归档每 1 小时支持选择任意过去 7 天的整点时刻关键不在存储而在前端如何无缝切换数据源。我们不修改fetchTopologyData()的签名而是通过一个timeContext状态控制// src/stores/topologyStore.ts export const topologyStore defineStore(topology, { state: () ({ timeContext: realtime as realtime | recent | historical, historicalTime: new Date(), // 仅当 timeContext historical 时有效 // ...其他 state }), getters: { isHistoricalMode: (state) state.timeContext historical }, actions: { setTimeContext(context: realtime | recent | historical, time?: Date) { this.timeContext context; if (time) this.historicalTime time; // 触发数据重载 this.reloadTopologyData(); } } });5.2 时间选择器组件精确到小时的简易日历我们不需要 FullCalendar 那么重一个轻量级的“小时选择器”即可。核心是生成过去 7 天的整点时间数组并高亮当前选中项!-- components/TimeSelector.vue -- template div classtime-selector div classtime-label回溯时间/div div classtime-options button v-fort in timeOptions :keyt.timestamp clickselectTime(t) :class{ active: t.timestamp selectedTime } {{ t.label }} /button /div /div /template script setup import { ref, computed, watch } from vue; import { useTopologyStore } from /stores/topologyStore; const topologyStore useTopologyStore(); const selectedTime refnumber(Date.now()); // 生成过去 7 天的整点时间每小时一个 const timeOptions computed(() { const now new Date(); const options []; for (let i 0; i 7 * 24; i) { const t new Date(now.getTime() - i * 60 * 60 * 1000); // 只保留整点setMinutes/Seconds/Milliseconds 为 0 t.setMinutes(0); t.setSeconds(0); t.setMilliseconds(0); options.push({ timestamp: t.getTime(), label: ${t.getMonth()1}/${t.getDate()} ${t.getHours()}:00 }); } return options.slice(0, 48); // 默认显示最近 48 小时 }); const selectTime (timeObj: { timestamp: number }) { selectedTime.value timeObj.timestamp; topologyStore.setTimeContext(historical, new Date(timeObj.timestamp)); }; // 监听 store 变化同步 UI watch(() topologyStore.historicalTime, (newVal) { if (newVal) selectedTime.value newVal.getTime(); }); /script5.3 历史数据加载复用现有 store 结构仅替换 API 端点fetchTopologyData()方法内部根据timeContext切换请求 URL// src/stores/dataService.ts export async function fetchTopologyData(): Promise{ nodes: Node[], links: Link[] } { let url ; if (topologyStore.timeContext realtime) { url /api/topology; } else if (topologyStore.timeContext recent) { url /api/topology/recent?hours1; } else if (topologyStore.timeContext historical) { const ts Math.floor(topologyStore.historicalTime.getTime() / 1000); url /api/topology/historical?timestamp${ts}; } try { const response await fetch(url); if (!response.ok) throw new Error(HTTP ${response.status}); return response.json(); } catch (err) { console.error(Failed to fetch topology data:, err); // 降级策略历史失败则切回实时 if (topologyStore.timeContext ! realtime) { topologyStore.setTimeContext(realtime); return fetchTopologyData(); // 递归重试实时数据 } throw err; } }后端/api/topology/historical接口只需接收timestamp参数从 TimescaleDB 查询该时刻前后 5 分钟的平均状态值即可。无需改动前端任何渲染逻辑——NodeItem.vue依然用node.status控制颜色LinkLine.vue依然用link.utilization控制粗细只是数据源变了。5.4 验证与技巧用“双图对比”快速定位异常起点最实用的技巧不是单图回溯而是双图并排对比。我们在TopologyCanvas.vue顶部加了一个“对比模式”开关template div classtopology-header TimeSelector / button clicktoggleCompareMode↔ 双图对比/button /div div classtopology-container div classtopology-main TopologyCanvas :datacurrentData / /div div v-ifisCompareMode classtopology-compare div classcompare-header对比时刻TimeDisplay :timecompareTime //div TopologyCanvas :datacompareData / /div /div /templatecompareData通过另一个独立的fetchTopologyData()调用获取时间戳比主图早 10 分钟。当发现某条连线在主图是红色utilization 90%而在对比图中是绿色说明这是突发流量如果两图都是红色则可能是配置错误或持续拥塞。这种视觉对比比看 20 行 Prometheus 图表快得多。从那以后我每次上线新版本拓扑图都强制走一遍“时间旅行”测试选一个已知故障时刻确认红色节点和断连线能准确复现再选一个正常时刻确认全图绿色。这成了我的上线 checklist 第一条。希望帮到你。本文还有配套的精品资源点击获取