Qt中集成 MQTT 来实现物联网通信:从原理到实战全解析

前言

在物联网(IoT)、工业控制、智能家居等场景中,轻量级的消息传输协议至关重要。所以,MQTT(Message Queuing Telemetry Transport)凭借 "低带宽、低功耗、低资源占用" 的特性,成为这类场景的首选。Qt 作为跨平台的应用开发框架,从 Qt5.10 开始(好像是,没细察)提供了 MQTT 模块,到现在已实现功能完善、接口稳定的 MQTT 通信支持。本文将从 MQTT 核心原理入手,尝试在 Q 中通过调用 MQTT 来实现通信。首先,先来了解一下MQTT。

一、MQTT 协议核心:为什么它是物联网通信的首选?

在使用之前,首先要搞懂 "MQTT 到底是什么""它的设计逻辑是什么"------ 理解这些底层逻辑,才能在实际开发中避免踩坑,而非单纯 "复制粘贴代码"。

1.1 MQTT 的核心定位:轻量级发布 / 订阅协议

MQTT 是由 IBM 在 1999 年设计的基于 TCP/IP 的发布 / 订阅(Publish/Subscribe) 协议,核心目标是 "在受限网络环境下实现设备间的可靠通信"。它的 "轻量" 体现在:

  • 报文头部最小仅 2 字节,极大降低网络传输开销;
  • 协议逻辑简单,嵌入式设备(如单片机、传感器)仅需极少资源就能实现;
  • 支持断线重连、消息持久化,保证弱网环境下的通信可靠性。

对比 HTTP 协议(请求 - 响应模式,客户端主动发起、服务端被动响应),MQTT 的发布 / 订阅模式更适合物联网场景:

  • HTTP:一对一的 "问答式" 通信,客户端问、服务端答,无法主动推送;
  • MQTT:一对多的 "广播式" 通信,发布者只管发消息,订阅者只管收消息,中间由服务器转发,设备间无需知道彼此的 IP / 端口。

1.2 MQTT 的核心架构:三大角色

MQTT 的通信体系由三个核心角色构成,三者分工明确,共同完成消息的传输:

(1)发布者(Publisher)

"消息的发送方",可以是任何设备 / 应用(如传感器、手机 APP、Qt 程序),它不关心谁会接收消息,只需要将消息发布到指定的 "主题(Topic)" 上。

举例:温湿度传感器每 5 秒采集一次数据,将 "温度 = 25℃,湿度 = 60%" 发布到主题 "sensor/room1/data"。

(2)订阅者(Subscriber)

"消息的接收方",同样可以是任何设备 / 应用,它需要提前向服务器 "订阅" 自己感兴趣的主题,当该主题有新消息时,服务器会主动将消息推送给它。

举例:Qt 编写的监控界面订阅 "sensor/room1/data",一旦传感器发布新数据,就能实时显示在界面上。

(3)代理服务器(Broker)

Broker是MQTT 通信的 "中转站",是整个架构的核心,主要作用是:

  • 接收发布者的消息并存储;
  • 管理所有订阅关系;
  • 将消息推送给订阅了对应主题的订阅者;
  • 处理客户端的连接、断开、重连等逻辑。

常见的 MQTT Broker有:

  • Eclipse Mosquitto(开源轻量,适合开发 / 测试);
  • EMQ X(开源企业级,支持集群、多协议);
  • AWS IoT Core、阿里云 IoT 平台(云服务商提供的托管式 Broker)。

1.3 MQTT 的核心概念:读懂协议的关键

(1)主题(Topic):消息的 "地址"

主题是 MQTT 消息的 "唯一标识",类似文件系统的路径,用斜杠/分隔层级,发布者和订阅者通过主题完成消息的 "匹配"。

  • 举一些设定主题示例:
    device/001/status(设备 001 的状态消息);
    command/all/restart(给所有设备发送重启命令);
    sensor/temperature(温度传感器数据)。
  • 主题通配符:订阅者可以通过通配符订阅多个相关主题,发布者不能使用通配符发布消息:
    +号:匹配 "单层" 主题,如订阅device/+/status的主题,可接收device/001/status、device/002/status,但不接收device/001/detail/status;
    #号:匹配 "多层" 主题(必须放在最后),如订阅device/#,可接收device/001/status、device/002/detail/data等所有以device/开头的主题。
(2)QoS(Quality of Service):消息的 "可靠性等级"

MQTT 定义了 3 种 QoS 等级,用于平衡 "可靠性" 和 "传输效率",发布者和订阅者可协商消息的 QoS 等级:

  • QoS 0:最多一次(At most once)
    消息 "发出去就不管了",服务器 / 客户端不会确认是否收到,可能丢失,但传输效率最高。
    适用场景:实时性要求高、允许少量丢失的场景(如传感器实时数据)。
  • QoS 1:至少一次(At least once)
    消息一定会被送达,但可能重复(如网络延迟导致重发),服务器 / 客户端会通过 "确认报文(PUBACK)" 保证消息送达。
    适用场景:不允许丢失、可接受少量重复的场景(如设备控制命令)。
  • QoS 2:恰好一次(Exactly once)
    消息仅被送达一次,通过 "四次握手"(PUBLISH→PUBREC→PUBREL→PUBCOMP)保证不丢不重,可靠性最高,但传输开销最大。
    适用场景:金融交易、关键指令等不允许丢失和重复的场景。
(3)保留消息(Retained Message)

发布者可以设置消息为 "保留消息",Broker 会存储该主题的最后一条保留消息;当新的订阅者订阅该主题时,Broker 会立即将这条保留消息推送给它。

举例:传感器发布 "温度 = 25℃" 并设置为保留消息,之后新启动的 Qt 监控界面订阅该主题,无需等待传感器下次发布,就能直接获取最新的温度数据。

(4)遗嘱消息(Last Will and Testament,LWT)

客户端连接 Broker 时,可以预设一条 "遗嘱消息" 和对应的主题;如果客户端异常断开连接(如断电、网络中断),Broker 会自动将这条遗嘱消息发布到指定主题,通知其他设备该客户端离线。

举例:设备连接时设置遗嘱消息为 "offline",主题为 "device/001/status";若设备突然断电,Broker 会自动发布 "offline" 到该主题,Qt 监控界面就能立即知道设备离线。

(5)会话(Session)

客户端与 Broker 的连接可以关联一个 "会话":

若客户端断开连接,Broker 会保存该会话的订阅关系、未完成的 QoS 1/2 消息;

客户端重连后,Broker 会继续推送未完成的消息,恢复订阅关系;

若不需要保留会话,可在连接时设置 "清理会话(Clean Session)" 为true,断开后 Broker 会删除所有相关数据。

二、Qt的 MQTT 模块:环境准备与核心接口

2.1 确认Qt版本与模块获取

Qt中的 MQTT 模块并非默认集成的安装好的,Qt5.10以上的版本虽然开始支持了 MQTT 模块,但在安装时需要先手动的选择安装与配置。

所以,首先看你的Qt版本,如果时Qt5.10以前的,那就本身不支持MQTT,而Qt5.10以上的版本的 MQTT 模块分为两种方式安装:

第一种:Qt 官方安装器(推荐用这种),安装 Qt时,在 "组件选择" 中勾选 "Qt x.x.x → Additional Libraries → Qt MQTT",安装器会自动编译并配置模块。

第二种:手动编译源码

