Rust+ Tauri实现漂亮小巧的Mqtt客户端工具--AtomMQTT Client 实现详解
基于 Rust + Tauri 的桌面 MQTT 调试客户端
项目开源地址:https://gitcode.com/qq8864/atomMqtt
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 — 消息结构
#[derive(Debug, Clone, Serialize)]pubstructMqttMessage{pubtopic:String,// 消息主题pubpayload:String,// UTF-8 载荷文本pubpayload_hex:String,// Hex 编码载荷(用于二进制数据查看)pubqos:u8,// 服务质量 (0/1/2)pubretain:bool,// 保留消息标志pubtimestamp:String,// 接收时间(毫秒精度)pubpacket_id:Option<u16>,// MQTT 包 ID}每条接收到的消息会同时保存UTF-8 文本和Hex 编码两种形式的载荷,前端可在 UI 中切换显示。
ConnectionStatus — 连接状态
pubstructConnectionStatus{pubconnected:bool,pubhost:String,pubport:u16,pubclient_id:String,pubconnected_at:Option<String>,}SubscriptionInfo — 订阅信息
pubstructSubscriptionInfo{pubtopic_filter:String,pubqos:u8,}2.2 全局状态管理
使用 Tauri 的State管理模式,通过AppState结构体管理所有运行时状态:
pubstructAppState{pubclient:Mutex<Option<AsyncClient>>,// MQTT 客户端句柄pubmessages:Arc<Mutex<Vec<MqttMessage>>>,// 消息缓冲区pubconnected:Arc<AtomicBool>,// 连接标志pubhost:Mutex<String>,pubport:Mutex<u16>,pubclient_id:Mutex<String>,pubconnected_at:Arc<Mutex<Option<String>>>,pubsubscriptions:Mutex<Vec<SubscriptionInfo>>,}设计要点:
AsyncClient用Mutex<Option<...>>包装,支持连接断开和重建messages和connected使用Arc跨线程共享(前端轮询线程与 MQTT 事件循环线程)AtomicBool用于connected标志——无锁读取,性能最优- 消息缓冲区上限10,000 条,避免内存泄漏
2.3 MQTT 连接与管理
连接流程使用rumqttc库的AsyncClientAPI:
asyncfnconnect(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> |
每个命令的核心模式:
#[tauri::command]asyncfncommand_name(state:State<'_,AppState>,...args)->Result<...,String>{// 1. 从 state 获取客户端句柄(加锁)letclient=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 主题:
: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 后端通信:
const{invoke}=window.__TAURI__.tauri;// 调用 Rust 命令constresult=awaitinvoke('connect',{host:'127.0.0.1',port:1883,clientId:'my-client',// ...});状态轮询
采用轮询模式(而非 WebSocket/SSE)获取运行状态和消息:
functionstartPolling(){pollTimer=setInterval(pollStatus,1000);// 每秒轮询}asyncfunctionpollStatus(){// 1. 获取连接状态conststatus=awaitinvoke('get_connection_status');setConnected(status.connected);// 2. 获取新消息constmsgs=awaitinvoke('get_messages');updateLog(msgs);}设计原因:Tauri v1 的 invoke 调用延迟极低(<1ms),轮询 1 秒间隔不会产生任何可感知的性能开销,且实现简单可靠。对于 MQTT 调试场景,消息延迟 1 秒以内完全可接受。
消息日志增量更新
letprevMsgCount=0;functionupdateLog(msgs){if(msgs.length===prevMsgCount)return;// 无新消息,跳过conststartIdx=prevMsgCount;constnewMsgs=msgs.slice(startIdx);// 仅处理新增消息for(constmsgofnewMsgs){constentry=document.createElement('div');// 构建 DOM 元素logContainer.appendChild(entry);}prevMsgCount=msgs.length;}快捷键支持
| 快捷键 | 功能 |
|---|---|
| Ctrl+Enter | 快速发布(publish 按钮) |
| Escape | 取消当前输入框焦点 |
| 消息条双击 | 切换 Hex/UTF-8 视图 |
4. 构建与部署
4.1 构建流程
推荐使用项目自带的构建脚本:
$ 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)
关键配置项:
{"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顶部添加:
#![windows_subsystem ="windows"]这行代码告诉链接器:这是一个 Windows GUI 应用,无需分配控制台。Tauri 文档也推荐所有正式发布的桌面应用加上此属性。
4.4 PE 头损坏修复 — 解决"此版本与 Windows 不兼容"
问题现象
在 Windows 上使用rust-lld(LLVM LLD 链接器,Rust 工具链自带)替代 MSVClink.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中生成修复脚本的核心逻辑:
fnmain(){println!("cargo:rerun-if-changed=build.rs");// 读取编译后的 exe 文件letexe_path=std::env::current_dir().unwrap().join("target\\release\\tauri-mqtt-client.exe");// 生成修复脚本 fix_pe.cmdletscript=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 栈构建桌面应用的简洁与高效。
