前言
在物联网(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通信全流程,满足基础通信,同时便于扩展,具体如下:
- 配置Broker的IP、端口、客户端ID、用户名/密码,灵活调整连接参数;
- 有个连接/断开Broker的按钮,实时显示连接状态,便于操作和排查;
- 可以发布消息到指定主题,可设置QoS等级、是否保留消息,满足不同的场景;
- 支持订阅指定主题(支持通配符),可设置QoS等级,实时接收并显示消息;
- 显示通信日志,包括连接状态、收发消息、错误信息等,便于排查问题;
- 实现断线重连功能,网络波动或Broker重启后,自动尝试重连,保证通信稳定性;
- 配置遗嘱消息,客户端异常断开时,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(轻量开源,适合测试),步骤如下:
- 安装Mosquitto:Windows系统可从官网下载安装包,Linux系统执行"sudo apt-get install mosquitto mosquitto-clients";
- 启动Mosquitto:默认无需配置,直接启动服务即可。Windows系统可在服务中启动"Mosquitto Broker",Linux系统执行"mosquitto -v";
- 编译Qt工程:使用Qt Creator打开工程,编译生成可执行文件;
- 运行Demo:点击"运行"按钮,启动Demo窗口,配置Broker参数(默认IP为127.0.0.1,端口1883,客户端ID自动生成,无需填写用户名/密码);
- 测试连接:Demo上点击"连接Broker",Demo日志区显示"连接成功",说明与Broker连接成功;
- 测试订阅与发布:在订阅区填写主题(如"qt/mqtt/demo/#"),点击"订阅主题";在发布区填写相同前缀的主题(如"qt/mqtt/demo/pub"),输入消息内容,点击"发布消息",日志区会显示"收到消息",说明通信正常;
- 测试断线重连:关闭Mosquitto服务,Demo会自动尝试重连;重新启动Mosquitto服务,Demo会重连成功,恢复通信;
- 测试遗嘱消息:启动Demo并连接Broker,直接关闭Demo窗口(模拟异常离线),使用Mosquitto客户端订阅"qt/mqtt/demo/will"主题,会收到离线通知消息,说明遗嘱消息生效。
3.4 常见问题排查
测试过程中若出现无法连接、无法发布/接收消息等问题,就得结合日志一一进行排查了,下面说一些可能的坑和问题点:
-
- 无法连接Broker:检查Broker IP和端口是否正确,确保Mosquitto服务已启动;若提示"客户端ID被拒绝",修改客户端ID(确保唯一);若提示"用户名密码错误",检查Broker是否开启认证,填写正确的认证信息;
-
- 无法发布消息:检查是否已连接Broker,发布主题是否包含通配符(发布者禁止使用),消息内容是否为空;
-
- 无法接收消息:检查订阅主题与发布主题是否匹配(或通配符是否正确),订阅是否成功,是否绑定消息接收信号;
-
- 断线重连失败:检查重连定时器是否启动,连接参数是否正确(避免重连时使用旧参数);
-
- 遗嘱消息未触发:确保是"异常离线"(直接关闭程序、断电),正常断开连接(点击"断开连接")不会触发遗嘱消息;检查遗嘱消息的主题和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 的多线程、数据库、网络模块,实现更复杂的物联网应用(如设备监控、远程控制、数据存储等)。