若安装时未勾选,可从 Qt 官方仓库下载 MQTT 源码(https://code.qt.io/cgit/qt/qtmqtt.git/)进行手动编译,可自行查阅编译流程,在此不做赘述。

2.2 工程配置:引入MQTT模块

无论采用哪种安装方式,使用MQTT模块时,都需要在Qt工程中添加模块依赖,否则会出现编译错误。

若使用qmake构建工程,在工程的.pro文件中添加以下代码:

cpp 复制代码
QT += core gui widgets mqtt  # 核心模块+MQTT模块

若使用CMake构建工程,需在CMakeLists.txt中添加以下配置,指定依赖的Qt模块:

cpp 复制代码
find_package(Qt REQUIRED COMPONENTS Core Gui Widgets Mqtt)
target_link_libraries(your_project_name PRIVATE Qt::Core Qt::Gui Qt::Widgets Qt::Mqtt)

配置完成后,重新构建工程,即可正常使用Qt的MQTT相关类和接口。

2.3 Qt中MQTT的核心类

Qt的MQTT模块封装了多个类,核心类仅有4个,分别负责连接管理、订阅管理、消息封装和连接属性配置。下面先对每个核心类做简单说明,再展开讲解常用接口和用法,贴合开发实际,只讲高频用到的内容,避免冗余。

(1)QMqttClient:核心通信类

QMqttClient是整个MQTT通信的入口类,负责与Broker建立连接、断开连接,管理会话,发布消息和订阅主题,是开发中最常用的核心类,所有MQTT通信操作都围绕它展开。

常用接口与信号分为四大类,重点掌握连接相关、状态相关、发布消息和订阅主题的接口,足以覆盖大部分开发场景。
连接相关接口:用于配置Broker连接参数并建立连接,是通信的第一步。

  • setHostname(const QString &hostname):设置Broker的IP地址或域名,比如本地Broker填写"127.0.0.1",公网Broker填写对应的域名(如"test.mosquitto.org")。
  • setPort(quint16 port):设置Broker的端口,默认端口为1883(非加密连接),SSL/TLS加密连接的默认端口为8883,需根据Broker的配置填写。
  • setClientId(const QString &clientId):设置客户端ID,Broker通过该ID唯一标识每个客户端,必须保证客户端ID不重复,否则会导致连接失败。
  • setUsername(const QString &username)、setPassword(const QString &password):设置Broker的认证信息,若Broker开启了用户名/密码认证,必须填写正确的信息,否则无法连接;若未开启认证,无需调用这两个接口。
  • connectToHost():发起与Broker的连接,该操作是异步的,不会阻塞主线程,连接结果需通过信号判断。
  • disconnectFromHost():主动断开与Broker的连接,断开前会清理会话相关资源(根据清理会话参数配置)。

状态信号:用于监听连接状态变化和错误信息,是排查连接问题、处理会话逻辑的关键,必须绑定对应的槽函数。

  • connected():连接成功时触发,可在该信号的槽函数中执行订阅主题、初始化通信逻辑等操作。
  • disconnected():断开连接时触发,可在该信号的槽函数中处理断线逻辑,比如启动重连定时器、清理订阅实例等。
  • connectionStateChanged(QMqttClient::ClientState state);连接状态变化时触发,参数state表示当前状态,包括未连接、连接中、已连接三种状态,可用于更新界面显示(如连接状态标签)。
  • errorOccurred(QMqttClient::ClientError error):连接或通信过程中出现错误时触发,参数error为错误码,可通过错误码判断问题原因(如IP错误、密码错误、客户端ID重复等)。

发布消息接口:用于将消息发布到指定主题,核心接口仅有一个,支持配置QoS等级和是否保留消息。

  • publish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos = 0, bool retain = false);发布消息到指定主题,返回消息ID(用于追踪QoS 1/2消息的重发和确认)。参数说明:topic是目标主题,message是消息内容(QByteArray类型,支持二进制和文本消息),qos是消息的QoS等级(默认0),retain表示是否设置为保留消息(默认false)。

订阅主题接口:用于订阅指定主题,获取该主题的消息,返回订阅实例,后续通过订阅实例接收消息。

  • subscribe(const QMqttTopicFilter &topic, quint8 qos = 0);订阅指定主题,返回QMqttSubscription类型的实例,该实例用于接收该主题的消息;若订阅失败,返回nullptr。参数topic是要订阅的主题(支持通配符),qos是订阅的QoS等级(默认0)。
  • unsubscribe(const QMqttTopicFilter &topic):取消订阅指定主题,取消后将不再接收该主题的消息,同时会清理对应的订阅实例资源。
(2)QMqttSubscription:订阅实例类

功能说明 :QMqttSubscription是订阅主题的实例类,每个订阅操作都会生成一个对应的实例,用于管理单个订阅的QoS等级、主题信息,以及接收该订阅的消息,核心作用是绑定消息接收信号。
常用信号:核心信号仅有一个,用于接收订阅主题的消息。

  • messageReceived(QMqttMessage msg);当订阅的主题有新消息发布时,Broker推送消息后触发该信号,参数msg是接收到的消息载体(QMqttMessage类型),通过该参数可获取消息内容、主题、QoS等级等信息。
    注意:每个订阅实例都需要绑定该信号的槽函数,否则无法接收消息;取消订阅时,需先断开该信号的连接,避免内存泄漏。
(3)QMqttMessage:消息载体类

功能说明 :QMqttMessage是MQTT消息的载体类,封装了消息的所有属性,包括消息内容、所属主题、QoS等级、是否保留、时间戳等,无论是发布消息还是接收消息,都需要通过该类操作。
常用接口:用于获取消息的各类属性,满足业务逻辑处理需求。

  • payload():返回消息内容,类型为QByteArray,可根据需求转换为字符串(文本消息)或二进制数据(二进制消息)。
  • topic():返回消息所属的主题,类型为QMqttTopicName,通过name()方法可获取主题的字符串形式。
  • qos():返回消息的QoS等级,类型为quint8,对应0、1、2三个等级。
  • isRetained():判断该消息是否为保留消息,返回bool值(true为保留消息,false为非保留消息)。
  • timestamp():返回消息的发送时间戳,类型为QDateTime,可用于记录消息的接收时间或判断消息的时效性。
(4)QMqttConnectionProperties:连接属性配置类

功能说明:QMqttConnectionProperties用于配置MQTT连接的额外属性,比如遗嘱消息、清理会话、会话过期时间等,通常在建立连接前配置,应用到QMqttClient实例中,优化连接的可靠性和安全性。

常用接口:主要用于配置遗嘱消息和会话相关属性,是提升通信可靠性的关键。

  • setWillTopic(const QMqttTopicName &topic):设置遗嘱消息的主题,当客户端异常断开时,Broker会将遗嘱消息发布到该主题。
  • setWillMessage(const QByteArray &message):设置遗嘱消息的内容,支持文本和二进制消息。
  • setWillQoS(quint8 qos):设置遗嘱消息的QoS等级,建议设为1,确保遗嘱消息能被成功推送。
  • setWillRetain(bool retain):设置遗嘱消息是否为保留消息,建议设为true,确保新订阅者能获取到设备离线的遗嘱消息。
  • setCleanSession(bool clean):设置是否清理会话,true表示断开后清理会话,false表示保留会话。
  • setSessionExpiryInterval(quint32 interval):设置会话过期时间(单位:秒),若客户端断开后,超过该时间未重连,Broker会清理会话资源。

配置完成后,通过QMqttClient的setConnectionProperties()方法,将属性应用到客户端实例中,再发起连接。

三、Qt调用MQTT实战

接下来通过做一个MQTT通信Demo来试试,这个Demo包含连接Broker、发布消息、订阅主题、接收消息、断线重连、遗嘱消息等核心功能,界面简洁,代码注释详细,新手可直接复制使用,编译后就能快速测试MQTT通信。同时,案例会呼应前文讲解的QoS等级、保留消息、遗嘱消息等核心特性,帮大家打通"理论+实践"的壁垒。

