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 的多线程、数据库、网络模块,实现更复杂的物联网应用(如设备监控、远程控制、数据存储等)。

相关推荐
micro_xx2 小时前
借助Matlab有限元工具pde进行静态结构有限元分析
开发语言·matlab
代码无bug抓狂人2 小时前
C语言之可分解的正整数(蓝桥杯省B)
c语言·开发语言·算法
Cher ~2 小时前
常见C++编译器套件
开发语言·c++
上海合宙LuatOS2 小时前
LuatOS ——Modbus RTU 通信模式
java·linux·服务器·开发语言·网络·嵌入式硬件·物联网
国产化创客2 小时前
ESPHome 核心原理+全流程开发与集成
物联网·智能家居·智能硬件
xyq20242 小时前
《jEasyUI 启用行内编辑》
开发语言
野生技术架构师2 小时前
Java 21虚拟线程 vs Kotlin协程:高并发编程模型的终极对决与选型思考
java·开发语言·kotlin
LYOBOYI1232 小时前
qml的布局策略
c++·qt
言之。2 小时前
Kotlin快速入门
android·开发语言·kotlin