Wireshark 自定义协议解析器与多字段时序图 - 设计文档
1. 项目概述
1.1 目标
创建一个完整的 Wireshark 自定义协议解析器,支持:
- 自定义二进制协议解析(Lua)
- I/O Graph 聚合绘图(AVG+小间隔)
- 多字段独立坐标系时序图(Lua Tap + Chart.js HTML)
- 按需数据提取(紧凑列式存储,JS 端按需生成图表数据)
1.2 技术栈
| 组件 | 版本 | 用途 |
|---|---|---|
| Wireshark | 3.4.x / 4.6.x | 协议分析平台 |
| Lua | 5.2 / 5.3 | 解析器和绘图插件 |
| GCC (MinGW64) | 8.1.0+ | pcap 生成器编译 |
| Chart.js | 4.x | 前端图表渲染 |
2. 架构设计
2.1 系统架构
┌──────────────────────────────────────────────────────────┐
│ 用户入口 │
├──────────┬──────────────┬───────────────────────────────┤
│ Wireshark│ I/O Graph │ Tools > MYPROTO > │
│ GUI │ (聚合绘图) │ 逐包原始值绘图 │
├──────────┴──────────────┴───────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ HTML5 + Chart.js 页面 │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ (最多5个独立坐标系) │ │
│ │ │图表1 │ │图表2 │ │图表3 │ 每个有独立Y轴 │ │
│ │ │下拉选│ │下拉选│ │下拉选│ 共享X轴(时间对齐) │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ │ + 十字线贯穿所有图表 + 信息面板 + 缩放/平移 │ │
│ └──────────────────────────────────────────────────┘ │
│ ▲ │
│ pktTimes + fieldData │
│ (紧凑列式数据,按需生成图表) │
│ ▲ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ myproto_plotter.lua │ │
│ │ Listener.new() → tap.packet() → 紧凑列式序列化 │ │
│ └──────────────────────────────────────────────────┘ │
│ ▲ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ myproto_dissector.lua │ │
│ │ Proto + ProtoField → dissector() → 字段注册 │ │
│ └──────────────────────────────────────────────────┘ │
│ ▲ │
│ UDP Port 5555 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ sensor_data.pcap (gen_pcap.exe 生成) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
2.2 文件清单
| 文件 | 说明 |
|---|---|
myproto_dissector.lua |
协议解析器(11个字段,注册到 UDP 5555) |
myproto_plotter.lua |
多字段时序图插件(Listener + HTML 生成) |
gen_pcap.c |
C 程序,生成测试 pcap |
gen_plot.py |
Python 辅助脚本(tshark 输出转 HTML) |
build.bat |
一键编译+生成 pcap |
plot.bat |
一键提取+生成 HTML 图表 |
3. 协议定义
3.1 MYPROTO 协议结构(22字节)
| 偏移 | 字段 | 类型 | 字节 | I/O Graph | 绘图字段 |
|---|---|---|---|---|---|
| 0 | magic | uint16 | 2 | ❌ | ❌ |
| 2 | version | uint8 | 1 | ❌ | ❌ |
| 3 | msg_type | uint8 | 1 | ❌ | ✅ MsgType |
| 4 | sequence | uint32 | 4 | ✅ | ✅ Sequence |
| 8 | timestamp_ms | uint32 | 4 | ✅ | ✅ Timestamp(ms) |
| 12 | temperature | int16 | 2 | ✅ | ✅ Temp© |
| 14 | humidity | uint16 | 2 | ✅ | ✅ Humidity(%) |
| 16 | pressure | uint16 | 2 | ✅ | ✅ Pressure(hPa) |
| 18 | voltage | int16 | 2 | ✅ | ✅ Voltage(V) |
| 20 | status | uint8 | 1 | ❌ | ✅ Status |
| 21 | reserved | uint8 | 1 | ❌ | ❌ |
I/O Graph 可绘制字段 :必须是数值类型(uint*/int*/float/double)
绘图插件字段:8个(含非 I/O Graph 字段如 MsgType、Status)
4. 解析器设计
4.1 字段注册规则
lua
-- ✅ 数值字段 - I/O Graph 和绘图插件均可用
ProtoField.uint8 / .uint16 / .uint24 / .uint32
ProtoField.int8 / .int16 / .int24 / .int32
ProtoField.float / .double
-- ❌ 非数值字段 - 两种绘图均不可用
ProtoField.string / .bytes / .bool
4.2 Wireshark 4.x Lua API 变更速查
| 3.4.x 语法 | 4.6.x 状态 | 4.6.x 替代方案 |
|---|---|---|
Listener.new("proto") |
❌ Tap not found | Listener.new() 无参 |
Field.new() in callback |
❌ Error | 必须模块顶层调用 |
pinfo.relative_time |
❌ No such method | Field.new("frame.time_relative") |
pinfo.cols.protocol == "X" |
❌ userdata | #{ field_f() } > 0 |
tap:reset() |
❌ No such method | 自定义 reset_data() |
| NSTime 直接运算 | ❌ type error | tonumber(tostring(value)) |
string.gsub(s, "%", d) |
❌ invalid % | gsub(pat, function() return d end) |
string.format("...%)..") |
❌ invalid conversion | %% 转义 |
5. 多字段时序图设计
5.1 需求与方案映射
| 需求 | 方案 |
|---|---|
| 最多5个字段独立坐标系,时序对齐 | 5个 Chart.js 实例,共享 pktTimes 数组 |
| 下拉列表选择字段,默认1个,添加按钮 | addChart()/removeChart() 动态管理 |
| 鼠标虚线贯穿所有图表 + 交点坐标 | crosshairPlugin + findNearestIdx() 二分查找 |
| 放大/缩小/还原 | 按钮控制 + 滚轮缩放 + 拖拽平移,X轴联动 |
| 数据按需提取 | 紧凑列式存储,JS端 makeChartData() 按需生成 |
| 中文界面 | 按钮/下拉/提示全部中文 |
5.2 数据格式设计
核心设计原则:数据按需组织,不事先生成
紧凑列式存储(嵌入 HTML)
javascript
// 共享时间数组(仅一份)
var pktTimes = [0.000, 0.998, 1.980, 2.992, ...];
// 每个字段一个值数组(无重复时间)
var fieldData = {
"Temp(C)": [25.00, 25.79, 26.58, ...],
"Humidity(%)": [66.82, 67.54, 68.17, ...],
"Pressure(hPa)":[103.11, 103.09, 103.06, ...],
"Voltage(V)": [3.395, 3.421, 3.443, ...],
"Sequence": [0, 1, 2, ...],
"Timestamp(ms)":[783080489, 783081571, ...],
"MsgType": [0, 1, 2, ...],
"Status": [0, 1, 2, ...]
};
对比:
| 指标 | 旧方案(per-field times) | 新方案(列式共享) |
|---|---|---|
| 时间存储 | 每字段重复一份 | 共享一份 |
| HTML大小(200包8字段) | ~42KB | ~27KB (-36%) |
| 图表数据 | 嵌入时全部预生成 | JS按需 makeChartData() |
按需图表数据生成
javascript
function makeChartData(fieldName) {
if (!fieldName || !fieldData[fieldName]) return [];
var vals = fieldData[fieldName];
var data = [];
for (var i = 0; i < pktTimes.length; i++) {
data.push({x: pktTimes[i], y: vals[i]});
}
return data;
}
仅在用户从下拉框选择字段时调用,将列式数据转换为 Chart.js scatter 格式。
5.3 Lua 数据序列化
lua
local pkt_times = ser(times) -- 共享时间数组
local field_data = string.format(
'{"Temp(C)":%s,"Humidity(%%)":%s,"Pressure(hPa)":%s,"Voltage(V)":%s,' ..
'"Sequence":%s,"Timestamp(ms)":%s,"MsgType":%s,"Status":%s}',
ser(temp_vals), ser(hum_vals), ser(press_vals), ser(volt_vals),
ser_int(seq_vals), ser_int(ts_vals), ser_int(msg_vals), ser_int(stat_vals)
)
-- 注:Humidity(%%) 中 %% 是 string.format 转义,输出为 Humidity(%)
-- 注:整数用 ser_int() 避免小数点
5.4 HTML 模板注入
lua
local html = plot_html_template
html = html:gsub("__PKT_TIMES__", function() return pkt_times end)
html = html:gsub("__FIELD_DATA__", function() return field_data end)
使用 function() return data end 包裹 gsub 替换值,避免值中含 % 导致报错。
6. 交互设计
6.1 图表管理
初始状态:1个图表(Temp(C))
┌─────────────────────────────┐
│ [选择字段 ▼ Temp(C)] [✕] │
│ ─────────────────────────── │
│ ~~~波形~~~ │
└─────────────────────────────┘
点击"添加图表"后:2个图表
┌─────────────────────────────┐
│ [选择字段 ▼ Temp(C)] [✕] │
│ ~~~波形~~~ │
├─────────────────────────────┤
│ [选择字段 ▼ Humidity(%)] [✕] │
│ ~~~波形~~~ │
└─────────────────────────────┘
最多5个图表,添加按钮自动禁用
仅1个图表时,删除按钮隐藏
6.2 十字线交互
鼠标移动 → findNearestIdx(timeVal) 二分查找 → crosshairIdx
→ 所有图表 afterDraw 画虚线 + 交点圆点 + 数值标签
→ infoPanel 显示已选字段的值(不是全部字段)
┌──────────────────────────────────────────┐
│ t = 5.980s | Temp(C): 26.58 | Humidity(%): 68.17 │
└──────────────────────────────────────────┘
关键优化:
findNearestIdx()使用二分查找(O(log n)),而非线性扫描crosshairIdx基于索引而非时间值,infoPanel 直接fieldData[name][idx]取值scheduleRedraw()通过requestAnimationFrame节流,每帧最多重绘一次- 仅
crosshairIdx变化时才触发重绘
6.3 缩放/平移
| 操作 | 实现 |
|---|---|
| 按钮:放大/缩小/还原 | doZoom(factor) / doReset() |
| 鼠标滚轮缩放 | 以鼠标位置为中心,1.2x 倍率 |
| 拖拽平移 | mousedown/mousemove/mouseup + applyXRange() |
| X轴联动 | 任何图表缩放/平移后,applyXRange() 同步所有图表 |
关键细节:
mousedown检查e.target.tagName,跳过 SELECT/OPTION/BUTTON,避免拦截下拉框wheel事件{passive: false}+e.preventDefault()阻止页面滚动- 拖拽期间
isPanning标志阻止十字线更新
6.4 下拉框不拦截修复
问题 :canvas getBoundingClientRect() 覆盖整个图表行(含下拉框区域),mousedown 事件被拦截为拖拽。
修复:
javascript
cc.addEventListener('mousedown', function(e) {
if (e.button !== 0) return;
var tag = e.target.tagName;
if (tag === 'SELECT' || tag === 'OPTION' || tag === 'BUTTON') return;
// ... 开始拖拽
});
6.5 infoPanel 仅显示已选字段
问题:默认显示全部8个字段值,信息过多。
修复:遍历已添加图表的 dropdown,只显示选中字段:
javascript
function updateInfoPanel() {
var shown = {};
for (var i = 0; i < chartIds.length; i++) {
var fn = document.getElementById('sel_' + chartIds[i]).value;
if (fn && fieldData[fn] && !shown[fn]) {
shown[fn] = true;
html += fn + ': ' + fieldData[fn][crosshairIdx].toFixed(2);
}
}
}
7. Lua Tap 插件设计
7.1 架构
用户点击 "Retap & Plot"
│
▼
reset_data() 清空历史数据
│
▼
retap_packets() 重新扫描所有包
│
▼
tap.packet() 逐包回调
├─ temp_f() 提取温度 ─→ temp_vals
├─ hum_f() 提取湿度 ─→ hum_vals
├─ press_f()提取气压 ─→ press_vals
├─ volt_f() 提取电压 ─→ volt_vals
├─ seq_f() 提取序列号 ─→ seq_vals
├─ ts_f() 提取时间戳 ─→ ts_vals
├─ msg_f() 提取消息类型→ msg_vals
└─ stat_f() 提取状态 ─→ stat_vals
│
▼
do_plot() 生成 HTML
├─ ser() 序列化浮点数组
├─ ser_int() 序列化整数数组
├─ string.format() 组装 fieldData
├─ gsub(fn) 安全替换到模板
├─ io.open() 写入 HTML
└─ os.execute() 打开浏览器
7.2 Field.new() 规则
lua
-- ✅ 必须在模块顶层调用
local temp_f = Field.new("myproto.temperature")
local time_f = Field.new("frame.time_relative")
-- ❌ 不能在菜单回调或 dissector 内调用
win:add_button("Plot", function()
local f = Field.new("xxx") -- ERROR!
end)
7.3 协议判断
lua
function tap.packet(pinfo, tvb)
-- ❌ 4.x 中 pinfo.cols.protocol 是 userdata,不能字符串比较
-- if pinfo.cols.protocol == "MYPROTO" then ... end
-- ✅ 用 Field 提取判断
local temp_info = { temp_f() }
if #temp_info == 0 then return end -- 非myproto包跳过
end
7.4 NSTime 转换
lua
-- ❌ 4.x 中 frame.time_relative 返回 NSTime,不能直接运算
-- local t = time_info[1].value -- type error
-- ✅ 转换为 number
local t = tonumber(tostring(time_info[1].value))
7.5 string.format 中的 % 转义
lua
-- ❌ "Humidity(%)" 中的 % 被解释为格式转义
-- string.format("... Humidity(%) ...", ...) -- invalid conversion '%)
-- ✅ %% 转义,输出为 %
string.format('..."Humidity(%%)"...', ...)
7.6 gsub 安全替换
lua
-- ❌ 如果 series_data 含 %,gsub 会报错
-- html:gsub("__DATA__", series_data)
-- ✅ 用 function 包裹
html:gsub("__PKT_TIMES__", function() return pkt_times end)
html:gsub("__FIELD_DATA__", function() return field_data end)
8. 已修复的全部问题
| # | 错误 | 修复方案 | 文件 |
|---|---|---|---|
| 1 | Tap myproto not found |
Listener.new() 无参 |
plotter |
| 2 | Field_new before Taps |
Field.new() 移到模块顶层 |
plotter |
| 3 | No such method 'reset' |
自定义 reset_data() |
plotter |
| 4 | No MYPROTO data found |
Field 提取判断非 pinfo.cols.protocol |
plotter |
| 5 | No such method 'relative_time' |
Field.new("frame.time_relative") |
plotter |
| 6 | number expected, got NSTime |
tonumber(tostring(value)) |
plotter |
| 7 | invalid use of '%' in replacement |
gsub(pat, function() return data end) |
plotter |
| 8 | invalid conversion '%)' to 'format' |
Humidity(%%) 转义 |
plotter |
| 9 | 下拉框不能下拉/选择 | mousedown 检查 e.target.tagName |
plotter + gen_plot |
| 10 | infoPanel 显示全部字段 | 只遍历已选图表的 dropdown | plotter + gen_plot |
| 11 | 英文界面 | 按钮/下拉/提示改中文 | plotter + gen_plot |
9. 前端 JS 架构
9.1 模块划分
├── 数据层
│ ├── pktTimes[] 共享时间数组
│ ├── fieldData{} 列式字段数据
│ └── makeChartData(fn) 按需生成 Chart.js scatter 数据
│
├── 图表管理
│ ├── addChart(field) 添加图表(默认1个,最多5个)
│ ├── removeChart(id) 删除图表(至少保留1个)
│ ├── changeField(id) 切换字段(销毁+重建 Chart 实例)
│ └── updateAddBtn() 更新按钮状态(禁用/显示删除)
│
├── 十字线
│ ├── crosshairPlugin Chart.js afterDraw 插件
│ ├── findNearestIdx(t) 二分查找最近包索引
│ └── updateInfoPanel() 仅显示已选字段值
│
├── 缩放/平移
│ ├── doZoom(factor) 按钮缩放
│ ├── doReset() 还原
│ ├── applyXRange(mn,mx) X轴联动
│ └── 事件:wheel/mousedown/mousemove/mouseup
│
└── 性能优化
├── scheduleRedraw() requestAnimationFrame 节流
├── crosshairIdx 索引复用(非时间值查找)
└── 'none' animation Chart.js 跳过动画
9.2 关键函数签名
javascript
makeChartData(fieldName) → [{x: time, y: value}, ...]
addChart(fieldSel) → void (创建 DOM + Chart 实例)
removeChart(id) → void (销毁 Chart + 移除 DOM)
findNearestIdx(timeVal) → number (二分查找,O(log n))
applyXRange(min, max) → void (所有图表联动)
scheduleRedraw() → void (RAF 节流)
10. 数据生成器设计
10.1 gen_pcap.c 要点
- 网络字节序 :
put16()/put32()大端写入 - IP 校验和 :
ip_checksum()计算 - 传感器模拟:正弦波 + 随机抖动
10.2 数据范围
| 字段 | 最小值 | 最大值 | 单位换算 |
|---|---|---|---|
| 温度 | 17.00°C | 32.99°C | 值/100 |
| 湿度 | 30.00% | 70.00% | 值/100 |
| 气压 | 99.30 hPa | 103.29 hPa | 值/100 |
| 电压 | 3.100V | 3.499V | 值/1000 |
11. 构建与部署
11.1 编译
bash
gcc -O2 -o gen_pcap.exe gen_pcap.c -lm
gen_pcap.exe sensor_data.pcap 200
11.2 安装插件
bash
copy myproto_dissector.lua %APPDATA%\Wireshark\plugins\
copy myproto_plotter.lua %APPDATA%\Wireshark\plugins\
11.3 版本兼容性
| Wireshark | Lua | 解析器 | I/O Graph | Lua Tap | io,stat |
|---|---|---|---|---|---|
| 3.4.x | 5.2 | ✅ | ✅ AVG | ⚠️ 部分API | ⚠️ 返回0 |
| 4.6.x | 5.3 | ✅ | ✅ AVG | ✅ 全部修复 | ✅ 正确语法 |
文档版本 : 3.0
更新日期 : 2026-05-17
兼容版本: Wireshark 3.4.x / 4.6.x