3.1 基本功能与界面

Demo的核心功能覆盖MQTT通信全流程,满足基础通信,同时便于扩展,具体如下:

  1. 配置Broker的IP、端口、客户端ID、用户名/密码,灵活调整连接参数;
  2. 有个连接/断开Broker的按钮,实时显示连接状态,便于操作和排查;
  3. 可以发布消息到指定主题,可设置QoS等级、是否保留消息,满足不同的场景;
  4. 支持订阅指定主题(支持通配符),可设置QoS等级,实时接收并显示消息;
  5. 显示通信日志,包括连接状态、收发消息、错误信息等,便于排查问题;
  6. 实现断线重连功能,网络波动或Broker重启后,自动尝试重连,保证通信稳定性;
  7. 配置遗嘱消息,客户端异常断开时,Broker自动发布离线通知,便于设备状态监控。

界面就用Qt Widgets,简洁直观,就四个功能区,布局逻辑贴合操作流程:

  • 左侧:连接配置区,包含Broker IP、端口、客户端ID、用户名/密码的输入框,以及连接/断开按钮、连接状态显示标签,聚焦连接参数配置;
  • 中间:发布消息区,包含发布主题输入框、消息内容编辑框、QoS等级选择框、保留消息勾选框,以及发布按钮,负责消息下发;
  • 右侧:订阅区,包含订阅主题输入框、QoS等级选择框,以及订阅/取消订阅按钮,负责主题订阅与取消;
  • 底部:日志显示区,只读显示所有通信日志,错误日志标红,便于区分,支持自动滚动到最新日志,助力问题排查。

3.2 完整代码实现

Demo用Qt Widgets开发,项目结构包含三个文件:头文件(mqttclientdemo.h)、源文件(mqttclientdemo.cpp)、主函数文件(main.cpp),关键逻辑加上注释,同时处理参数校验、错误排查、资源释放等细节。

(1)头文件(mqttclientdemo.h)

头文件中定义核心类MqttClientDemo,继承自QMainWindow,声明界面控件、MQTT核心对象、槽函数和工具函数,梳理类的结构和接口,便于后续维护,同时引入所需的MQTT相关头文件。

cpp 复制代码
#ifndef MQTTCLIENTDEMO_H
#define MQTTCLIENTDEMO_H

#include <QMainWindow>
#include <QMqttClient>
#include <QMqttSubscription>
#include <QTimer>
#include <QLineEdit>
#include <QTextEdit>
#include <QComboBox>
#include <QCheckBox>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QDateTime>

class MqttClientDemo : public QMainWindow
{
    Q_OBJECT

public:
    MqttClientDemo(QWidget *parent = nullptr);
    ~MqttClientDemo();

private slots:
    // 连接/断开Broker的槽函数
    void onConnectBtnClicked();
    void onDisconnectBtnClicked();
    // 发布消息的槽函数
    void onPublishBtnClicked();
    // 订阅/取消订阅的槽函数
    void onSubscribeBtnClicked();
    void onUnsubscribeBtnClicked();
    // MQTT客户端状态回调槽函数
    void onConnected();                  // 连接成功回调
    void onDisconnected();               // 断开连接回调
    void onConnectionStateChanged(QMqttClient::ClientState state); // 状态变化回调
    void onErrorOccurred(QMqttClient::ClientError error); // 错误回调
    // 接收订阅消息的槽函数
    void onMessageReceived(QMqttMessage msg);
    // 断线重连的槽函数
    void onReconnectTimerTimeout();

private:
    // 初始化界面的工具函数
    void initUI();
    // 初始化MQTT客户端的工具函数
    void initMqttClient();
    // 添加日志的工具函数,支持错误日志标红
    void addLog(const QString &log, bool isError = false);
    // 设置遗嘱消息的工具函数
    void setWillMessage();
    // 参数校验工具函数,校验连接、发布、订阅的必填参数
    bool checkConnectParams();
    bool checkPublishParams();
    bool checkSubscribeParams();

    // MQTT核心对象
    QMqttClient *m_mqttClient;
    // 存储已订阅的主题和对应的订阅实例,用于取消订阅和管理
    QMap<QString, QMqttSubscription*> m_subscriptions;
    // 断线重连定时器,用于循环尝试重连
    QTimer *m_reconnectTimer;
    // 是否开启自动重连(默认开启)
    bool m_autoReconnect = true;
    // 重连间隔(默认5000毫秒,即5秒)
    const int m_reconnectInterval = 5000;

    // 界面控件声明,按功能分区整理
    // 连接配置区控件
    QLineEdit *m_leHostname;    // Broker IP/域名输入框
    QLineEdit *m_lePort;        // 端口输入框
    QLineEdit *m_leClientId;    // 客户端ID输入框
    QLineEdit *m_leUsername;    // 用户名输入框
    QLineEdit *m_lePassword;    // 密码输入框
    QPushButton *m_btnConnect;  // 连接Broker按钮
    QPushButton *m_btnDisconnect; // 断开连接按钮
    QLabel *m_lblState;         // 连接状态显示标签

    // 发布消息区控件
    QLineEdit *m_lePublishTopic; // 发布主题输入框
    QTextEdit *m_tePublishMsg;   // 发布消息内容编辑框
    QComboBox *m_cbxPublishQos;  // 发布QoS等级选择框
    QCheckBox *m_chkRetain;      // 保留消息勾选框
    QPushButton *m_btnPublish;   // 发布消息按钮

    // 订阅区控件
    QLineEdit *m_leSubscribeTopic; // 订阅主题输入框
    QComboBox *m_cbxSubscribeQos;  // 订阅QoS等级选择框
    QPushButton *m_btnSubscribe;   // 订阅主题按钮
    QPushButton *m_btnUnsubscribe; // 取消订阅按钮

    // 日志区控件
    QTextEdit *m_teLog;           // 日志显示框
};

#endif // MQTTCLIENTDEMO_H
(2)源文件(mqttclientdemo.cpp)

源文件中实现头文件声明的所有函数,包含界面初始化、MQTT客户端初始化、信号槽绑定、核心通信逻辑、错误处理、日志记录等功能,补全所有未完成的函数实现,关键逻辑就依照前文的MQTT核心特性和Qt接口用法。

cpp 复制代码
#include "mqttclientdemo.h"
#include <QMessageBox>
#include <QRandomGenerator>

MqttClientDemo::MqttClientDemo(QWidget *parent)
    : QMainWindow(parent)
{
    // 初始化界面,创建所有控件并布局
    initUI();
    // 初始化MQTT客户端,绑定信号槽
    initMqttClient();
    // 设置窗口标题和默认大小,提升用户体验
    setWindowTitle("Qt MQTT通信Demo");
    resize(1000, 700);
}

MqttClientDemo::~MqttClientDemo()
{
    // 析构函数中释放资源,避免内存泄漏
    // 若当前处于连接状态,先主动断开连接
    if (m_mqttClient->state() == QMqttClient::Connected) {
        m_mqttClient->disconnectFromHost();
    }
    // 停止重连定时器
    if (m_reconnectTimer->isActive()) {
        m_reconnectTimer->stop();
    }
    // 释放所有订阅实例
    for (auto sub : m_subscriptions.values()) {
        disconnect(sub, &QMqttSubscription::messageReceived, this, &MqttClientDemo::onMessageReceived);
        delete sub;
    }
    m_subscriptions.clear();
    // 释放MQTT客户端和重连定时器资源
    delete m_mqttClient;
    delete m_reconnectTimer;
}

