Packet Tracer是网络教学中常用的模拟器,它能够记录设备之间的ICMP、ARP等流量。通常我们只能在界面里查看事件列表,或者逐个设备查看路由表。但如果要批量分析多个拓扑、自动生成实验报告,就需要把模拟数据提取出来。本文将用Node.js实现一个Mock2Image工具,将包追踪结果转换成图像。

一、理解Packet Tracer的包追踪输出格式
Packet Tracer的仿真模式(Simulation Mode)下,用户发送一个ICMP包时,模拟器会生成PDU事件列表。这些事件记录了数据包从源设备到目标设备经过的每一个接口,以及每一跳的OSI模型封装信息。如果只做简单分析,可以手动查看事件列表,但一旦涉及多个拓扑或者需要自动化处理,手动操作效率太低。比较实用的做法是通过File菜单导出事件日志,或者直接保存工程文件后用外部工具解析。工程文件.pkt是二进制格式,不适合直接用Node.js读取,因此推荐先在Packet Tracer里把仿真事件导出为XML或文本日志。
导出的XML通常包含packets根节点,每个packet节点内有source、destination以及若干个hop子节点。下面是一段简化后的XML示例:
<packets>
<packet>
<source>PC0</source>
<destination>Server0</destination>
<time>1.024</time>
<hop>
<device>Router0</device>
<interface>GigabitEthernet0/0</interface>
</hop>
<hop>
<device>Switch1</device>
<interface>FastEthernet0/2</interface>
</hop>
</packet>
</packets>
解析时需要注意,部分Packet Tracer版本会在XML文件开头加上BOM头,直接用fs.readFileSync读取后交给xml2js可能会出现首标签解析失败,所以读取后最好先做一次stripBOM处理。另外,hop节点里的interface字段有时会带有前后空格,解析后需要trim。时间字段可能被记录为字符串,排序时要转成数字,否则会出现1.10排在1.9前面的问题。
拿到XML之后,就可以用Node.js的xml2js库把文件转成JSON对象。代码结构很直接:读取文件、调用parseStringPromise、遍历packets数组,把每一条记录映射成包含src、dst、hops的路径对象。hops数组里每个元素保留device和interface两个字段,后续构建拓扑图时只需要用到device,interface可以用于做更细粒度的路径标注。
二、用Node.js解析并构建包转发路径
解析阶段的目标是把原始日志转换成结构化的路径数据。这里使用Node.js内置的fs模块读取文件,再用xml2js的parseStringPromise处理异步解析。为了避免回调嵌套,可以把整个流程写在一个async函数里。下面这段代码完成了从XML文件到路径数组的转换:
const fs = require('fs');
const { parseStringPromise } = require('xml2js');
async function loadTrace(filePath) {
const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
const result = await parseStringPromise(raw);
const packets = result.packets.packet || [];
return packets.map(p => ({
src: p.source[0].trim(),
dst: p.destination[0].trim(),
time: Number(p.time[0]),
hops: (p.hop || []).map(h => ({
device: h.device[0].trim(),
intf: h.interface[0].trim()
}))
}));
}
构建图结构时,用Map存储邻接表比用对象更合适,因为设备名称可能包含空格或特殊字符,Map的键不会和原型链属性冲突。每个节点作为键,对应的值是一个数组,保存从该节点出发可以到达的下一跳设备。遍历所有trace时,把src和每个hop.device依次连接起来,最后再把最后一个hop.device连接到dst。这样即使多个数据包的路径有分叉或合并,图也能完整表达转发关系。
function buildGraph(traces) {
const graph = new Map();
for (const trace of traces) {
let prev = trace.src;
for (const hop of trace.hops) {
if (!graph.has(prev)) graph.set(prev, []);
graph.get(prev).push(hop.device);
prev = hop.device;
}
if (!graph.has(prev)) graph.set(prev, []);
graph.get(prev).push(trace.dst);
}
return graph;
}
实际使用时,经常会遇到同一对节点之间有多条重复边的情况,比如同一个数据包被重复记录,或者多次追踪产生的路径完全一致。可以在buildGraph之后加一个去重步骤:把每个节点的邻接数组用Set去重,再转回数组。如果需要保留重复次数作为边权重,也可以改为在数组中保留重复项,在渲染时根据数组长度决定线条粗细。
三、生成追踪图像:从SVG到PNG的Mock2Image实现
把模拟数据映射成图像是Mock2Image的核心环节。在Node.js环境中,生成SVG是最轻量的方案,因为它不需要任何原生依赖,直接拼接字符串就能得到可用的矢量图。SVG文件可以被浏览器直接打开,也可以交给sharp或resvg-js等库转换成PNG。对于简单的网络拓扑,使用垂直布局就能满足需求:把所有节点按出现顺序排成一列,x坐标固定,y坐标按索引递增,这样画出来的路径图清晰且不会重叠。
function renderSvg(graph) {
const nodes = [...new Set([...graph.keys(), ...graph.values().flat()])];
const width = 800;
const height = nodes.length * 80 + 40;
const pos = {};
nodes.forEach((node, i) => {
pos[node] = { x: 160, y: 40 + i * 80 };
});
let svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">`;
for (const [from, tos] of graph) {
for (const to of tos) {
svg += `<line x1="${pos[from].x}" y1="${pos[from].y}" x2="${pos[to].x}" y2="${pos[to].y}" stroke="#333" stroke-width="2"/>`;
}
}
for (const node of nodes) {
svg += `<circle cx="${pos[node].x}" cy="${pos[node].y}" r="18" fill="#4a90d9"/>`;
svg += `<text x="${pos[node].x}" y="${pos[node].y + 38}" text-anchor="middle" font-size="12">${node}</text>`;
}
svg += `</svg>`;
return svg;
}
上面的代码返回一段完整的SVG字符串。注意拼接时所有的<和>都要写成HTML实体,这样才能在字符串中正常表示标签。生成后的SVG可以直接用fs.writeFileSync存成.svg文件,也可以通过resvg-js渲染成PNG,方便插入实验报告。如果节点名称过长,可以把text的font-size调小,或者把x坐标按节点名称长度做偏移,避免文本溢出画布。
对于更复杂的拓扑,垂直布局会显得很松散。这时可以引入简单的力导向布局算法,或者使用现成的graphviz库生成dot文件再渲染。不过对于教学实验中的包追踪场景,节点数量通常不超过十个,手工计算坐标已经足够,而且代码更容易理解和维护。
四、处理乱序包与接口命名不一致的问题
真实导出的日志中,数据包并不总是按时间顺序排列。如果直接按文件中的顺序构建路径,可能会出现路径方向错误或者重复边。解决方法是先对所有traces按time字段排序,确保同一次追踪内的hop顺序正确。下面这个normalizeHop函数可以同时处理接口名称中的空格,并把常见的简写统一成完整形式,例如把Fa0/1转换为FastEthernet0/1。
function normalizeIntf(name) {
const map = {
'Fa': 'FastEthernet',
'Gi': 'GigabitEthernet',
'Se': 'Serial'
};
return name.replace(/^(Fa|Gi|Se)(?=\d)/, (m) => map[m] || m)
.replace(/\s+/g, '');
}
function normalizeHop(hop) {
return {
device: hop.device.trim(),
intf: normalizeIntf(hop.intf)
};
}
另一个常见问题是Packet Tracer中的设备名称可能带有尾随空格,或者同一设备在不同导出文件中大小写不一致。解析时统一做trim和小写转换,可以避免图中出现两个几乎同名却无法合并的节点。对于重复包,如果时间戳完全相同且路径一致,可以只保留一条,减少图结构中的冗余边。
把上述模块串联起来后,整个工具的执行流程就是:读取XML日志、解析并排序、构建图结构、规范化接口名称、生成SVG或PNG。这个流程可以封装成一个命令行工具,接收输入文件和输出路径参数,方便在多个实验拓扑中复用。后续如果需要对比不同配置下的转发路径,只需分别生成图像,再用图像差异工具或直接对比图结构即可。相比手动截图Packet Tracer界面,自动化生成的方式明显更适合批量分析和版本记录。

