基于 Rust + Tauri 的桌面 MQTT 调试客户端
1. 概述
AtomMQTT Client 是一个跨平台桌面 MQTT 调试工具,使用 Tauri v1 框架构建。其核心架构为:
┌─────────────────────────────────────┐
│ Web 前端 │
│ HTML + CSS (Tokyo Night) + JS │
│ ───────── Tauri IPC ──────────▶ │
├─────────────────────────────────────┤
│ Rust 后端 │
│ rumqttc MQTT 客户端 │
│ Tokio 异步事件循环 │
│ Tauri 命令处理层 │
└─────────────────────────────────────┘
技术栈:
| 层次 | 技术 | 版本 |
|---|---|---|
| 桌面框架 | Tauri | 1.6 |
| 后端语言 | Rust | 2021 edition |
| MQTT 协议 | rumqttc | 0.24 |
| 异步运行时 | Tokio | 1.36 (full) |
| 序列化 | serde + serde_json | 1.0 |
| 前端 | 原生 HTML/CSS/JS | --- |
项目开源地址:https://gitcode.com/qq8864/atomMqtt

2. Rust 后端实现
2.1 数据模型
MqttMessage --- 消息结构
rust
#[derive(Debug, Clone, Serialize)]
pub struct MqttMessage {
pub topic: String, // 消息主题
pub payload: String, // UTF-8 载荷文本
pub payload_hex: String, // Hex 编码载荷(用于二进制数据查看)
pub qos: u8, // 服务质量 (0/1/2)
pub retain: bool, // 保留消息标志
pub timestamp: String, // 接收时间(毫秒精度)
pub packet_id: Option<u16>, // MQTT 包 ID
}
每条接收到的消息会同时保存 UTF-8 文本 和 Hex 编码两种形式的载荷,前端可在 UI 中切换显示。
ConnectionStatus --- 连接状态
rust
pub struct ConnectionStatus {
pub connected: bool,
pub host: String,
pub port: u16,
pub client_id: String,
pub connected_at: Option<String>,
}
SubscriptionInfo --- 订阅信息
rust
pub struct SubscriptionInfo {
pub topic_filter: String,
pub qos: u8,
}
2.2 全局状态管理
使用 Tauri 的 State 管理模式,通过 AppState 结构体管理所有运行时状态:
rust
pub struct AppState {
pub client: Mutex<Option<AsyncClient>>, // MQTT 客户端句柄
pub messages: Arc<Mutex<Vec<MqttMessage>>>, // 消息缓冲区
pub connected: Arc<AtomicBool>, // 连接标志
pub host: Mutex<String>,
pub port: Mutex<u16>,
pub client_id: Mutex<String>,
pub connected_at: Arc<Mutex<Option<String>>>,
pub subscriptions: Mutex<Vec<SubscriptionInfo>>,
}
设计要点:
AsyncClient用Mutex<Option<...>>包装,支持连接断开和重建messages和connected使用Arc跨线程共享(前端轮询线程与 MQTT 事件循环线程)AtomicBool用于connected标志------无锁读取,性能最优- 消息缓冲区上限 10,000 条,避免内存泄漏
2.3 MQTT 连接与管理
连接流程使用 rumqttc 库的 AsyncClient API:
rust
async fn connect(
state: State<'_, AppState>,
host: String, port: u16, client_id: String,
username: Option<String>, password: Option<String>,
clean_session: Option<bool>,
) -> Result<String, String>
步骤:
- 断开旧连接:如果已有连接,先优雅断开
- 创建 MQTT 选项:设置 Keep-Alive(30秒)、Clean Session、可选的用户名密码认证
- 创建客户端 :
AsyncClient::new(options, 100)--- 100 为消息队列容量 - 存储客户端句柄 :保存到
AppState.client供 publish/subscribe 使用 - 启动事件循环 :
tokio::spawn一个异步任务持续处理 MQTT 事件
事件循环处理
在后台线程中循环调用 eventloop.poll().await:
| 事件类型 | 处理逻辑 |
|---|---|
ConnAck |
连接成功 → 设置 connected = true,记录连接时间 |
Publish |
收到消息 → 生成 MqttMessage 并存入缓冲区 |
Disconnect |
服务端断开 → 设置 connected = false,退出循环 |
PingResp |
Keep-Alive 响应 → 忽略 |
| 错误 | 记录日志,等待 3 秒后重试(容忍瞬态网络错误) |
2.4 Tauri 命令层
共注册 8 个 Tauri 命令 ,前端通过 window.__TAURI__.tauri.invoke() 调用:
| 命令 | 功能 | 参数 | 返回值 |
|---|---|---|---|
connect |
连接 Broker | host, port, client_id, username?, password?, clean_session? | 连接成功信息 |
disconnect |
断开连接 | 无 | () |
publish |
发布消息 | topic, payload, qos, retain | () |
subscribe |
订阅主题 | topic_filter, qos | () |
unsubscribe |
取消订阅 | topic_filter | () |
get_messages |
获取消息列表 | 无 | Vec<MqttMessage> |
clear_messages |
清空消息 | 无 | () |
get_connection_status |
获取连接状态 | 无 | ConnectionStatus |
get_subscriptions |
获取订阅列表 | 无 | Vec<SubscriptionInfo> |
每个命令的核心模式:
rust
#[tauri::command]
async fn command_name(state: State<'_, AppState>, ...args) -> Result<..., String> {
// 1. 从 state 获取客户端句柄(加锁)
let client = state.client.lock()?.as_ref().ok_or("Not connected")?.clone();
// 2. 执行操作
client.do_something(...).await.map_err(|e| e.to_string())?;
// 3. 更新状态
Ok(...)
}
Result 的 Err(String) 会自动传递给前端 JavaScript 的 catch 块。
3. 前端实现
3.1 HTML 布局
采用经典的左-右两栏布局:
┌─ Title Bar ──────────────────────────────────┐
│ ◈ AtomMQTT Client ● 在线 v1.0.0 │
├──────────┬────────────────────────────────────┤
│ Sidebar │ Tabs: [发布] [订阅] [消息日志] │
│ │ │
│ 连接设置 │ ── 发布 Tab ── │
│ Broker │ 主题: [__________] │
│ 客户端ID │ 载荷: [__________] │
│ 用户名 │ QoS: [v] 保留: [x] [发布] │
│ 密码 │ │
│ [连接] │ ── 订阅 Tab ── │
│ [断开] │ 过滤器: [___] QoS: [v] [订阅] │
│ │ 活跃订阅列表 │
│ 状态: 在线│ │
│ 时间: ...│ ── 消息日志 Tab ── │
│ │ [自动滚动] [Hex] [清空] │
│ │ 2026-05-28 12:34:56.789 │
│ │ test/topic │
│ │ hello world │
├──────────┴────────────────────────────────────┤
│ Status Bar: ● 已连接 就绪 │
└───────────────────────────────────────────────┘
3.2 样式系统 (Tokyo Night 主题)
使用 CSS 自定义属性定义配色方案,灵感来自 Tokyo Night 主题:
css
:root {
--bg-primary: #1a1b26;
--bg-secondary: #24253a;
--bg-tertiary: #2d2e42;
--text-primary: #e2e3eb;
--accent: #7aa2f7;
--success: #9ece6a;
--danger: #f7768e;
}
主题特性:
- 深色背景 + 高对比度文字,适合长时间调试使用
- 等宽字体栈(Cascadia Code → Fira Code → JetBrains Mono → Consolas)
- 平滑动画和圆角设计
- 响应式布局,最小宽度 680px
3.3 JavaScript 逻辑
通信机制
前端通过 window.__TAURI__.tauri.invoke() 与 Rust 后端通信:
javascript
const { invoke } = window.__TAURI__.tauri;
// 调用 Rust 命令
const result = await invoke('connect', {
host: '127.0.0.1',
port: 1883,
clientId: 'my-client',
// ...
});
状态轮询
采用 轮询模式(而非 WebSocket/SSE)获取运行状态和消息:
javascript
function startPolling() {
pollTimer = setInterval(pollStatus, 1000); // 每秒轮询
}
async function pollStatus() {
// 1. 获取连接状态
const status = await invoke('get_connection_status');
setConnected(status.connected);
// 2. 获取新消息
const msgs = await invoke('get_messages');
updateLog(msgs);
}
设计原因: Tauri v1 的 invoke 调用延迟极低(<1ms),轮询 1 秒间隔不会产生任何可感知的性能开销,且实现简单可靠。对于 MQTT 调试场景,消息延迟 1 秒以内完全可接受。
消息日志增量更新
javascript
let prevMsgCount = 0;
function updateLog(msgs) {
if (msgs.length === prevMsgCount) return; // 无新消息,跳过
const startIdx = prevMsgCount;
const newMsgs = msgs.slice(startIdx); // 仅处理新增消息
for (const msg of newMsgs) {
const entry = document.createElement('div');
// 构建 DOM 元素
logContainer.appendChild(entry);
}
prevMsgCount = msgs.length;
}
快捷键支持
| 快捷键 | 功能 |
|---|---|
| Ctrl+Enter | 快速发布(publish 按钮) |
| Escape | 取消当前输入框焦点 |
| 消息条双击 | 切换 Hex/UTF-8 视图 |
4. 构建与部署
4.1 构建流程
推荐使用项目自带的构建脚本:
bash
$ build.bat
脚本执行步骤:
cargo build --release--- 编译 Rust 后端 + Tauri 打包前端静态资源fix_pe.cmd--- 自动修复 PE 头(仅 rust-lld 链接器需要,见 4.4 节)- 复制
target/release/tauri-mqtt-client.exe→dist/AtomMQTT-Client.exe
Tauri 在编译过程中会自动:
- 编译 Rust 后端 →
tauri-mqtt-client.exe - 读取
tauri.conf.json中的distDir: "public"→ 将public/目录打包为静态资源 - 嵌入 Windows 资源(图标、版本信息)
- 输出最终的
target/release/tauri-mqtt-client.exe
4.2 Tauri 配置 (tauri.conf.json)
关键配置项:
json
{
"build": {
"distDir": "public", // 前端静态文件目录
"devPath": "public" // 开发模式下也直接使用静态文件
},
"package": {
"productName": "AtomMQTT Client",
"version": "1.0.0"
},
"tauri": {
"allowlist": {
"shell": { "open": true } // 允许打开外部链接
},
"windows": [{
"title": "AtomMQTT Client",
"width": 1080,
"height": 800,
"minWidth": 680,
"minHeight": 500,
"center": true
}]
}
}
4.3 Windows 子系统设置 --- 消除 DOS 窗口
默认 Rust 编译 Windows 程序时使用 控制台子系统(Subsystem=3) ,导致程序启动时会同时弹出一个 DOS 控制台窗口。对于桌面 GUI 应用,需要改为 GUI 子系统(Subsystem=2)。
在 main.rs 顶部添加:
rust
#![windows_subsystem = "windows"]
这行代码告诉链接器:这是一个 Windows GUI 应用,无需分配控制台。Tauri 文档也推荐所有正式发布的桌面应用加上此属性。
4.4 PE 头损坏修复 --- 解决"此版本与 Windows 不兼容"
问题现象
在 Windows 上使用 rust-lld (LLVM LLD 链接器,Rust 工具链自带)替代 MSVC link.exe 链接时,生成的 .exe 文件的 DOS 头中的 e_lfanew 字段被写为 0 。该字段是 PE 文件格式的入口指针------它指向真正的 PE 签名(PE\0\0)在文件中的偏移量。一旦为零,Windows 加载器无法定位 PE 头,就会报告:
"此版本与正在运行的 Windows 版本不兼容"
PE 文件结构示意
┌─ DOS Header (64 bytes) ──────────────────┐
│ ... │
│ e_lfanew = 0x78 ← 指向 PE 签名偏移 │
│ ... │
├─ DOS Stub ────────────────────────────────┤
│ "This program cannot be run in DOS mode" │
├─ PE Signature ────────────────────────────┤
│ "PE\0\0" ← Windows 从此处加载 │
├─ COFF / Optional Headers ─────────────────┤
│ ... │
└───────────────────────────────────────────┘
根本原因
rust-lld 在生成 PE 文件时会写入错误的值到 e_lfanew 字段(偏移 0x3C 处,4 字节)。这在 rust-lld 的多个版本中均有出现,是 LLVM LLD 的一个已知问题。
解决方案:运行时修复
在 build.rs(Tauri 构建脚本)中添加一个 PE 头修复步骤 :编译完成后扫描生成的 .exe 文件,找到 PE\0\0 签名在文件中的实际偏移量,然后将正确的值写回 e_lfanew 字段。
build.rs 中生成修复脚本的核心逻辑:
rust
fn main() {
println!("cargo:rerun-if-changed=build.rs");
// 读取编译后的 exe 文件
let exe_path = std::env::current_dir()
.unwrap()
.join("target\\release\\tauri-mqtt-client.exe");
// 生成修复脚本 fix_pe.cmd
let script = format!(
"powershell -Command \"$b=[System.IO.File]::ReadAllBytes('{}'); ... \"",
exe_path.display()
);
std::fs::write("fix_pe.cmd", script).unwrap();
}
修复脚本运行后,Windows 加载器能够正确读取 PE 头,应用正常启动。
构建修复前后对比
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 启动是否弹 DOS 窗口 | ❌ 弹出控制台窗口 | ✅ 无窗口 |
| 是否可运行 | ❌ "版本不兼容" 错误 | ✅ 正常启动 |
| 构建工具 | cargo build | build.bat |
| 额外步骤 | 无 | build.rs 生成 → fix_pe.cmd 执行 |
5. 文件结构
tools/tauri-mqtt-client/
├── Cargo.toml # Rust 依赖配置
├── build.rs # Tauri 构建脚本(含 PE 头修复代码生成)
├── build.bat # 一键构建脚本(编译 → 修复 PE → 复制到 dist)
├── fix_pe.cmd # PE 头修复脚本(由 build.rs 自动生成)
├── tauri.conf.json # Tauri 应用配置
├── .cargo/
│ └── config.toml # Rust 链接器配置(rust-lld)
├── icons/
│ ├── icon.ico # Windows 程序图标
│ └── icon.png # PNG 图标
├── public/ # 前端静态文件(distDir)
│ ├── index.html # 主页面
│ ├── styles.css # 样式表(640 行)
│ └── script.js # 前端逻辑(353 行)
├── src/
│ └── main.rs # Rust 后端(323 行)
├── dist/ # 构建产出
│ └── AtomMQTT-Client.exe # 可执行文件
└── install.bat # Windows 安装脚本
6. 与同等工具的比较
| 特性 | AtomMQTT Client | MQTTX (Electron) | mosquitto_sub (CLI) |
|---|---|---|---|
| 二进制体积 | ~7 MB | ~120 MB | ~500 KB(但不含 GUI) |
| 内存占用 | ~40 MB | ~200 MB | --- |
| 启动速度 | <500ms | ~3s | 瞬发 |
| GUI 框架 | Tauri (系统 WebView) | Electron (Chromium) | 无 |
| QoS 支持 | 0/1/2 | 0/1/2 | 0/1/2 |
| Hex 视图 | ✅ | ✅ | ❌ |
| 主题 | Tokyo Night 深色 | 可切换 | 无 |
AtomMQTT Client 的核心优势在于极低的资源占用 和快速的启动速度,利用了操作系统内置的 WebView2 Runtime,无需捆绑 Chromium。
7. 总结
AtomMQTT Client 是一个轻量、高效、美观的 MQTT 桌面调试工具:
- Rust 后端 通过
rumqttc实现完整的 MQTT 3.1.1 协议支持 - Tauri 框架提供原生桌面体验,无需 Electron 的臃肿
- 原生前端(HTML/CSS/JS)零依赖,启动即用
- Tokyo Night 深色主题适合开发者长时间使用
- 双视图消息日志(UTF-8 / Hex)方便调试二进制协议
整个项目约 1,300 行代码(Rust 323 行 + JS 353 行 + CSS 640 行),体现了 Rust + Tauri 栈构建桌面应用的简洁与高效。