// 初始化界面:创建控件、设置布局、绑定按钮点击信号
void MqttClientDemo::initUI()
{
    // 整体布局:中心窗口+水平主布局,用于容纳各个功能区域
    QWidget *centralWidget = new QWidget(this);
    QHBoxLayout *mainLayout = new QHBoxLayout(centralWidget);
    setCentralWidget(centralWidget);

    // 左侧:连接配置区,用GroupBox分组,提升界面整洁度
    QGroupBox *connectGroup = new QGroupBox("Broker连接配置", this);
    QVBoxLayout *connectLayout = new QVBoxLayout(connectGroup);

    // 1. Broker IP输入布局(标签+输入框)
    QHBoxLayout *hostnameLayout = new QHBoxLayout();
    QLabel *lblHostname = new QLabel("Broker IP:", this);
    m_leHostname = new QLineEdit(this);
    m_leHostname->setPlaceholderText("如:127.0.0.1 或 test.mosquitto.org");
    m_leHostname->setText("127.0.0.1"); // 默认填写本地Broker IP,方便测试
    hostnameLayout->addWidget(lblHostname);
    hostnameLayout->addWidget(m_leHostname);
    connectLayout->addLayout(hostnameLayout);

    // 2. 端口输入布局
    QHBoxLayout *portLayout = new QHBoxLayout();
    QLabel *lblPort = new QLabel("端口:", this);
    m_lePort = new QLineEdit(this);
    m_lePort->setPlaceholderText("默认1883,SSL加密连接为8883");
    m_lePort->setText("1883"); // 默认填写非加密端口
    portLayout->addWidget(lblPort);
    portLayout->addWidget(m_lePort);
    connectLayout->addLayout(portLayout);

    // 3. 客户端ID输入布局
    QHBoxLayout *clientIdLayout = new QHBoxLayout();
    QLabel *lblClientId = new QLabel("客户端ID:", this);
    m_leClientId = new QLineEdit(this);
    // 生成随机客户端ID(避免多个客户端使用相同ID导致连接失败)
    QString randomClientId = QString("QtMqttDemo_%1").arg(QRandomGenerator::global()->generate64() % 10000);
    m_leClientId->setText(randomClientId);
    clientIdLayout->addWidget(lblClientId);
    clientIdLayout->addWidget(m_leClientId);
    connectLayout->addLayout(clientIdLayout);

    // 4. 用户名输入布局
    QHBoxLayout *usernameLayout = new QHBoxLayout();
    QLabel *lblUsername = new QLabel("用户名:", this);
    m_leUsername = new QLineEdit(this);
    m_leUsername->setPlaceholderText("Broker未开启认证则留空");
    usernameLayout->addWidget(lblUsername);
    usernameLayout->addWidget(m_leUsername);
    connectLayout->addLayout(usernameLayout);

    // 5. 密码输入布局
    QHBoxLayout *passwordLayout = new QHBoxLayout();
    QLabel *lblPassword = new QLabel("密码:", this);
    m_lePassword = new QLineEdit(this);
    m_lePassword->setEchoMode(QLineEdit::Password); // 密码隐藏显示
    m_lePassword->setPlaceholderText("Broker未开启认证则留空");
    passwordLayout->addWidget(lblPassword);
    passwordLayout->addWidget(m_lePassword);
    connectLayout->addLayout(passwordLayout);

    // 6. 连接/断开按钮布局
    QHBoxLayout *connectBtnLayout = new QHBoxLayout();
    m_btnConnect = new QPushButton("连接Broker", this);
    m_btnDisconnect = new QPushButton("断开连接", this);
    m_btnDisconnect->setEnabled(false); // 初始状态:未连接,断开按钮不可用
    connectBtnLayout->addWidget(m_btnConnect);
    connectBtnLayout->addWidget(m_btnDisconnect);
    connectLayout->addLayout(connectBtnLayout);

    // 7. 连接状态显示布局
    QHBoxLayout *stateLayout = new QHBoxLayout();
    QLabel *lblState = new QLabel("连接状态:", this);
    m_lblState = new QLabel("未连接", this);
    m_lblState->setStyleSheet("color: red; font-weight: bold;"); // 未连接标红
    stateLayout->addWidget(lblState);
    stateLayout->addWidget(m_lblState);
    connectLayout->addLayout(stateLayout);

    // 将连接配置区添加到主布局
    mainLayout->addWidget(connectGroup);

    // 中间:发布消息区,用GroupBox分组
    QGroupBox *publishGroup = new QGroupBox("发布消息", this);
    QVBoxLayout *publishLayout = new QVBoxLayout(publishGroup);

    // 1. 发布主题输入布局
    QHBoxLayout *publishTopicLayout = new QHBoxLayout();
    QLabel *lblPublishTopic = new QLabel("发布主题:", this);
    m_lePublishTopic = new QLineEdit(this);
    m_lePublishTopic->setPlaceholderText("如:qt/mqtt/demo/pub");
    m_lePublishTopic->setText("qt/mqtt/demo/pub"); // 默认主题,方便测试
    publishTopicLayout->addWidget(lblPublishTopic);
    publishTopicLayout->addWidget(m_lePublishTopic);
    publishLayout->addLayout(publishTopicLayout);

    // 2. 发布消息内容编辑布局(标签+文本编辑框)
    QLabel *lblPublishMsg = new QLabel("消息内容:", this);
    m_tePublishMsg = new QTextEdit(this);
    m_tePublishMsg->setPlaceholderText("输入要发布的消息内容");
    m_tePublishMsg->setText("Hello Qt MQTT!"); // 默认消息内容
    publishLayout->addWidget(lblPublishMsg);
    publishLayout->addWidget(m_tePublishMsg);

    // 3. 发布QoS等级选择布局
    QHBoxLayout *publishQosLayout = new QHBoxLayout();
    QLabel *lblPublishQos = new QLabel("QoS等级:", this);
    m_cbxPublishQos = new QComboBox(this);
    // 添加三个QoS等级选项,数据存储为对应的等级数值(0、1、2)
    m_cbxPublishQos->addItem("QoS 0(最多一次)", 0);
    m_cbxPublishQos->addItem("QoS 1(至少一次)", 1);
    m_cbxPublishQos->addItem("QoS 2(恰好一次)", 2);
    publishQosLayout->addWidget(lblPublishQos);
    publishQosLayout->addWidget(m_cbxPublishQos);
    publishLayout->addLayout(publishQosLayout);

    // 4. 保留消息勾选布局
    QHBoxLayout *retainLayout = new QHBoxLayout();
    m_chkRetain = new QCheckBox("保留消息", this);
    retainLayout->addWidget(m_chkRetain);
    publishLayout->addLayout(retainLayout);

    // 5. 发布消息按钮
    m_btnPublish = new QPushButton("发布消息", this);
    m_btnPublish->setEnabled(false); // 初始状态:未连接,发布按钮不可用
    publishLayout->addWidget(m_btnPublish);

    // 将发布消息区添加到主布局
    mainLayout->addWidget(publishGroup);

    // 右侧:订阅区,用GroupBox分组
    QGroupBox *subscribeGroup = new QGroupBox("订阅主题", this);
    QVBoxLayout *subscribeLayout = new QVBoxLayout(subscribeGroup);

    // 1. 订阅主题输入布局
    QHBoxLayout *subscribeTopicLayout = new QHBoxLayout();
    QLabel *lblSubscribeTopic = new QLabel("订阅主题:", this);
    m_leSubscribeTopic = new QLineEdit(this);
    m_leSubscribeTopic->setPlaceholderText("如:qt/mqtt/demo/sub 或 qt/mqtt/#");
    m_leSubscribeTopic->setText("qt/mqtt/demo/#"); // 默认通配符主题,方便测试
    subscribeTopicLayout->addWidget(lblSubscribeTopic);
    subscribeTopicLayout->addWidget(m_leSubscribeTopic);
    subscribeLayout->addLayout(subscribeTopicLayout);

    // 2. 订阅QoS等级选择布局
    QHBoxLayout *subscribeQosLayout = new QHBoxLayout();
    QLabel *lblSubscribeQos = new QLabel("QoS等级:", this);
    m_cbxSubscribeQos = new QComboBox(this);
    // 添加三个QoS等级选项,与发布区一致
    m_cbxSubscribeQos->addItem("QoS 0(最多一次)", 0);
    m_cbxSubscribeQos->addItem("QoS 1(至少一次)", 1);
    m_cbxSubscribeQos->addItem("QoS 2(恰好一次)", 2);
    subscribeQosLayout->addWidget(lblSubscribeQos);
    subscribeQosLayout->addWidget(m_cbxSubscribeQos);
    subscribeLayout->addLayout(subscribeQosLayout);

    // 3. 订阅/取消订阅按钮布局
    QHBoxLayout *subscribeBtnLayout = new QHBoxLayout();
    m_btnSubscribe = new QPushButton("订阅主题", this);
    m_btnUnsubscribe = new QPushButton("取消订阅", this);
    // 初始状态:未连接,订阅和取消订阅按钮均不可用
    m_btnSubscribe->setEnabled(false);
    m_btnUnsubscribe->setEnabled(false);
    subscribeBtnLayout->addWidget(m_btnSubscribe);
    subscribeBtnLayout->addWidget(m_btnUnsubscribe);
    subscribeLayout->addLayout(subscribeBtnLayout);

    // 将订阅区添加到主布局
    mainLayout->addWidget(subscribeGroup);

    // 底部:日志区,占满主布局底部,用GroupBox分组
    QGroupBox *logGroup = new QGroupBox("通信日志", this);
    QVBoxLayout *logLayout = new QVBoxLayout(logGroup);
    m_teLog = new QTextEdit(this);
    m_teLog->setReadOnly(true); // 日志区只读,禁止编辑
    m_teLog->setStyleSheet("background-color: #f8f8f8;"); // 设置浅色背景,提升可读性
    logLayout->addWidget(m_teLog);

    // 将日志区添加到主布局,设置占比,确保底部铺满
    mainLayout->addStretch(); // 填充空白,使左侧、中间、右侧区域对齐
    QVBoxLayout *rightLayout = new QVBoxLayout();
    rightLayout->addWidget(logGroup);
    mainLayout->addLayout(rightLayout, 1); // 占1份空间,确保日志区铺满底部

    // 绑定所有按钮的点击信号与对应的槽函数
    connect(m_btnConnect, &QPushButton::clicked, this, &MqttClientDemo::onConnectBtnClicked);
    connect(m_btnDisconnect, &QPushButton::clicked, this, &MqttClientDemo::onDisconnectBtnClicked);
    connect(m_btnPublish, &QPushButton::clicked, this, &MqttClientDemo::onPublishBtnClicked);
    connect(m_btnSubscribe, &QPushButton::clicked, this, &MqttClientDemo::onSubscribeBtnClicked);
    connect(m_btnUnsubscribe, &QPushButton::clicked, this, &MqttClientDemo::onUnsubscribeBtnClicked);
}

