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 --- 消息结构
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>>,
}

设计要点:

  • AsyncClientMutex<Option<...>> 包装,支持连接断开和重建
  • messagesconnected 使用 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>

步骤:

  1. 断开旧连接:如果已有连接,先优雅断开
  2. 创建 MQTT 选项:设置 Keep-Alive(30秒)、Clean Session、可选的用户名密码认证
  3. 创建客户端AsyncClient::new(options, 100) --- 100 为消息队列容量
  4. 存储客户端句柄 :保存到 AppState.client 供 publish/subscribe 使用
  5. 启动事件循环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(...)
}

ResultErr(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

脚本执行步骤:

  1. cargo build --release --- 编译 Rust 后端 + Tauri 打包前端静态资源
  2. fix_pe.cmd --- 自动修复 PE 头(仅 rust-lld 链接器需要,见 4.4 节)
  3. 复制 target/release/tauri-mqtt-client.exedist/AtomMQTT-Client.exe

Tauri 在编译过程中会自动:

  1. 编译 Rust 后端 → tauri-mqtt-client.exe
  2. 读取 tauri.conf.json 中的 distDir: "public" → 将 public/ 目录打包为静态资源
  3. 嵌入 Windows 资源(图标、版本信息)
  4. 输出最终的 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 栈构建桌面应用的简洁与高效。

相关推荐
lly20240613 小时前
建造者模式
开发语言
之歆13 小时前
Day20_PC 端电商商品详情页前端实战:从布局到放大镜与选项卡
开发语言·前端·javascript·css·less
Gopher_HBo13 小时前
Go语言学习笔记(二)
后端
SimonKing13 小时前
57K星标的开源AI视频神器:三分钟出片,零门槛
java·后端·程序员
众创岛13 小时前
java环境配置(windows)
java·开发语言
光泽雨13 小时前
C# 扩展方法(Extension Method)在语法上的核心灵魂。
开发语言·c#
代码小书生13 小时前
shutil,一个文件操作的 Python 库!
开发语言·python·策略模式
啄缘之间13 小时前
10.【学习】SPI & UART 验证环境与测试用例
开发语言·经验分享·学习·fpga开发·测试用例·verilog