QT5.15交叉编译和移植qmqtt教程
目录环境配置交叉编译与移植交叉编译移植使用代码示例MQTT相关概念基本地址格式SSL 证书从哪里获取环境配置QT版本qt5.15.15交叉编译与移植交叉编译官方MQTT库链接https://github.com/qt/qtmqtt选择对应的版本本文选5.15.2版本QT使用的是Q5.15.15但是mqtt库中没有对应的qtmqtt版本最近的版本是5.15.2,cd qtmqtt-5.15.2目录创建安装目录mkdir build进入 build目录下执行qmakecd build aarch64-v01c01-linux-gnu-qmake ../qtmqtt.pro生成Makefile文件当然也可以直接在qtmqtt-5.15.2根目录下执行aarch64-v01c01-linux-gnu-qmake ./qtmqtt.pro可以看到执行后生成了bin 、include、lib目录但是推荐创建build目录在此目录下去交叉编译这样编译后生成的库都在build目录下。编译make -j 16执行make编译报如下告警/qtmqtt-5.15.2/src/mqtt/qmqttauthenticationproperties.h:33:10: fatal error: QtMqtt/qmqttglobal.h: No such file or directory 33 | #include QtMqtt/qmqttglobal.h | ^~~~~~~~~~~~~~~~~~~~~~ compilation terminated.原因是执行make编译时include/QtMqtt目录下没有对应的头文件执行cp ../src/mqtt/*.h include/QtMqtt/将对应的头文件都拷贝过来重新make编译可以发现上面的告警已经解决但是报了另外的告警如下/qtmqtt-5.15.2/examples/mqtt/quicksubscription/qmlmqttclient.h:55:10: fatal error: QtMqtt/QMqttClient: No such file or directory 55 | #include QtMqtt/QMqttClient | ^~~~~~~~~~~~~~~~~~~~ compilation terminated. /qtmqtt-5.15.2/examples/mqtt/simpleclient/mainwindow.h:55:10: fatal error: QMqttClient: No such file or directory 55 | #include QMqttClient | ^~~~~~~~~~~~~ compilation terminated. /qtmqtt-5.15.2/examples/mqtt/subscriptions/subscriptionwindow.h:55:10: fatal error: QtMqtt/QMqttMessage: No such file or directory 55 | #include QtMqtt/QMqttMessage | ^~~~~~~~~~~~~~~~~~~~~ compilation terminated.只需要将刚刚拷贝的对应头文件再复制一份并修改成对应的名称即可cp include/QtMqtt/qmqttclient.h include/QtMqtt/QMqttClient cp include/QtMqtt/qmqttmessage.h include/QtMqtt/QMqttMessage再次编译以上问题解决报其他头文件如下找到对应的头文件拷贝成对应缺失的文件即可cp include/QtMqtt/qmqtttopicname.h include/QtMqtt/QMqttTopicName cp include/QtMqtt/qmqttsubscription.h include/QtMqtt/QMqttSubscription再次编译成功生成的动态库如下打包qtmqtt库mkdir libqtmqtt-5.15.2 cp include libqtmqtt-5.15.2 -rf cp lib libqtmqtt-5.15.2 -rf tar -czvf libqtmqtt-5.15.2.tar.gz libqtmqtt-5.15.2移植使用将上述库文件解压到开发板上如/opt/目录下并解压tar -xvf libqtmqtt-5.15.2.tar.gz配置环境变量 vi /etc/profileexport LD_LIBRARY_PATH/opt/libqtmqtt-5.15.2/lib:$LD_LIBRARY_PATH使能环境变量source /etc/profile将上述库文件解压到工程目录下项目文件配置.pro中包含network模块并指定库路径和头文件# 添加network QT network # 包含qtmqtt 库路径和头文件 LIBS -L/path/to/libqtmqtt-5.15.2/lib -lQt5Mqtt INCLUDEPATH /path/to/libqtmqtt-5.15.2/include # rpath编译时嵌入可执行文件中的固定路径可选路径为开发板上qtmqtt库路径 QMAKE_RPATHDIR /opt/libqtmqtt-5.15.2/lib在 .pro 文件中添加 QMAKE_RPATHDIR将开发板上的目标路径写入可执行文件这样程序运行时自动搜索该路径就可以不用修改vi /etc/profile配置环境变量了不过建议vi /etc/profile 配置环境变量 和.pro 文件中添加 QMAKE_RPATHDIR都做。代码示例建立一个MqttWorker类在UI主线程中将MqttWorker放到子线中去执行通过信号与槽的方式相互通信UI主线程只负责刷新UI。// mqttclient.h#ifndef MQTT_CLIENT_H #define MQTT_CLIENT_H #include QObject #include QHash #include QDateTime #include QTimer #include QtMqtt/qmqttconnectionproperties.h #include QtMqtt/QMqttClient #include QtMqtt/QMqttSubscription class MqttWorker : public QObject { Q_OBJECT public: explicit MqttWorker(QObject *parent nullptr); ~MqttWorker(); public slots: void onConnectToBroker(const QString host, quint16 port, const QString clientId, const QString username QString(), const QString password QString(), const QString caCertPath QString()); void onDisconnectFromBroker(); void onPublish(const QString topic, const QByteArray payload, quint8 qos 0); void onSubscribe(const QString topic, quint8 qos 0); void onUnsubscribe(const QString topic); signals: void Sig_MqttConnected(); void Sig_MqttDisconnected(); void Sig_MqttErrorOccurred(const QString error); void Sig_MqttMessageReceived(const QString topic, const QByteArray payload); void Sig_MqttSubscriptionStateChanged(const QString topic, bool subscribed); private slots: void onConnected(); void onDisconnected(); void onMqttStateChanged(QMqttClient::ClientState state); void onMqttError(QMqttClient::ClientError error); void onMessageReceived(const QMqttMessage msg); void onReconnectTimerTimeout(); private: void addLog(const QString log, bool isError); void setWillMessage(const QString clientId); bool IsValidTopic(const QString topic); bool CheckSubscribeTopic(const QString topic); private: QMqttClient *m_client; // 存储已订阅的主题和对应的订阅实例用于取消订阅和管理 QHashQString, QMqttSubscription* m_subscriptions; // 断线重连定时器 QTimer *m_reconnectTimer; // 开启自动重连 bool m_autoReconnect true; // 重连间隔5秒 const int m_reconnectInterval 5000; }; #endif// mqttclient.cpp#include mqttclient.h #include QSslConfiguration #include QSslCertificate MqttWorker::MqttWorker(QObject *parent) : QObject(parent) { m_client new QMqttClient(this); // 创建断线重连定时器设置循环触发 m_reconnectTimer new QTimer(this); m_reconnectTimer-setInterval(m_reconnectInterval); m_reconnectTimer-setSingleShot(false); // 非单次触发循环尝试重连 // 绑定重连定时器的超时信号触发重连逻辑 connect(m_reconnectTimer, QTimer::timeout, this, MqttWorker::onReconnectTimerTimeout); connect(m_client, QMqttClient::connected, this, MqttWorker::onConnected); connect(m_client, QMqttClient::disconnected, this, MqttWorker::onDisconnected); connect(m_client, QMqttClient::stateChanged, this, MqttWorker::onMqttStateChanged); connect(m_client, QMqttClient::errorChanged, this, MqttWorker::onMqttError); } MqttWorker::~MqttWorker() { // 析构函数中释放资源避免内存泄漏 if (m_client-state() QMqttClient::Connected) { m_client-disconnectFromHost(); } // 停止重连定时器 if (m_reconnectTimer-isActive()) { m_reconnectTimer-stop(); } // 释放所有订阅实例 for (auto sub : m_subscriptions.values()) { disconnect(sub, QMqttSubscription::messageReceived, this, MqttWorker::onMessageReceived); delete sub; } m_subscriptions.clear(); // 释放MQTT客户端和重连定时器资源 delete m_client; delete m_reconnectTimer; } // 添加日志格式化时间区分普通日志和错误日志自动滚动到最新条目 void MqttWorker::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) { logStr QString(font colorred%1/font).arg(logStr); } qDebug() logStr; } // 设置遗嘱消息配置客户端异常断开时Broker发布的离线通知 void MqttWorker::setWillMessage(const QString clientId) { // 创建连接属性对象用于配置遗嘱消息和会话参数 QMqttConnectionProperties properties; // 配置会话过期时间300秒5分钟未重连则清理会话 properties.setSessionExpiryInterval(300); // 配置遗嘱消息的主题设备离线状态通知主题 // QMqttTopicName willTopic(qt/mqtt/demo/will); QString willTopic qt/mqtt/demo/will; // 配置遗嘱消息内容包含当前客户端ID便于区分哪个设备离线 QString willMsg QString(Client %1 is offline).arg(clientId); // 配置遗嘱消息参数QoS设为1确保送达保留消息设为true新订阅者可获取 m_client-setWillTopic(willTopic); m_client-setWillMessage(willMsg.toUtf8()); m_client-setWillQoS(1); m_client-setWillRetain(true); // 配置清理会话设为false重连后保留订阅关系和未完成消息 m_client-setCleanSession(false); // 将配置应用到MQTT客户端 m_client-setConnectionProperties(properties); addLog(遗嘱消息配置完成异常离线时将自动通知, false); } void MqttWorker::onConnectToBroker(const QString host, quint16 port, const QString clientId, const QString username, const QString password, const QString caCertPath) { if (host.isEmpty()) { addLog(连接失败Broker IP不能为空, true); return; } if (clientId.isEmpty()) { addLog(连接失败客户端ID不能为空, true); return; } m_client-setHostname(host); m_client-setPort(port); m_client-setClientId(clientId); if (!username.isEmpty()) { m_client-setUsername(username); m_client-setPassword(password); } else { // 清空密码避免残留之前的密码 m_client-setPassword(); } // // 配置 SSLCA 证书路径 // if (!caCertPath.isEmpty()) { // QSslConfiguration sslConfig QSslConfiguration::defaultConfiguration(); // QListQSslCertificate caCerts QSslCertificate::fromPath(caCertPath); // if (!caCerts.isEmpty()) { // sslConfig.setCaCertificates(caCerts); // } // sslConfig.setProtocol(QSsl::TlsV1_2OrLater); // m_client-setSslConfiguration(sslConfig); // // 设置SSL端口8883 // m_client-setPort(8883); // } // 配置遗嘱消息 setWillMessage(clientId); m_client-connectToHost(); } void MqttWorker::onDisconnectFromBroker() { m_client-disconnectFromHost(); } bool MqttWorker::IsValidTopic(const QString topic) { if (topic.isEmpty() || topic.contains( )) { return false; } // 发布主题不能包含通配符 if (topic.contains() || topic.contains(#)) { return false; } return true; } void MqttWorker::onPublish(const QString topic, const QByteArray payload, quint8 qos) { if (IsValidTopic(topic) false) { emit Sig_MqttErrorOccurred(publish failed! topic is invalid); return; } if (m_client-state() QMqttClient::Connected) { m_client-publish(topic, payload, qos); } else { emit Sig_MqttErrorOccurred(publish failed! Not connected); } } // 订阅参数校验校验订阅主题是否合法 bool MqttWorker::CheckSubscribeTopic(const QString topic) { if (topic.isEmpty() || topic.contains( )) { addLog(订阅失败订阅主题不能为空, true); return false; } // 校验通配符格式#只能在主题末尾 if (topic.contains(#) !topic.endsWith(#)) { addLog(订阅失败通配符#只能放在主题末尾, true); return false; } return true; } void MqttWorker::onSubscribe(const QString topic, quint8 qos) { if (m_client-state() ! QMqttClient::Connected) { emit Sig_MqttErrorOccurred(subscribe failed! Not connected); return; } if (!CheckSubscribeTopic(topic)) { emit Sig_MqttErrorOccurred(subscribe failed! topic error); return; } // 校验是否已订阅该主题避免重复订阅 if (m_subscriptions.contains(topic)) { addLog(QString(订阅失败已订阅主题【%1】).arg(topic), true); return; } auto sub m_client-subscribe(topic, qos); if (sub) { // 存储订阅实例用于后续取消订阅 m_subscriptions.insert(topic, sub); connect(sub, QMqttSubscription::messageReceived, this, MqttWorker::onMessageReceived); connect(sub, QMqttSubscription::stateChanged, this, [this, topic](QMqttSubscription::SubscriptionState state) { bool subscribed (state QMqttSubscription::Subscribed); emit Sig_MqttSubscriptionStateChanged(topic, subscribed); }); } else { emit Sig_MqttErrorOccurred(subscribe failed: topic); } } // 取消订阅 void MqttWorker::onUnsubscribe(const QString topic) { // 校验当前是否已连接 if (m_client-state() ! QMqttClient::Connected) { addLog(取消订阅失败当前未连接到Broker, true); return; } // 校验是否已订阅该主题 if (!m_subscriptions.contains(topic)) { addLog(QString(取消订阅失败未订阅主题【%1】).arg(topic), true); return; } // 取消订阅 m_client-unsubscribe(QMqttTopicFilter(topic)); // 断开消息接收信号释放订阅实例 QMqttSubscription *sub m_subscriptions.take(topic); disconnect(sub, QMqttSubscription::messageReceived, this, MqttWorker::onMessageReceived); delete sub; addLog(QString(取消订阅成功主题%1).arg(topic), false); } void MqttWorker::onConnected() { addLog(连接成功已成功连接到Broker, false); emit Sig_MqttConnected(); // 停止重连定时器若之前在重连 if (m_reconnectTimer-isActive()) { m_reconnectTimer-stop(); } } void MqttWorker::onDisconnected() { addLog(已断开与Broker的连接, false); emit Sig_MqttDisconnected(); // 开启自动重连若启用 if (m_autoReconnect) { addLog(QString(将在%1毫秒后尝试重连).arg(m_reconnectInterval), false); m_reconnectTimer-start(); } } void MqttWorker::onMqttStateChanged(QMqttClient::ClientState state) { switch (state) { case QMqttClient::Disconnected: addLog(连接状态未连接, false); break; case QMqttClient::Connecting: addLog(连接状态连接中, false); break; case QMqttClient::Connected: addLog(连接状态已连接, false); break; default: addLog(QString(连接状态未知状态%1).arg(state), false); break; } } void MqttWorker::onMqttError(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; } emit Sig_MqttErrorOccurred(QString(MQTT Error: %1).arg(errorMsg)); } void MqttWorker::onMessageReceived(const QMqttMessage msg) { // 获取消息相关属性 QString topic msg.topic().name(); QString content msg.payload().data(); quint8 qos msg.qos(); bool isRetain msg.retain(); QDateTime time QDateTime::currentDateTime(); // 添加接收日志 addLog(QString(收到消息主题%1QoS%2保留消息%3时间%4) .arg(topic) .arg(qos) .arg(isRetain ? 是 : 否) .arg(time.toString(yyyy-MM-dd HH:mm:ss)), false); addLog(QString(消息内容%1).arg(content), false); emit Sig_MqttMessageReceived(msg.topic().name(), msg.payload()); } // 断线重连 void MqttWorker::onReconnectTimerTimeout() { if (m_client-state() QMqttClient::Connected) { m_reconnectTimer-stop(); return; } // 发起重连 addLog(尝试重连Broker..., false); m_client-connectToHost(); }// MainWindow.h#include QThread #include MqttWorker.h class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); signals: void Sig_MqttConnectToBroker(const QString host, quint16 port, const QString clientId, const QString username QString(), const QString password QString(), const QString caCertPath QString()); void Sig_PublishMessage(const QString topic, const QByteArray payload, quint8 qos 0); void Sig_SubscribeTopic(const QString topic, quint8 qos 0); private slots: void onConnectButtonClicked(); void onMqttConnected(); void onMqttMessage(const QString topic, const QByteArray payload); void onMqttError(const QString error); private: QThread *m_mqttThread; MqttWorker *m_mqttWorker; };// MainWindow.cppMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 创建工作线程和 worker 对象 m_mqttThread new QThread(this); m_mqttWorker new MqttWorker(); m_mqttWorker-moveToThread(m_mqttThread); // 连接跨线程信号槽自动使用队列连接 connect(m_mqttThread, QThread::finished, m_mqttWorker, QObject::deleteLater); connect(this, MainWindow::Sig_MqttConnectToBroker, m_mqttWorker, MqttWorker::onConnectToBroker); connect(this, MainWindow::Sig_PublishMessage, m_mqttWorker, MqttWorker::onPublish); connect(this, MainWindow::Sig_SubscribeTopic, m_mqttWorker, MqttWorker::onSubscribe); connect(m_mqttWorker, MqttWorker::Sig_MqttConnected, this, MainWindow::onMqttConnected); connect(m_mqttWorker, MqttWorker::Sig_MqttMessageReceived, this, MainWindow::onMqttMessage); connect(m_mqttWorker, MqttWorker::Sig_MqttErrorOccurred, this, MainWindow::onMqttError); m_mqttThread-start(); // 启动线程事件循环开始 // UI 连接示例 connect(ui.connectButton, QPushButton::clicked, this, MainWindow::onConnectButtonClicked); } MainWindow::~MainWindow() { m_mqttThread-quit(); m_mqttThread-wait(); } void MainWindow::onConnectButtonClicked() { // 通过信号将参数传递给工作线程 emit Sig_MqttConnectToBroker(broker.example.com, 8883, myClientId, username, password, /path/to/ca.crt); } void MainWindow::onMqttConnected() { // 连接成功后在主线程更新 UI ui.statusLabel-setText(Connected); // 订阅主题也通过信号发送 emit Sig_SubscribeTopic(sensor/#, 0); } void MainWindow::onMqttMessage(const QString topic, const QByteArray payload) { // 在主线程中安全更新 UI ui.textEdit-append(QString([%1] %2).arg(topic, QString::fromUtf8(payload))); } void MainWindow::onMqttError(const QString error) { QMessageBox::warning(this, MQTT Error, error); }MQTT相关概念MQTT服务器的地址格式主要取决于连接方式协议、端口、是否使用域名或IP以下是常见格式的总结基本地址格式标准TCP连接非加密mqtt://域名或IP:1883示例mqtt://broker.emqx.io:1883TLS/SSL加密连接推荐用于生产环境mqtts://域名或IP:8883示例mqtts://a1B2c3D4e5F.iot-as-mqtt.cn-shanghai.aliyuncs.com:88839WebSocket连接用于浏览器等WebSocket环境ws://域名:8083/path示例ws://broker.emqx.io:8083/mqtt12WebSocket over SSL加密WebSocketwss://域名:8084/path示例wss://broker.emqx.io:8084/mqttQMqttClient有默认端口1883默认明文TCP端口8883默认 TLS/SSL 加密端口如果你调用setPort()时未指定则默认使用 1883。但实际端口号取决于Broker 的监听配置你可以使用任何未被占用的端口如 1884、8884 等只要 Broker 在该端口上提供服务。SSL 证书从哪里获取证书的来源取决于你的使用场景场景推荐方式说明公网部署如云 Broker使用公共 CA 签发的证书例如 Lets Encrypt免费、DigiCert 等。在客户端只需配置 CA 证书如ca.crt用于验证 Broker 身份。内部测试/私有网络使用自签名证书用 OpenSSL 等工具生成 CA 证书和 Broker 证书。客户端需要信任该自签名 CA。企业环境使用企业 CA由企业内部 CA 签发客户端导入企业根证书。在 Qt 中加载证书若只需验证 Broker 证书单向认证将 CA 证书PEM 格式通过资源文件或文件系统加载设置到QSslConfiguration::setCaCertificates()。若需要客户端证书认证双向 TLS还需调用sslConfig.setLocalCertificate()和sslConfig.setPrivateKey()。用户名和密码在 MQTT 协议中是以明文形式放在 CONNECT 包中的除非使用 TLS 加密整个连接。因此如果未启用 TLS凭证会暴露在网络中。建议始终配合 TLS 使用用户名密码以确保安全。// 配置 SSLCA 证书路径 QString caCertPath /path/ca.crt; if (!caCertPath.isEmpty()) { QSslConfiguration sslConfig QSslConfiguration::defaultConfiguration(); QListQSslCertificate caCerts QSslCertificate::fromPath(caCertPath); if (!caCerts.isEmpty()) { sslConfig.setCaCertificates(caCerts); } sslConfig.setProtocol(QSsl::TlsV1_2OrLater); m_client-setSslConfiguration(sslConfig); // 设置SSL端口8883 m_client-setPort(8883); } // 禁用证书验证仅测试用生产环境不建议 // sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); m_client-connectToHost();注意需确保 Broker 已配置 SSL 证书Qt 客户端需信任该证书否则会触发TransportError。如果需要使用Qt Network 模块的 SSL/TLS 功能则configure时需要去掉-no-openssl并配置-openssl-runtime同时需要先交叉编译和移植openssl库并指定库路径。如果 OpenSSL 头文件和库文件不在系统默认路径也可以通过设置环境变量 OPENSSL_LIBS 来指定这和用 -L 和 -l 参数是等价的。export OPENSSL_LIBS-L/path/to/your/openssl/lib -lssl -lcrypto移植openssl库 libssl.so 和 libcrypto.so 部署到开发板上并设置 LD_LIBRARY_PATH 或 rpath 确保程序能找到。并指定LD_LIBRARY_PATH环境变量。-openssl-runtime (推荐) 这是最常用的选项。它会在编译时链接 OpenSSL 库让程序在运行时动态加载 libssl.so 和 libcrypto.so。这种方式更灵活如果你的开发板需要更新 OpenSSL 库直接替换文件即可无需重新编译 Qt。-openssl-linked 这个选项会将 OpenSSL 库的代码也链接进 Qt 库中会导致 Qt 库体积变大。在嵌入式开发中如无特殊需求不推荐使用。如证书使用举例https://blog.csdn.net/joyopirate/article/details/155568880?utm_mediumdistribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-155568880-blog-159249617.235^v43^pc_blog_bottom_relevance_base1spm1001.2101.3001.4242.1utm_relevant_index2MQTT服务器搭建https://blog.csdn.net/2302_77149904/article/details/148771260参考链接https://blog.csdn.net/liyuanbhu/article/details/106597506https://blog.51cto.com/xiaohaiwa/5379260https://blog.csdn.net/qianniulaoren/article/details/157582308https://blog.csdn.net/joyopirate/article/details/155568880?utm_mediumdistribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-155568880-blog-159249617.235^v43^pc_blog_bottom_relevance_base1spm1001.2101.3001.4242.1utm_relevant_index2https://blog.csdn.net/nchu_zhangyiqing/article/details/126781994?spm1001.2101.3001.6650.7utm_mediumdistribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-7-126781994-blog-80314922.235%5Ev43%5Epc_blog_bottom_relevance_base1depth_1-utm_sourcedistribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-7-126781994-blog-80314922.235%5Ev43%5Epc_blog_bottom_relevance_base1utm_relevant_index10https://blog.csdn.net/haokan123456789/article/details/153929802?utm_mediumdistribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1-153929802-blog-131756822.235^v43^pc_blog_bottom_relevance_base1spm1001.2101.3001.4242.2utm_relevant_index4