// 初始化MQTT客户端:创建对象、初始化重连定时器、绑定核心信号槽
void MqttClientDemo::initMqttClient()
{
    // 创建MQTT客户端对象,父对象设为当前窗口,便于自动释放
    m_mqttClient = new QMqttClient(this);
    // 创建断线重连定时器,设置循环触发
    m_reconnectTimer = new QTimer(this);
    m_reconnectTimer->setInterval(m_reconnectInterval);
    m_reconnectTimer->setSingleShot(false); // 非单次触发,循环尝试重连
    // 绑定重连定时器的超时信号,触发重连逻辑
    connect(m_reconnectTimer, &QTimer::timeout, this, &MqttClientDemo::onReconnectTimerTimeout);

    // 绑定MQTT客户端的核心信号与槽函数,处理连接状态和错误
    connect(m_mqttClient, &QMqttClient::connected, this, &MqttClientDemo::onConnected);
    connect(m_mqttClient, &QMqttClient::disconnected, this, &MqttClientDemo::onDisconnected);
    connect(m_mqttClient, &QMqttClient::connectionStateChanged, this, &MqttClientDemo::onConnectionStateChanged);
    connect(m_mqttClient, &QMqttClient::errorOccurred, this, &MqttClientDemo::onErrorOccurred);

    // 添加初始化日志,提示用户MQTT客户端已准备就绪
    addLog("MQTT客户端初始化完成,可配置Broker参数并连接");
}

// 添加日志:格式化时间,区分普通日志和错误日志,自动滚动到最新条目
void MqttClientDemo::addLog(const QString &log, bool isError)
{
    // 格式化日志:当前时间 + 日志内容,时间格式为yyyy-MM-dd HH:mm:ss
    QString timeStr = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss");
    QString logStr = QString("[%1] %2").arg(timeStr).arg(log);
    // 错误日志标红显示,普通日志默认颜色
    if (isError) {
        m_teLog->append(QString("<font color='red'>%1</font>").arg(logStr));
    } else {
        m_teLog->append(logStr);
    }
    // 自动滚动到最后一行,确保用户能看到最新日志
    QTextCursor cursor = m_teLog->textCursor();
    cursor.movePosition(QTextCursor::End);
    m_teLog->setTextCursor(cursor);
}

// 设置遗嘱消息:配置客户端异常断开时,Broker发布的离线通知
void MqttClientDemo::setWillMessage()
{
    // 创建连接属性对象,用于配置遗嘱消息和会话参数
    QMqttConnectionProperties properties;
    // 配置遗嘱消息的主题(设备离线状态通知主题)
    QMqttTopicName willTopic("qt/mqtt/demo/will");
    // 配置遗嘱消息内容,包含当前客户端ID,便于区分哪个设备离线
    QString willMsg = QString("Client %1 is offline").arg(m_leClientId->text());
    // 配置遗嘱消息参数:QoS设为1(确保送达),保留消息设为true(新订阅者可获取)
    properties.setWillTopic(willTopic);
    properties.setWillMessage(willMsg.toUtf8());
    properties.setWillQoS(1);
    properties.setWillRetain(true);
    // 配置清理会话:设为false,重连后保留订阅关系和未完成消息
    properties.setCleanSession(false);
    // 配置会话过期时间:300秒(5分钟),未重连则清理会话
    properties.setSessionExpiryInterval(300);
    // 将配置应用到MQTT客户端
    m_mqttClient->setConnectionProperties(properties);
    addLog("遗嘱消息配置完成,异常离线时将自动通知");
}

// 连接参数校验:校验Broker IP、端口、客户端ID是否合法
bool MqttClientDemo::checkConnectParams()
{
    if (m_leHostname->text().isEmpty()) {
        addLog("连接失败:Broker IP不能为空", true);
        return false;
    }
    if (m_lePort->text().isEmpty() || !m_lePort->text().toUInt()) {
        addLog("连接失败:请输入合法的端口号(数字)", true);
        return false;
    }
    if (m_leClientId->text().isEmpty()) {
        addLog("连接失败:客户端ID不能为空", true);
        return false;
    }
    return true;
}

