简介这是一份面向物联网与嵌入式开发者的 Qt 集成 MQTT 实战工程包基于 Qt 5.6.1 与 minGW 4.9.2 编译环境搭建演示如何把第三方 MQTT 客户端库接入 Qt 应用并让应用与阿里云物联网平台完成设备接入、消息发布和订阅帮助初学者打通从本地编译到云端通信的链路。压缩包共 28 个文件约 1.91MB以 20 个 .h 头文件与 2 个 .cpp 源文件承载核心实现辅以 .ui 界面文件和 .pro 工程配置并附带 2 个 .a 静态库和 2 个 .dll 动态库可直接链接调用免去另行编译依赖。已有 1701 人学习下载。读者能获得一套可运行的示例工程与清晰的分层结构理解 MQTT 网络层、客户端与消息帧的组织方式掌握阿里云平台 DeviceID 与密钥认证的配置要点适合有一定 C 基础、希望切入物联网通信的开发者对照实践。1. 当现场只剩 Qt 5.6.1 和 MinGW 4.9.2MQTT 得自己搬一个做工业采集器的开发者拿着压缩包来找我现场那台工控机锁死在 Qt 5.6.1 MinGW 4.9.2 32 位这套老工具链上客户又要求设备直连物联网平台上报数据。Qt 官方直到 5.12 才把 MQTT 模块正式纳入发行版5.6.1 的安装目录里翻遍也找不到 QtMqtt 的影子pro 文件里敲下QT mqttqmake 直接抛一句 Unknown module。这份 QT5.6.1MQTTminGW4.9.2 的资源包干的正是把 MQTT 协议栈回移到 5.6 时代并跑通云平台接入这件事一套能在老工具链上编译通过的 qtmqtt 源码、一个封装好三元组鉴权和 Topic 收发的示例工程、外加一份标出坑位的配置。适合手上被老版本 Qt 绑死、又必须让设备接入物联网平台的上位机和嵌入式开发者。2. 工程结构拆解压缩包解开后每个目录各管什么先别急着编译把目录摊开看一眼能省掉后面一半的返工。这类回移工程的结构通常是三段式第三方协议库、业务应用、公共工具彼此靠 pro 文件的 INCLUDEPATH 和 LIBS 串起来而不是靠什么包管理器。2.1 顶层布局3rdparty、app、common 三段式解开压缩包后大致是这么一棵树路径作用是否要改3rdparty/qtmqtt/MQTT 3.1.1 协议栈源码回移后的版本编译兼容性要改3rdparty/qtmqtt/src/mqtt/库的核心实现qmake 工程的真正入口改得最多app/业务 demo含界面、设备客户端封装二次开发主战场common/HMAC-SHA1、配置读取等公共代码按需补demo.pro顶层 subdirs 工程一般不动3rdparty目录单独拎出来是有讲究的协议库和业务代码分家以后换云平台、换协议栈版本只动这一层app里的业务逻辑不受牵连。我见过不少人图省事把 qtmqtt 的源文件直接拷进 app 目录编译是能过但等到要升版本或者同时维护两个产品线就只能靠 diff 手工合并血泪经验。common/hmacsha1.cpp这份文件看着不起眼却是连接云平台的命门。Qt 5.6 的QCryptographicHash只提供裸哈希没有 HMAC 封装签名算法得自己用 SHA1 拼出来后面第 4 章会完整贴一遍。2.2 qtmqtt 源码树的协议分层src/mqtt下面的文件按职责分四层理清了这个分层改代码时就知道该动哪儿src/mqtt/ ├── qmqttclient.h/.cpp # 对外门面连接、订阅、发布、信号 ├── qmqttconnection.h/.cpp # 传输层socket 读写、心跳、报文序号 ├── qmqttcontrolpacket.h/.cpp # 报文编解码CONNECT/PUBLISH/SUBSCRIBE 等 ├── qmqtttopicname.h/.cpp # Topic 值类型做合法性校验 ├── qmqtttopicfilter.h/.cpp # 带通配符的订阅过滤器 ├── qmqttsubscription.h/.cpp # 订阅句柄承载下行消息信号 ├── qmqttmessage.h/.cpp # 消息体topic payload qos retain └── mqtt.pro # 库的 qmake 工程门面层QMqttClient只做参数校验和状态机调度真正的 TCP 收发全压在QMqttConnection里。心跳保活、报文 ID 分配、重发队列都在QMqttConnection内部用QTimer和QQueue完成。这一层在 5.6 上改起来最疼因为它用了一批 5.7 之后才有的容器和随机数 API。值类型那四个类TopicName、TopicFilter、Message、Subscription都是轻量封装没有Q_OBJECT之外的重逻辑回移时基本不用动。判断一个文件要不要改看它 include 了什么就行。2.3 业务工程如何引用这个库app/app.pro里的引用方式决定了后面编译顺不顺写法有两种QT core gui network greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 # 方式一库已经 make install 进 Qt 安装目录直接声明模块 # QT mqtt # 方式二推荐不污染 Qt 安装目录用相对路径直连源码和产物 INCLUDEPATH $$PWD/../3rdparty/qtmqtt/src/mqtt DEPENDPATH $$PWD/../3rdparty/qtmqtt/src/mqtt LIBS -L$$PWD/../3rdparty/qtmqtt/lib -lQt5Mqtt SOURCES main.cpp \ deviceclient.cpp \ mainwindow.cpp \ ../common/hmacsha1.cpp HEADERS deviceclient.h \ mainwindow.h \ ../common/hmacsha1.hINCLUDEPATH让编译器找得到头文件LIBS的-L指库目录、-l指库名MinGW 下libQt5Mqtt.a或libQt5Mqtt.dll.a会按-lQt5Mqtt被解析出来。CONFIG c11不能省qtmqtt 源码里用了 lambda 和nullptrMinGW 4.9.2 默认标准是 gnu98不加这行会报一堆语法错。注意HEADERS里必须带上带Q_OBJECT的头文件qmake 靠它触发 moc漏一个就是链接期 vtable 未定义这个问题在第 5 章还会展开。3. 编译落地MinGW 4.9.2 下把 qtmqtt 编成可链接的库结构看清了接下来是最容易翻车的一步。老工具链编译新源码问题不在语法在 API 的有无。3.1 先对齐 qmake 与编译器版本打开 Qt 命令行不是系统的 cmd第一件事确认 qmake 指向的是哪套qmake -v正常输出类似QMake version 3.0 Using Qt version 5.6.1 in C:/Qt/Qt5.6.1/5.6/mingw49_32/lib看到mingw49_32就对了。如果输出的是 64 位路径或者另一个 Qt 版本说明环境变量串了编出来的库位数对不上链接时会报file format not recognized。这种错误信息看着吓人其实就是用错了终端。再确认编译器g --version应当是g (GCC) 4.9.2。MinGW 4.9.2 对 C11 的支持已经够用auto、lambda、右值引用、std::function都没问题唯一要留意的是它对部分 C14 特性不支持所以源码里遇到auto返回值推导C14的写法得改成显式返回类型。3.2 从新版本回移到 Qt 5.6 的兼容性改造qtmqtt 官方源码树的基线是 Qt 5.9 往上直接拿到 5.6.1 上编编译错误会集中在下面这几处。逐个改别看一条改一条先扫一遍心里有数。原始 API引入版本Qt 5.6 替代方案qAsConst()5.7显式const引用或 const_iteratorQOverloadT::of()5.7static_cast函数指针QRandomGenerator5.10qrand()qsrand()QDateTime::currentSecsSinceEpoch()5.8toMSecsSinceEpoch() / 1000QMetaObject::invokeMethod接 lambda5.10QTimer::singleShot(0, ...)转发信号重载的消解是最先撞上的// qmqttconnection.cpp // 原写法依赖 QOverloadQt 5.7 引入5.6 编译不过 // connect(m_socket, QOverloadQAbstractSocket::SocketError::of(QAbstractSocket::error), // this, QMqttConnection::onSocketError); // 回移写法QAbstractSocket::error 在 5.6 里是重载成员有同名 getter // 用 static_cast 明确挑出信号那个签名 connect(m_socket, static_castvoid (QAbstractSocket::*)(QAbstractSocket::SocketError)(QAbstractSocket::error), this, QMqttConnection::onSocketError, Qt::UniqueConnection);static_cast里那串void (QAbstractSocket::*)(...)是成员函数指针类型编译器靠它在一堆同名成员里选出信号版本。补一个Qt::UniqueConnection防止重连逻辑里重复连接导致槽函数被调多次这个坑我在现场踩过一个错误信号触发了三次重连。报文 ID 的生成要用老随机数// qmqttconnection.cpp分配 PUBLISH 报文标识符 // 原写法Qt 5.10 // quint16 id static_castquint16(QRandomGenerator::global()-bounded(1, 65535)); // 回移写法qrand 不播种的话每次启动序列完全一样 // 多台设备同时上线会撞 ID所以用毫秒时间戳播一次种 static bool s_seeded false; if (!s_seeded) { qsrand(static_castuint(QDateTime::currentMSecsSinceEpoch() 0xFFFFFFFF)); s_seeded true; } quint16 id static_castquint16(qrand() % 65535 1);s_seeded用函数内静态变量保证整个进程只播一次种。 0xFFFFFFFF是因为qsrand只吃 32 位直接截断在 64 位时间戳上行为不确定。时间戳获取也要换// 原写法Qt 5.8 // qint64 ts QDateTime::currentSecsSinceEpoch(); // 回移写法 qint64 ts QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000;toMSecsSinceEpoch()返回的是 UTC 毫秒数除以 1000 得到秒和currentSecsSinceEpoch()语义一致都是 UTC不受本地时区影响。这点很关键签名里的时间戳必须和平台服务端对齐用本地时间拼字符串就等着被拒。qAsConst的替换比较机械凡是for (const auto x : qAsConst(container))的地方改成显式 const 迭代或者先取一个 const 引用// 原写法 // for (const auto pending : qAsConst(m_pendingMessages)) { ... } // 回移写法 const QQueueQMqttMessage pending m_pendingMessages; for (int i 0; i pending.size(); i) { if (pending.at(i).id() id) return pending.at(i); }const QQueueQMqttMessage 这一句就把隐式共享容器的 detach 挡住了at()是 const 接口运行时不会拷贝底层数据。比起qAsConst写法丑一点效果一样。3.3 构建、安装与工程链接改完源码进库目录编译cd 3rdparty/qtmqtt qmake qtmqtt.pro CONFIGrelease CONFIG-debug_and_release mingw32-make -j4CONFIG-debug_and_release是为了只出 release 一份产物省一半编译时间和磁盘。调试期需要断点的话换成CONFIGdebug重编一遍即可两种产物库名不同可以共存。编译成功后在lib/下能看到libQt5Mqtt.a静态和libQt5Mqtt.dll.aQt5Mqtt.dll动态。默认是动态库mqtt.pro里没有显式写CONFIG staticlib就是这个结果。要不要make install取决于引用方式# 会往 Qt 安装目录写头文件和库需要管理员权限 mingw32-make install装完之后Qt 安装目录的mkspecs/modules/下才会出现qt_lib_mqtt.pri这时候QT mqtt才认识这个模块。注意这一步不是必须的很多现场机器没有管理员权限装不进去。这种情况就走 2.3 节里的方式二用INCLUDEPATHLIBS直连代价是每次换机器要手动确认库路径。链接顺序也有讲究。MinGW 的 ld 是从左到右解析符号LIBS里-lQt5Mqtt得排在使用它的目标文件后面。如果 pro 里同时有-lQt5Mqtt和-lQt5Network把 MQTT 放前面因为它依赖 network 里的 socket 符号。3.4 写个最小验证程序确认链路通别急着跑完整的业务 demo先写个二十行的最小程序确认库真的能链上、能建对象// main.cpp最小验证只验证库可用与对象可构造 #include QCoreApplication #include QMqttClient #include QDebug int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QMqttClient client; client.setHostname(QStringLiteral(127.0.0.1)); client.setPort(1883); client.setClientId(QStringLiteral(probe-001)); // 连不上没关系重点看有没有正常进入 Connecting 状态 QObject::connect(client, QMqttClient::stateChanged, [](QMqttClient::ClientState s) { qDebug() state - s; }); client.connectToHost(); return app.exec(); }setHostname故意填本机是为了区分「库链不上」和「服务器连不上」两类问题。程序能跑起来、打印出state - 1Connecting说明编译链接这一关过了剩下的都是网络和鉴权的事。如果这里直接崩在QMqttClient构造上十有八九是 moc 没生成回第 2 章检查HEADERS。4. 接入云平台三元组鉴权与 Topic 收发的完整链路库能用了接下来是业务里最实在的一段怎么让设备通过平台的鉴权连上去再把数据发对地方。4.1 设备三元组与 clientId 拼装主流云厂商的物联网平台普遍用三元组做设备身份ProductKey产品标识、DeviceName设备名、DeviceSecret设备密钥。这三个值在平台上创建设备时一次性给出DeviceSecret 只显示一次丢了只能重置。MQTT 的 CONNECT 报文里没有专门的「产品」字段平台的做法是把附加信息塞进 clientId、username、password 三个位置clientId 里带设备名、安全模式、签名方法、时间戳username 里带设备名和产品标识用连接password 里放签名结果clientId 的格式各平台略有差异但套路一致形如deviceName|securemode3,signmethodhmacsha1,timestamp秒级时间戳|拼接代码// deviceclient.cpp组装鉴权参数 struct AuthInfo { QString clientId; QString username; QString password; }; AuthInfo buildAuth(const QString productKey, const QString deviceName, const QString deviceSecret) { AuthInfo info; // 时间戳只取一次clientId 和签名串里必须是同一个值 const qint64 ts QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000; info.clientId QString(%1|securemode3,signmethodhmacsha1,timestamp%2|) .arg(deviceName) .arg(ts); info.username QString(%1%2).arg(deviceName, productKey); // 待签名串按参数名字典序拼接clientId deviceName productKey timestamp const QString content QString(clientId%1deviceName%2productKey%3timestamp%4) .arg(info.clientId, deviceName, productKey) .arg(ts); const QByteArray digest hmacSha1(deviceSecret.toUtf8(), content.toUtf8()); info.password QString::fromLatin1(digest.toHex()); return info; }ts只算一次并贯穿两处这是关键。分两次调用取时间跨秒时 clientId 里的时间戳和签名串里的不一致平台侧算出来的签名对不上直接拒连。字典序拼接待签名串是平台的硬约定不是随便排的。参数名按 ASCII 升序clientId、deviceName、productKey、timestamp值紧跟其后不带任何分隔符和引号。顺序错了签名必然错而错误提示往往只有一句「鉴权失败」排查起来非常难受。toUtf8()这两处也别省。DeviceSecret 一般由大小写字母和数字组成理论上toLatin1()结果一样但签名串里如果哪天塞进中文设备名Latin-1 会静默丢字节签名就错了。4.2 在 Qt 5.6 里手写 HMAC-SHA1 签名Qt 5.6 的QCryptographicHash只有Sha1、Md5这些裸哈希没有 HMAC。RFC 2104 定义的 HMAC 算法本身不复杂密钥超长先哈希然后补零到块长分别与 ipad0x36和 opad0x5c异或做两次哈希嵌套。// hmacsha1.cppRFC 2104 标准实现只依赖 QCryptographicHash #include hmacsha1.h #include QCryptographicHash QByteArray hmacSha1(const QByteArray key, const QByteArray message) { const int blockSize 64; // SHA1 的分组长度固定 64 字节 QByteArray k key; // 密钥超过块长时先做一次 SHA1 压缩短于块长时右侧补 0 if (k.size() blockSize) k QCryptographicHash::hash(k, QCryptographicHash::Sha1); k.resize(blockSize, \0); QByteArray iKeyPad(blockSize, \0); QByteArray oKeyPad(blockSize, \0); for (int i 0; i blockSize; i) { iKeyPad[i] static_castchar(k.at(i) ^ 0x36); oKeyPad[i] static_castchar(k.at(i) ^ 0x5c); } // 内层哈希ipad || message const QByteArray inner QCryptographicHash::hash(iKeyPad message, QCryptographicHash::Sha1); // 外层哈希opad || inner return QCryptographicHash::hash(oKeyPad inner, QCryptographicHash::Sha1); }blockSize 64是 SHA1 的规范值换成 SHA256 就是同一个数SHA256 分组也是 64 字节改哈希算法时这个常量不用动。k.resize(blockSize, \0)在密钥短于 64 时右补零字节注意补的是\0不是空格这一点错了签名全废。iKeyPad message这种写法依赖QByteArray的operator会新分配一块内存。签名调用频率低连一次算一次这点开销可以忽略不用为了省内存写成流式。返回值是 20 字节的原始摘要调用侧用.toHex()转成 40 字符的小写十六进制串。有些平台要求大写改.toUpper()一个字的事但改错了就是连不上。4.3 QMqttClient 的连接参数与心跳鉴权参数算好就可以建连接了// deviceclient.cpp建立连接 void DeviceClient::connectToPlatform() { const AuthInfo auth buildAuth(m_productKey, m_deviceName, m_deviceSecret); if (!m_client) { m_client new QMqttClient(this); connect(m_client, QMqttClient::connected, this, DeviceClient::onConnected); connect(m_client, QMqttClient::disconnected, this, DeviceClient::onDisconnected); connect(m_client, QMqttClient::messageReceived, this, DeviceClient::onMessageReceived); connect(m_client, QMqttClient::errorChanged, this, DeviceClient::onClientError); } m_client-setHostname(m_host); // 平台给出的接入域名 m_client-setPort(1883); // TCP 直连TLS 走 8883 m_client-setClientId(auth.clientId); m_client-setUsername(auth.username); m_client-setPassword(auth.password); m_client-setKeepAlive(60); // 心跳间隔秒 m_client-setCleanSession(true); // 见第 5 章第 4 条的说明 m_client-connectToHost(); }setKeepAlive(60)意味着客户端每 60 秒内至少要和平台有一次报文往来QMqttConnection内部用 PINGREQ 保底。平台侧一般允许 1.5 倍容差超过就判定掉线并踢连接所以这个值不要设得太大弱网环境可以降到 30。setCleanSession(true)表示每次连接都重建会话平台侧不保留订阅关系和离线消息。这个选择对上报型设备合适对需要接收离线指令的设备就不合适了取舍在第 5 章展开。errorChanged传的是枚举值槽函数里最好映射成可读字符串再打日志void DeviceClient::onClientError(QMqttClient::ClientError err) { // 枚举转可读文本直接打数字在日志里没法看 qWarning() mqtt client error: err m_client-errorString(); }errorString()是 QMqttClient 里的辅助函数返回当前错误的人类可读描述。两个一起打排查时能省不少时间。4.4 属性上报和指令下发的 Topic 规划连上之后就是收发。平台侧对 Topic 有固定的模板通常长这样用途Topic 模板方向属性上报/sys/{pk}/{dn}/thing/event/property/post上行属性设置/sys/{pk}/{dn}/thing/service/property/set下行事件上报/sys/{pk}/{dn}/thing/event/{eventId}/post上行服务调用/sys/{pk}/{dn}/thing/service/{serviceId}下行{pk}换成 ProductKey{dn}换成 DeviceName。上行 Topic 由设备拼下行 Topic 由平台投递设备要做的就是提前订阅。订阅下行// deviceclient.cpp订阅属性设置和平台指令 void DeviceClient::subscribeDownlink() { const QString setTopic QString(/sys/%1/%2/thing/service/property/set) .arg(m_productKey, m_deviceName); QMqttSubscription *sub m_client-subscribe(QMqttTopicFilter(setTopic), 1); if (!sub) { qWarning() subscribe failed, client not connected; return; } // subscribe 是异步的只有 state 变成 Subscribed 才算真订阅成功 connect(sub, QMqttSubscription::stateChanged, this, [this](QMqttSubscription::SubscriptionState s) { if (s QMqttSubscription::Subscribed) qInfo() downlink ready; else if (s QMqttSubscription::Error) qWarning() downlink subscribe error; }); connect(sub, QMqttSubscription::messageReceived, this, DeviceClient::onCommandMessage); }subscribe()返回非空只代表请求发出去了不代表订阅成功。stateChanged是异步回调一定要监听。我见过只判断返回值就认为订阅成功的代码结果网络抖动时订阅丢了程序还傻等着收指令。上行发布// deviceclient.cpp属性上报 bool DeviceClient::reportProperty(const QString identifier, const QVariant value) { if (!m_client || m_client-state() ! QMqttClient::Connected) return false; QJsonObject params; params.insert(identifier, QJsonValue::fromVariant(value)); QJsonObject root; root.insert(id, QString::number(m_msgId)); root.insert(version, 1.0); root.insert(params, params); QMqttMessage msg; msg.setTopic(QMqttTopicName( QString(/sys/%1/%2/thing/event/property/post).arg(m_productKey, m_deviceName))); // toJson 默认输出 UTF-8中文值不会乱码 msg.setPayload(QJsonDocument(root).toJson(QJsonDocument::Compact)); msg.setQos(1); const qint32 id m_client-publish(msg); if (id -1) { qWarning() publish rejected, connection down; return false; } return true; }QJsonDocument::toJson(QJsonDocument::Compact)输出去掉所有空白的紧凑 JSON省带宽。默认编码就是 UTF-8所以在setPayload之前不需要再toUtf8()多转一次反而可能出问题。m_client-publish()返回报文 ID返回 -1 表示没连上被拒这个返回值必须判断不能当 void 用。5. 联调避坑五个把我卡到凌晨的现场问题理论通了不代表现场能跑。下面这几条都是我和身边人实际撞过的按「现象 → 原因 → 解决」记下来。5.1 连接一闪就断服务端日志只有一行拒绝现象connected信号从来不触发disconnected倒是来了客户端日志里能看到 TCP 层握手成功紧接着被断开没有任何错误码。原因九成是鉴权参数错了。最常见的是设备时间不准。工控机常年离线主板电池没电系统时间可能差了几天而签名里带的是秒级时间戳平台侧会校验时间偏差一般允许几分钟超了直接拒。解决设备启动时先做一次时间同步没有 NTP 条件的话退一步的做法是把偏差控制在一个可接受范围或者让平台侧提供时间校准接口。排查阶段可以在拼完 clientId 之后把 clientId、username、待签名串、签名结果四个值全打出来和平台的在线调试工具算出来的比对一眼就能看出是时间戳问题还是拼接顺序问题。注意待签名串里如果混进了多余的空格或换行肉眼几乎看不出来打印时用qDebug() content.toUtf8().toHex()按字节看。5.2 编译过了运行崩链接期报 vtable 未定义现象undefined reference to vtable for QMqttClient或者程序能编译但运行时QMqttClient一构造就崩。原因带Q_OBJECT宏的类需要 moc 生成元对象代码。moc 是靠 qmake 扫HEADERS变量触发的如果头文件只写在INCLUDEPATH里、没出现在业务工程的HEADERS里moc 就不会处理它虚函数表自然缺失。解决把所有带Q_OBJECT的头文件明确列进 pro 的HEADERS。如果是 qtmqtt 库里自带的类确认库本身编译时这些头文件已经加进mqtt.pro。改完 pro 一定要重新跑一次qmake光make是不会重新生成 Makefile 的很多人卡在这一步。5.3 中文 payload 到平台变问号现象设备名或告警文本里有中文平台侧收到的 JSON 里是???或者乱码。原因把QString转QByteArray时用了toLatin1()。toLatin1()对超出 Latin-1 范围的字符直接替换成?且不报错是典型的静默失败。解决所有跨进程、跨网络的字符串转换统一走toUtf8()。用QJsonDocument序列化时本身就是 UTF-8只要不从别处塞进 Latin-1 编码的QByteArray就没事。排查方法是打印payload.toHex()看中文那几个字节是不是 UTF-8 的三字节序列比如「告」是E5 91 8A不是的话就找到源头改掉。5.4 订阅返回成功却收不到下行现象subscribe()返回了非空指针stateChanged也变成Subscribed了但平台下发指令时messageReceived不触发。原因多半是重连后没有重新订阅。setCleanSession(true)时每次连接都是全新会话上一次的订阅关系平台侧已经清了。连接断开再恢复代码里如果没有重新调subscribeDownlink()就等于没订阅。解决把订阅动作绑定到connected信号上每次连接成功都重新订阅一遍。订阅本身是幂等的重复订同一个 Topic 不会出错。另一种做法是setCleanSession(false)走持久会话但这要求 clientId 稳定不变且平台侧支持会话保留不同平台行为不一致不如重订阅来得可靠。5.5 跑一晚上内存涨到 300MB现象全天候运行白天内存平稳夜里涨用任务管理器看是线性增长重启就好。原因消息对象没释放。两种常见写法一是在messageReceived槽里new了对象挂到列表上等异步处理处理完忘了delete二是每次 publish 构造QMqttMessage都用堆分配出作用域没释放。解决能不 new 就不 newQMqttMessage是值类型栈上构造直接传给publish()完全没问题。确实要跨线程传递的用deleteLater()而不是delete避免在信号槽执行期间析构对象。加一个定时器每 60 秒打印一次qDebug() m_pendingList.size()规模不收敛就说明有泄漏点。6. 二次开发把示例改成能上设备的连接骨架示例工程的连接逻辑是线性的构造、连、发。真上设备得补两件事——断线之后怎么办以及 UI 卡住时消息怎么处理。6.1 用状态机管住重连直接监听disconnected然后立刻connectToHost()是最常见的写法也是最容易把平台打崩的写法。平台侧有连接频率限制断线后疯狂重连会被临时封禁。稳妥的做法是状态机加指数退避// deviceclient.cpp断线重连间隔从 2s 起翻倍封顶 60s void DeviceClient::onDisconnected() { if (m_manualClose) return; m_retryDelay qMin(m_retryDelay * 2, 60000); qInfo() disconnected, retry in m_retryDelay ms; QTimer::singleShot(m_retryDelay, this, [this]() { if (!m_manualClose) connectToPlatform(); // 内部会重新算时间戳和签名 }); }m_retryDelay初始值设 1000第一次断线等 2 秒之后 4、8、16、32、60 秒封顶。connectToPlatform()每次都重新走buildAuth()因为时间戳必须重新取用旧签名重连必然被拒。m_manualClose是主动断开时的标志位不加这个程序退出时也会触发重连进程退不掉。6.2 把消息处理从 UI 线程剥出来messageReceived是在 Qt 的网络线程还是主线程触发取决于QMqttClient对象归属哪个线程。默认在主线程一旦下行指令的处理逻辑里有耗时操作写文件、等硬件响应界面就卡住。做法是把QMqttClient整个搬到工作线程方案改动量适用场景默认主线程零消息量小、处理快moveToThread搬客户端中高频上报、UI 要求流畅独立进程大需要和 UI 完全隔离中间方案最实用在工作线程里创建QMqttClient业务侧通过信号槽跨线程投递数据Qt 自动用队列连接保证线程安全。注意QMqttClient的所有本文还有配套的精品资源点击获取