// 发布参数校验:校验发布主题、消息内容是否合法
bool MqttClientDemo::checkPublishParams()
{
    if (m_lePublishTopic->text().isEmpty()) {
        addLog("发布失败:发布主题不能为空", true);
        return false;
    }
    if (m_tePublishMsg->toPlainText().isEmpty()) {
        addLog("发布失败:消息内容不能为空", true);
        return false;
    }
    // 校验发布主题是否包含通配符(发布者不能使用通配符)
    if (m_lePublishTopic->text().contains("+") || m_lePublishTopic->text().contains("#")) {
        addLog("发布失败:发布主题不能包含通配符(+/#)", true);
        return false;
    }
    return true;
}

// 订阅参数校验:校验订阅主题是否合法
bool MqttClientDemo::checkSubscribeParams()
{
    if (m_leSubscribeTopic->text().isEmpty()) {
        addLog("订阅失败:订阅主题不能为空", true);
        return false;
    }
    // 校验通配符格式(#只能在主题末尾)
    if (m_leSubscribeTopic->text().contains("#") && !m_leSubscribeTopic->text().endsWith("#")) {
        addLog("订阅失败:通配符#只能放在主题末尾", true);
        return false;
    }
    return true;
}

// 连接Broker按钮点击槽函数
void MqttClientDemo::onConnectBtnClicked()
{
    // 先校验连接参数
    if (!checkConnectParams()) {
        return;
    }
    // 配置连接参数
    m_mqttClient->setHostname(m_leHostname->text());
    m_mqttClient->setPort(m_lePort->text().toUInt());
    m_mqttClient->setClientId(m_leClientId->text());
    // 若填写了用户名/密码,设置认证信息
    if (!m_leUsername->text().isEmpty()) {
        m_mqttClient->setUsername(m_leUsername->text());
        m_mqttClient->setPassword(m_lePassword->text());
    } else {
        // 清空密码(避免残留之前的密码)
        m_mqttClient->setPassword("");
    }
    // 配置遗嘱消息
    setWillMessage();
    // 发起连接(异步操作)
    m_mqttClient->connectToHost();
    addLog("正在连接Broker...");
}

// 断开连接按钮点击槽函数
void MqttClientDemo::onDisconnectBtnClicked()
{
    if (m_mqttClient->state() != QMqttClient::Connected) {
        addLog("无需断开:当前未连接到Broker", true);
        return;
    }
    // 主动断开连接,会触发disconnected信号
    m_mqttClient->disconnectFromHost();
    // 停止重连定时器
    if (m_reconnectTimer->isActive()) {
        m_reconnectTimer->stop();
    }
    addLog("正在断开与Broker的连接...");
}

// 发布消息按钮点击槽函数
void MqttClientDemo::onPublishBtnClicked()
{
    // 校验当前是否已连接
    if (m_mqttClient->state() != QMqttClient::Connected) {
        addLog("发布失败:当前未连接到Broker", true);
        return;
    }
    // 校验发布参数
    if (!checkPublishParams()) {
        return;
    }
    // 获取发布参数
    QMqttTopicName topic(m_lePublishTopic->text());
    QByteArray msg = m_tePublishMsg->toPlainText().toUtf8();
    quint8 qos = m_cbxPublishQos->currentData().toUInt();
    bool retain = m_chkRetain->isChecked();
    // 发布消息,获取消息ID(用于QoS 1/2的消息追踪)
    quint16 msgId = m_mqttClient->publish(topic, msg, qos, retain);
    addLog(QString("消息发布成功,消息ID:%1,主题:%2,QoS:%3,保留消息:%4")
           .arg(msgId)
           .arg(topic.name())
           .arg(qos)
           .arg(retain ? "是" : "否"));
    addLog(QString("发布内容:%1").arg(m_tePublishMsg->toPlainText()));
}

// 订阅主题按钮点击槽函数
void MqttClientDemo::onSubscribeBtnClicked()
{
    // 校验当前是否已连接
    if (m_mqttClient->state() != QMqttClient::Connected) {
        addLog("订阅失败:当前未连接到Broker", true);
        return;
    }
    // 校验订阅参数
    if (!checkSubscribeParams()) {
        return;
    }
    QString topicStr = m_leSubscribeTopic->text();
    // 校验是否已订阅该主题(避免重复订阅)
    if (m_subscriptions.contains(topicStr)) {
        addLog(QString("订阅失败:已订阅主题【%1】").arg(topicStr), true);
        return;
    }
    // 获取订阅QoS等级
    quint8 qos = m_cbxSubscribeQos->currentData().toUInt();
    QMqttTopicFilter filter(topicStr);
    // 订阅主题,获取订阅实例
    QMqttSubscription *sub = m_mqttClient->subscribe(filter, qos);
    if (!sub) {
        addLog(QString("订阅失败:主题【%1】订阅失败").arg(topicStr), true);
        return;
    }
    // 绑定消息接收信号,接收该主题的消息
    connect(sub, &QMqttSubscription::messageReceived, this, &MqttClientDemo::onMessageReceived);
    // 存储订阅实例,用于后续取消订阅
    m_subscriptions.insert(topicStr, sub);
    addLog(QString("订阅成功,主题:%1,QoS:%2").arg(topicStr).arg(qos));
    // 启用取消订阅按钮
    m_btnUnsubscribe->setEnabled(true);
}

// 取消订阅按钮点击槽函数
void MqttClientDemo::onUnsubscribeBtnClicked()
{
    // 校验当前是否已连接
    if (m_mqttClient->state() != QMqttClient::Connected) {
        addLog("取消订阅失败:当前未连接到Broker", true);
        return;
    }
    QString topicStr = m_leSubscribeTopic->text();
    // 校验是否已订阅该主题
    if (!m_subscriptions.contains(topicStr)) {
        addLog(QString("取消订阅失败:未订阅主题【%1】").arg(topicStr), true);
        return;
    }
    // 取消订阅
    m_mqttClient->unsubscribe(QMqttTopicFilter(topicStr));
    // 断开消息接收信号,释放订阅实例
    QMqttSubscription *sub = m_subscriptions.take(topicStr);
    disconnect(sub, &QMqttSubscription::messageReceived, this, &MqttClientDemo::onMessageReceived);
    delete sub;
    addLog(QString("取消订阅成功,主题:%1").arg(topicStr));
    // 若没有已订阅的主题,禁用取消订阅按钮
    if (m_subscriptions.isEmpty()) {
        m_btnUnsubscribe->setEnabled(false);
    }
}

// 连接成功回调槽函数
void MqttClientDemo::onConnected()
{
    addLog("连接成功:已成功连接到Broker");
    // 更新界面状态:禁用连接按钮,启用其他操作按钮
    m_btnConnect->setEnabled(false);
    m_btnDisconnect->setEnabled(true);
    m_btnPublish->setEnabled(true);
    m_btnSubscribe->setEnabled(true);
    // 若有已订阅的主题,启用取消订阅按钮
    if (!m_subscriptions.isEmpty()) {
        m_btnUnsubscribe->setEnabled(true);
    }
    // 更新连接状态标签(绿色)
    m_lblState->setText("已连接");
    m_lblState->setStyleSheet("color: green; font-weight: bold;");
    // 停止重连定时器(若之前在重连)
    if (m_reconnectTimer->isActive()) {
        m_reconnectTimer->stop();
    }
}

// 断开连接回调槽函数
void MqttClientDemo::onDisconnected()
{
    addLog("已断开与Broker的连接");
    // 更新界面状态:启用连接按钮,禁用其他操作按钮
    m_btnConnect->setEnabled(true);
    m_btnDisconnect->setEnabled(false);
    m_btnPublish->setEnabled(false);
    m_btnSubscribe->setEnabled(false);
    m_btnUnsubscribe->setEnabled(false);
    // 更新连接状态标签(红色)
    m_lblState->setText("未连接");
    m_lblState->setStyleSheet("color: red; font-weight: bold;");
    // 开启自动重连(若启用)
    if (m_autoReconnect) {
        addLog(QString("将在%1毫秒后尝试重连").arg(m_reconnectInterval));
        m_reconnectTimer->start();
    }
}

// 连接状态变化回调槽函数
void MqttClientDemo::onConnectionStateChanged(QMqttClient::ClientState state)
{
    switch (state) {
        case QMqttClient::Disconnected:
            addLog("连接状态:未连接");
            break;
        case QMqttClient::Connecting:
            addLog("连接状态:连接中");
            break;
        case QMqttClient::Connected:
            addLog("连接状态:已连接");
            break;
        default:
            addLog(QString("连接状态:未知状态(%1)").arg(state));
            break;
    }
}

// 错误回调槽函数:处理连接和通信过程中的错误
void MqttClientDemo::onErrorOccurred(QMqttClient::ClientError error)
{
    QString errorMsg;
    switch (error) {
        case QMqttClient::NoError:
            errorMsg = "无错误";
            break;
        case QMqttClient::InvalidProtocolVersion:
            errorMsg = "错误:MQTT协议版本无效";
            break;
        case QMqttClient::IdRejected:
            errorMsg = "错误:客户端ID被拒绝(可能重复)";
            break;
        case QMqttClient::ServerUnavailable:
            errorMsg = "错误:Broker不可用(IP或端口错误)";
            break;
        case QMqttClient::BadUsernameOrPassword:
            errorMsg = "错误:用户名或密码错误";
            break;
        case QMqttClient::NotAuthorized:
            errorMsg = "错误:未授权(Broker拒绝连接)";
            break;
        default:
            errorMsg = QString("错误:未知错误(%1)").arg(error);
            break;
    }
    // 添加错误日志(标红)
    addLog(errorMsg, true);
}

// 接收订阅消息的槽函数
void MqttClientDemo::onMessageReceived(QMqttMessage msg)
{
    // 获取消息相关属性
    QString topic = msg.topic().name();
    QString content = msg.payload().data();
    quint8 qos = msg.qos();
    bool isRetain = msg.isRetained();
    QDateTime time = msg.timestamp();
    // 添加接收日志
    addLog(QString("收到消息,主题:%1,QoS:%2,保留消息:%3,时间:%4")
           .arg(topic)
           .arg(qos)
           .arg(isRetain ? "是" : "否")
           .arg(time.toString("yyyy-MM-dd HH:mm:ss")));
    addLog(QString("消息内容:%1").arg(content));
}

// 断线重连的槽函数(定时器超时触发)
void MqttClientDemo::onReconnectTimerTimeout()
{
    if (m_mqttClient->state() == QMqttClient::Connected) {
        m_reconnectTimer->stop();
        return;
    }
    // 重新配置连接参数(避免参数变更后未更新)
    m_mqttClient->setHostname(m_leHostname->text());
    m_mqttClient->setPort(m_lePort->text().toUInt());
    m_mqttClient->setClientId(m_leClientId->text());
    if (!m_leUsername->text().isEmpty()) {
        m_mqttClient->setUsername(m_leUsername->text());
        m_mqttClient->setPassword(m_lePassword->text());
    }
    // 发起重连
    addLog("尝试重连Broker...");
    m_mqttClient->connectToHost();
}
(3)主函数文件(main.cpp)

主函数用于启动应用程序,创建核心窗口实例,是Qt应用的入口,代码没什么说的,就完成基础的应用初始化和窗口显示。

cpp 复制代码
#include <QApplication>
#include "mqttclientdemo.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    // 创建MQTT通信Demo窗口实例
    MqttClientDemo w;
    // 显示窗口
    w.show();
    // 启动应用程序事件循环
    return a.exec();
}

3.3 工程配置与测试步骤

完成代码编写后,需正确配置工程并执行测试,确保Demo能正常运行。

(1)工程配置

使用qmake构建工程,在.pro文件中添加以下配置,引入核心模块和MQTT模块:

cpp 复制代码
QT += core gui widgets mqtt  # 核心模块+MQTT模块
TEMPLATE = app  # 生成应用程序
SOURCES += main.cpp \
           mqttclientdemo.cpp  # 源文件
HEADERS  += mqttclientdemo.h  # 头文件
(2)测试步骤

测试前需准备MQTT Broker,推荐使用Eclipse Mosquitto(轻量开源,适合测试),步骤如下:

  1. 安装Mosquitto:Windows系统可从官网下载安装包,Linux系统执行"sudo apt-get install mosquitto mosquitto-clients";
  2. 启动Mosquitto:默认无需配置,直接启动服务即可。Windows系统可在服务中启动"Mosquitto Broker",Linux系统执行"mosquitto -v";
  3. 编译Qt工程:使用Qt Creator打开工程,编译生成可执行文件;
  4. 运行Demo:点击"运行"按钮,启动Demo窗口,配置Broker参数(默认IP为127.0.0.1,端口1883,客户端ID自动生成,无需填写用户名/密码);
  5. 测试连接:Demo上点击"连接Broker",Demo日志区显示"连接成功",说明与Broker连接成功;
  6. 测试订阅与发布:在订阅区填写主题(如"qt/mqtt/demo/#"),点击"订阅主题";在发布区填写相同前缀的主题(如"qt/mqtt/demo/pub"),输入消息内容,点击"发布消息",日志区会显示"收到消息",说明通信正常;
  7. 测试断线重连:关闭Mosquitto服务,Demo会自动尝试重连;重新启动Mosquitto服务,Demo会重连成功,恢复通信;
  8. 测试遗嘱消息:启动Demo并连接Broker,直接关闭Demo窗口(模拟异常离线),使用Mosquitto客户端订阅"qt/mqtt/demo/will"主题,会收到离线通知消息,说明遗嘱消息生效。

3.4 常见问题排查

测试过程中若出现无法连接、无法发布/接收消息等问题,就得结合日志一一进行排查了,下面说一些可能的坑和问题点:

    1. 无法连接Broker:检查Broker IP和端口是否正确,确保Mosquitto服务已启动;若提示"客户端ID被拒绝",修改客户端ID(确保唯一);若提示"用户名密码错误",检查Broker是否开启认证,填写正确的认证信息;
    1. 无法发布消息:检查是否已连接Broker,发布主题是否包含通配符(发布者禁止使用),消息内容是否为空;
    1. 无法接收消息:检查订阅主题与发布主题是否匹配(或通配符是否正确),订阅是否成功,是否绑定消息接收信号;
    1. 断线重连失败:检查重连定时器是否启动,连接参数是否正确(避免重连时使用旧参数);
    1. 遗嘱消息未触发:确保是"异常离线"(直接关闭程序、断电),正常断开连接(点击"断开连接")不会触发遗嘱消息;检查遗嘱消息的主题和QoS配置是否正确。

四、Qt MQTT开发注意事项

完成基础Demo后,可能结合实际项目中的场景进行要进行更复杂的处理,同时注意规避开发中的常见陷阱,提升通信的稳定性和安全性。

4.1 连接相关注意点

(1)客户端 ID 必须唯一

Broker 通过客户端 ID 唯一标识每个客户端,若两个客户端使用相同的 ID 连接,Broker 会断开先连接的客户端(触发IdRejected错误)。

解决方案:

不要硬编码客户端 ID,而是生成随机 ID(如 Demo 中用QtMqttDemo_xxxx);

若需要固定 ID(如设备唯一标识),确保每个设备的 ID 不重复。

(2)清理会话(Clean Session)的选择

Clean Session = true:断开连接后,Broker 删除所有订阅关系、未完成的消息,重连后需重新订阅;

Clean Session = false:Broker 保留会话,重连后恢复订阅、推送未完成的消息。

建议:

临时客户端(如手机 APP):设为true,避免 Broker 存储过多无效会话;

持久化设备(如传感器):设为false,保证重连后不丢失消息。

(3)处理连接超时

Qt 的connectToHost()是异步操作,不会阻塞主线程,若 Broker 无响应,客户端会一直处于 "连接中" 状态。

解决方案:

添加连接超时定时器,若超过指定时间(如 10 秒)未连接成功,主动停止连接并提示用户;

代码如下:

cpp 复制代码
QTimer *connectTimeoutTimer = new QTimer(this);
connectTimeoutTimer->setSingleShot(true);
connect(connectTimeoutTimer, &QTimer::timeout, this, [=]() {
    if (m_mqttClient->state() == QMqttClient::Connecting) {
        m_mqttClient->disconnectFromHost();
        addLog("连接超时!请检查Broker地址或网络", true);
    }
});
connectTimeoutTimer->start(10000); // 10秒超时
(4)SSL/TLS 加密连接

若需要加密通信(如公网传输),需配置 SSL/TLS:

cpp 复制代码
// 配置SSL
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
sslConfig.setProtocol(QSsl::TlsV1_2OrLater);
m_mqttClient->setSslConfiguration(sslConfig);
// 设置SSL端口(8883)
m_mqttClient->setPort(8883);

注意:需确保 Broker 已配置 SSL 证书,Qt 客户端需信任该证书(否则会触发TransportError)。

4.2 订阅相关注意点

(1)主题格式校验

订阅 / 发布的主题必须符合 MQTT 规范:

  • 不能包含空格、通配符(发布时);
  • 层级分隔符只能用/;
  • 长度不超过 65535 字符。

解决方案:

在订阅 / 发布前校验主题格式,避免无效操作;

示例代码

cpp 复制代码
bool isValidTopic(const QString &topic) {
    if (topic.isEmpty() || topic.contains(" ")) {
        return false;
    }
    // 发布主题不能包含通配符
    if (topic.contains("+") || topic.contains("#")) {
        return false;
    }
    return true;
}
(2)避免重复订阅

多次订阅同一主题会创建多个QMqttSubscription实例,导致重复接收消息。

解决方案:

维护订阅列表(如 Demo 中的m_subscriptions),订阅前检查是否已订阅该主题;

若需修改 QoS,先取消订阅再重新订阅。

(3)处理订阅失败

subscribe()返回nullptr表示订阅失败,常见原因:

  • 未连接 Broker;
  • 主题格式错误;
  • Broker 拒绝订阅(权限不足)。

解决方案:

  • 订阅前检查连接状态;
  • 捕获订阅失败的情况,给出明确的日志提示。

4.3 发布相关注意点

(1)QoS 等级的选择

QoS 等级越高,可靠性越好,但传输开销越大:

  • 实时数据(如传感器数据):QoS 0;
  • 控制指令(如设备重启):QoS 1;
  • 关键数据(如金融交易):QoS 2。

注意:发布者和订阅者的 QoS 取最小值(如发布者 QoS 2,订阅者 QoS 1,实际按 QoS 1 传输)。

(2)保留消息的使用场景

保留消息适合 "需要获取最新状态" 的场景(如设备当前温度),但滥用会导致 Broker 存储过多无效消息。

注意:仅对 "状态类消息" 设置保留;若不需要保留消息,发布时设为false;

可发布空消息(QByteArray())到主题,清除该主题的保留消息。

(3)消息大小限制

MQTT 协议本身没有消息大小限制,但 Broker 通常会限制(如 Mosquitto 默认最大 1MB)。

解决方案:

  • 拆分大消息为多个小消息,或使用压缩算法(如 zlib);
  • 发布前检查消息大小,避免超过 Broker 限制。

4.4 异常处理注意点

(1)断线重连的实现

网络波动、Broker 重启等会导致连接断开,需实现断线重连:

  • 使用定时器循环尝试重连(如 Demo 中的m_reconnectTimer);
  • 重连间隔逐渐增加(如 5 秒→10 秒→30 秒),避免频繁重连占用资源;
    示例代码
cpp 复制代码
// 动态调整重连间隔
int m_reconnectCount = 0;
void onReconnectTimerTimeout() {
    if (m_mqttClient->state() != QMqttClient::Connected) {
        m_reconnectCount++;
        int interval = qMin(5000 * m_reconnectCount, 30000); // 最大30秒
        m_reconnectTimer->setInterval(interval);
        addLog(QString("第%1次重连...").arg(m_reconnectCount));
        m_mqttClient->connectToHost();
    } else {
        m_reconnectCount = 0; // 重连成功,重置计数
        m_reconnectTimer->setInterval(5000);
    }
}
(2)错误码的处理

errorOccurred()信号返回的错误码是排查问题的关键,需针对不同错误码给出解决方案:

  • ServerUnavailable:检查 Broker IP / 端口、是否启动;
  • BadUsernameOrPassword:检查用户名 / 密码;
  • IdRejected:检查客户端 ID 是否重复;
  • TransportError:检查网络、SSL 配置。
(3)资源释放

程序退出或断开连接时,需正确释放资源:

  • 断开 MQTT 连接;
  • 取消所有订阅;
  • 停止重连定时器;
  • 避免内存泄漏(如 Demo 的析构函数)。

4.5 性能优化注意点

(1)避免在消息接收槽函数中执行耗时操作

messageReceived()信号在 Qt 的主线程触发,若槽函数中执行耗时操作(如文件写入、复杂计算),会导致界面卡顿。

解决方案:

  • 将耗时操作放到子线程;
  • 使用QtConcurrent异步执行;
cpp 复制代码
void onMessageReceived(QMqttMessage msg) {
    // 主线程仅记录日志,耗时操作放到子线程
    addLog("收到消息,开始处理...");
    QtConcurrent::run([=]() {
        // 耗时操作:解析消息、写入数据库等
        processMessage(msg);
    });
}
(2)批量处理消息

若接收消息频率极高(如每秒数百条),可批量处理消息,减少 UI 刷新次数:

  • 使用队列缓存消息,每隔 100ms 批量刷新到界面;
  • 避免每条消息都触发 UI 更新。

五、总结

MQTT 协议凭借 "轻量、可靠、灵活" 的特性,成为物联网通信的事实标准,而 Qt中的 MQTT 模块则为跨平台应用开发提供了便捷的封装。MQTT 核心架构:由发布者、订阅者、Broker 组成,基于 "发布 / 订阅" 模式,通过主题匹配消息,QoS 等级决定消息可靠性;而且Qt中MQTT 核心类:QMqttClient(核心通信)、QMqttSubscription(订阅实例)、QMqttMessage(消息载体),接口语义清晰,容易上手;可以快速调用MQTT 通信功能,还可以结合 Qt 的多线程、数据库、网络模块,实现更复杂的物联网应用(如设备监控、远程控制、数据存储等)。

相关推荐
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
老梁agent1 天前
MCP 协议实战:用标准化方式让 Agent 调用工业工具
物联网·agent·mcp
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
老梁agent4 天前
一个 Agent 不够用?工业场景下的多 Agent 路由模式实战
物联网·agent
老梁agent5 天前
从 0 到 22 篇:工业 Agent 的六大设计原则
物联网·agent
Quz6 天前
QML Hello World 入门示例
qt
老梁agent7 天前
Agent 如何看懂时序数据?时间序列查询的 Tool 设计模式
物联网·agent
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt