本文基于 C++/Qt 实现一个 MQTT 测试工具,支持连接 MQTT Broker、订阅主题、发布消息、接收消息、断开连接和日志记录。和 HTTP、WebSocket 不同,MQTT 更强调"发布/订阅"通信模型,客户端之间通常不直接通信,而是通过 Broker 转发消息。本文从 MQTT 的基本概念出发,结合 Qt 中
QTcpSocket的使用,梳理一个 MQTT 测试模块应该如何设计和实现。
一、MQTT 是什么?为什么会有 Broker、Topic、订阅和发布?
MQTT 是一种常用于物联网、设备通信、消息推送的轻量级通信协议。
它和 HTTP、WebSocket 的思路都不太一样。
HTTP 更像是:
客户端:我请求一个接口
服务器:我返回一次结果
WebSocket 更像是:
客户端和服务器建立长连接,双方可以互相发消息
而 MQTT 更像是一个"消息中转站"模式:
客户端 A 把消息发给 Broker
Broker 根据 Topic 把消息转发给订阅了这个 Topic 的客户端
客户端 B 收到消息
这里面有几个非常重要的概念。
1. Broker 是什么?
Broker 可以理解成 MQTT 的"消息服务器"或者"中转站"。
客户端之间一般不直接通信,而是都连接到 Broker。
比如:
设备 A 连接 Broker
手机 App 连接 Broker
上位机工具连接 Broker
设备 A 发布消息到某个主题,Broker 再把这个消息转发给订阅了该主题的客户端。
2. Topic 是什么?
Topic 可以理解成"消息频道"。
比如:
ndatools/test
sensor/temp
device/001/status
客户端如果想接收某一类消息,就需要订阅对应的 Topic。
比如客户端订阅:
ndatools/test
那么只要有其他客户端向 ndatools/test 发布消息,这个客户端就能收到。
3. Publish 和 Subscribe 是什么?
MQTT 里面有两个核心动作:
Subscribe:订阅主题,表示我想接收这个 Topic 的消息
Publish:发布消息,表示我要向这个 Topic 发送数据
所以 MQTT 测试工具的核心功能不是"请求一个接口",而是:
连接 Broker
订阅 Topic
向 Topic 发布消息
接收 Broker 转发回来的消息
如果用生活化一点的话来说:
Broker:微信群服务器
Topic:某个群聊
Subscribe:加入这个群,准备接收群消息
Publish:往这个群里发消息
二、实现 MQTT 测试工具前,先构思哪些功能?
一个 MQTT 测试工具,本质上就是一个 MQTT 客户端。它要让用户可以连接 Broker,并围绕 Topic 进行消息收发。
所以界面上至少需要这些内容:
1. Broker 地址:例如 broker.emqx.io 或 127.0.0.1
2. 端口号:普通 MQTT 默认是 1883
3. ClientId:客户端唯一标识
4. 用户名和密码:有些 Broker 需要认证
5. 订阅 Topic:填写要接收消息的主题
6. 发布 Topic:填写要发送消息的主题
7. Payload:真正要发布的消息内容
8. QoS:消息服务质量等级
9. Retain:是否让 Broker 保留最后一条消息
10. 通信日志:显示连接、订阅、发布、接收和错误信息
当前这个 FormMqttTester 类并没有直接使用 QMqttClient,而是使用 QTcpSocket 手动实现 MQTT 报文的发送和解析。类中包含了 TCP Socket、接收缓冲区、MQTT 连接状态、CONNACK 等待标记、超时定时器、报文 ID 和日志缓存等成员。
核心成员可以这样理解:
cpp
private:
Ui::FormMqttTester *ui;
// MQTT 底层基于 TCP 通信,这里用 QTcpSocket 连接 Broker
QTcpSocket *m_socket = nullptr;
// 接收缓冲区,用来缓存 TCP 收到的 MQTT 字节数据
QByteArray m_receiveBuffer;
// 当前 MQTT 是否已经连接成功
bool m_mqttConnected = false;
// TCP 连接成功后,需要等待 MQTT Broker 返回 CONNACK
bool m_waitingConnAck = false;
// MQTT 连接确认超时定时器,避免一直卡在等待状态
QTimer *m_connAckTimer = nullptr;
// MQTT 报文 ID,订阅和 QoS 消息会用到
quint16 m_packetId = 1;
// 日志缓存,用于保存通信记录
QStringList m_logEntries;
这里有一个点特别重要:
TCP 连接成功,不代表 MQTT 连接成功。
因为 MQTT 是跑在 TCP 上层的协议。连接 Broker 时,流程其实是:
cpp
先建立 TCP 连接
↓
发送 MQTT CONNECT 报文
↓
等待 Broker 返回 CONNACK 报文
↓
CONNACK 返回成功,才算 MQTT 真正连接成功
所以代码中才需要 m_waitingConnAck 和 m_connAckTimer,用来判断 MQTT 连接确认有没有正常返回。
三、初始化界面和信号槽:先把 MQTT 的使用流程搭起来
初始化界面时,需要设置默认 Broker、端口、ClientId、Topic、QoS 和 Payload。
cpp
void FormMqttTester::initUi()
{
// 设置 Broker 地址提示,并默认使用一个公开 Broker
ui->lineEdit_MqttHost->setPlaceholderText("例如:broker.emqx.io 或 127.0.0.1");
ui->lineEdit_MqttHost->setText("broker.emqx.io");
// MQTT 普通 TCP 端口一般是 1883
ui->spinBox_MqttPort->setRange(1, 65535);
ui->spinBox_MqttPort->setValue(1883);
// ClientId 用时间戳生成,避免多个客户端 ID 冲突
ui->lineEdit_MqttClientId->setText(
QString("NDATools_%1").arg(QDateTime::currentMSecsSinceEpoch())
);
// 用户名和密码可以为空
ui->lineEdit_MqttUsername->setPlaceholderText("可为空");
ui->lineEdit_MqttPassword->setPlaceholderText("可为空");
ui->lineEdit_MqttPassword->setEchoMode(QLineEdit::Password);
// 当前工具支持 QoS 0 和 QoS 1
ui->comboBox_MqttSubQos->clear();
ui->comboBox_MqttSubQos->addItem("0");
ui->comboBox_MqttSubQos->addItem("1");
ui->comboBox_MqttPubQos->clear();
ui->comboBox_MqttPubQos->addItem("0");
ui->comboBox_MqttPubQos->addItem("1");
// 默认订阅和发布同一个 Topic,方便本机自测
ui->lineEdit_MqttSubTopic->setText("ndatools/test");
ui->lineEdit_MqttPubTopic->setText("ndatools/test");
// 设置默认发布内容
ui->textEdit_MqttPayload->setPlaceholderText("请输入要发布的MQTT消息...");
ui->textEdit_MqttPayload->setPlainText("Hello MQTT.");
// 日志区域只读
ui->plainTextEdit_MqttLog->setReadOnly(true);
ui->label_MqttStatus->setText("状态:未连接");
}
这里有几个设计点。
第一个,端口默认是 1883。因为普通 MQTT TCP 连接常用端口就是 1883。如果是 SSL 或 WebSocket 方式的 MQTT,端口和协议都不一样,当前这个模块暂时不处理。
第二,默认订阅 Topic 和发布 Topic 都是:
cpp
ndatools/test
这样做的好处是测试方便。连接成功后,先订阅这个 Topic,再往同一个 Topic 发布消息,如果 Broker 正常转发,就能在日志里看到自己收到的消息。
第三,ClientId 用时间戳生成,主要是为了避免重复。MQTT Broker 通常要求同一个 Broker 上连接的 ClientId 不要冲突,否则可能会把旧连接挤下线。
信号槽部分主要分成两类:
cpp
按钮操作:连接、断开、订阅、发布、清空、复制
Socket 事件:TCP连接成功、断开、收到数据、发生错误
cpp
void FormMqttTester::initConnections()
{
// 点击连接按钮
connect(ui->pushButton_MqttConnect, &QPushButton::clicked,
this, &FormMqttTester::onConnectClicked);
// 点击断开按钮
connect(ui->pushButton_MqttDisconnect, &QPushButton::clicked,
this, &FormMqttTester::onDisconnectClicked);
// 点击订阅按钮
connect(ui->pushButton_MqttSubscribe, &QPushButton::clicked,
this, &FormMqttTester::onSubscribeClicked);
// 点击发布按钮
connect(ui->pushButton_MqttPublish, &QPushButton::clicked,
this, &FormMqttTester::onPublishClicked);
// TCP 连接成功
connect(m_socket, &QTcpSocket::connected,
this, &FormMqttTester::onSocketConnected);
// TCP 连接断开
connect(m_socket, &QTcpSocket::disconnected,
this, &FormMqttTester::onSocketDisconnected);
// TCP 收到数据
connect(m_socket, &QTcpSocket::readyRead,
this, &FormMqttTester::onSocketReadyRead);
// TCP 连接错误
connect(m_socket, &QTcpSocket::errorOccurred,
this, &FormMqttTester::onSocketError);
// MQTT CONNACK 确认超时
connect(m_connAckTimer, &QTimer::timeout,
this, &FormMqttTester::onMqttConnectTimeout);
}
这部分其实体现了 MQTT 模块的整体逻辑:
cpp
用户点击按钮触发操作
QTcpSocket 负责底层网络事件
收到数据后解析 MQTT 报文
根据报文类型更新界面和日志
四、连接 Broker:先连 TCP,再发送 MQTT CONNECT 报文
点击连接按钮后,程序要先检查 Broker 地址和 ClientId。
cpp
void FormMqttTester::onConnectClicked()
{
// 读取 Broker 地址和端口
const QString host = ui->lineEdit_MqttHost->text().trimmed();
const int port = ui->spinBox_MqttPort->value();
// Broker 地址不能为空
if (host.isEmpty()) {
QMessageBox::warning(this, "提示", "请输入MQTT Broker地址。");
return;
}
// ClientId 不能为空
if (ui->lineEdit_MqttClientId->text().trimmed().isEmpty()) {
QMessageBox::warning(this, "提示", "ClientId不能为空。");
return;
}
// 如果当前 Socket 不是未连接状态,先中止旧连接
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
m_socket->abort();
}
// 清空接收缓冲区,避免旧数据影响本次连接
m_receiveBuffer.clear();
// 先设置为未连接状态
setMqttConnectedState(false);
// 记录连接日志
appendLog(QString("[%1] 正在连接Broker:%2:%3")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(host)
.arg(port));
// 连接过程中禁用连接按钮
ui->pushButton_MqttConnect->setEnabled(false);
setStatusText("状态:正在连接...");
// 连接 Broker,本质上是先建立 TCP 连接
m_socket->connectToHost(host, port);
}
这里最关键的是:
cpp
m_socket->connectToHost(host, port);
这一步只是建立 TCP 连接。
TCP 连接成功后,会进入:
cpp
void FormMqttTester::onSocketConnected()
{
appendLog(QString("[%1] TCP连接成功,发送MQTT CONNECT报文")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")));
setStatusText("状态:TCP已连接,等待MQTT确认...");
// 标记正在等待 Broker 返回 CONNACK
m_waitingConnAck = true;
// 启动连接确认超时检测
if (m_connAckTimer) {
m_connAckTimer->start();
}
// 发送 MQTT CONNECT 报文
sendConnectPacket();
}
这个地方很关键:
MQTT 连接不是 TCP 连上就结束,还要发送 MQTT CONNECT 报文。
sendConnectPacket() 就是手动组装 MQTT CONNECT 报文:
cpp
void FormMqttTester::sendConnectPacket()
{
QByteArray variableHeader;
// 写入协议名 MQTT
appendMqttString(variableHeader, "MQTT");
// MQTT 3.1.1 协议级别是 0x04
variableHeader.append(char(0x04));
// 默认 Clean Session 标志
quint8 connectFlags = 0x02;
// 读取用户名和密码
const QString username = ui->lineEdit_MqttUsername->text();
const QString password = ui->lineEdit_MqttPassword->text();
// 如果填写了用户名,就设置用户名标志
if (!username.isEmpty()) {
connectFlags |= 0x80;
}
// 如果填写了密码,就设置密码标志
if (!password.isEmpty()) {
connectFlags |= 0x40;
}
// 写入连接标志
variableHeader.append(char(connectFlags));
// Keep Alive,这里设置为 60 秒
variableHeader.append(char(0x00));
variableHeader.append(char(0x3C));
QByteArray payload;
// Payload 里必须包含 ClientId
appendMqttString(payload, ui->lineEdit_MqttClientId->text().trimmed());
// 如果有用户名,也写入 Payload
if (!username.isEmpty()) {
appendMqttString(payload, username);
}
// 如果有密码,也写入 Payload
if (!password.isEmpty()) {
appendMqttString(payload, password);
}
QByteArray packet;
// 0x10 表示 CONNECT 报文
packet.append(buildFixedHeader(0x10, variableHeader.size() + payload.size()));
// 拼接可变报头和负载
packet.append(variableHeader);
packet.append(payload);
// 通过 TCP Socket 发送 MQTT 报文
m_socket->write(packet);
m_socket->flush();
}
这段代码看起来比 HTTP 和 WebSocket 麻烦一些,因为这里没有直接调用高级 MQTT 客户端类,而是自己按照 MQTT 协议格式拼报文。
MQTT 报文大致可以理解成:
cpp
固定报头 Fixed Header
可变报头 Variable Header
有效载荷 Payload
当前代码里的 buildFixedHeader() 就是用来构造固定报头的:
cpp
QByteArray FormMqttTester::buildFixedHeader(quint8 firstByte, int remainingLength) const
{
QByteArray header;
// 第一个字节表示报文类型和标志位
header.append(char(firstByte));
// 后面是剩余长度字段
header.append(encodeRemainingLength(remainingLength));
return header;
}
因为 MQTT 的剩余长度是变长编码,所以又单独封装了:
cpp
QByteArray FormMqttTester::encodeRemainingLength(int length) const
{
QByteArray encoded;
do {
// 每次取低 7 位
quint8 byte = length % 128;
length /= 128;
// 如果后面还有数据,就把最高位置 1
if (length > 0) {
byte |= 128;
}
encoded.append(char(byte));
} while (length > 0);
return encoded;
}
这个地方不用死记 MQTT 报文细节,先理解思路就行:
cpp
Qt 用 QTcpSocket 负责发送字节流
程序自己按照 MQTT 协议格式拼出 CONNECT、SUBSCRIBE、PUBLISH 等报文
Broker 收到这些报文后返回对应确认报文
五、订阅、发布和接收消息:围绕 Topic 完成通信闭环
MQTT 最核心的功能就是订阅和发布。
订阅之前,必须先确认 MQTT 已经连接成功:
cpp
void FormMqttTester::onSubscribeClicked()
{
// 未连接时不能订阅
if (!m_mqttConnected) {
QMessageBox::warning(this, "提示", "请先连接MQTT Broker。");
return;
}
// 获取订阅 Topic
const QString topic = ui->lineEdit_MqttSubTopic->text().trimmed();
// Topic 不能为空
if (topic.isEmpty()) {
QMessageBox::warning(this, "提示", "请输入要订阅的Topic。");
return;
}
// 获取订阅 QoS
const quint8 qos =
static_cast<quint8>(ui->comboBox_MqttSubQos->currentText().toInt());
// 发送 SUBSCRIBE 报文
sendSubscribePacket(topic, qos);
}
真正发送订阅报文的函数是:
cpp
void FormMqttTester::sendSubscribePacket(const QString &topic, quint8 qos)
{
// MQTT SUBSCRIBE 报文需要报文 ID
const quint16 packetId = nextPacketId();
QByteArray variableHeader;
// 写入报文 ID
variableHeader.append(char(packetId >> 8));
variableHeader.append(char(packetId & 0xFF));
QByteArray payload;
// 写入订阅 Topic
appendMqttString(payload, topic);
// 写入订阅 QoS
payload.append(char(qos));
QByteArray packet;
// 0x82 表示 SUBSCRIBE 报文
packet.append(buildFixedHeader(0x82, variableHeader.size() + payload.size()));
packet.append(variableHeader);
packet.append(payload);
// 发送订阅报文
m_socket->write(packet);
m_socket->flush();
appendLog(QString("[%1] [订阅] Topic:%2,QoS:%3")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(topic)
.arg(qos));
}
发布消息的流程也类似,先判断连接状态,再读取 Topic 和 Payload:
cpp
void FormMqttTester::onPublishClicked()
{
// 未连接时不能发布
if (!m_mqttConnected) {
QMessageBox::warning(this, "提示", "请先连接MQTT Broker。");
return;
}
// 获取发布 Topic 和消息内容
const QString topic = ui->lineEdit_MqttPubTopic->text().trimmed();
const QString payload = ui->textEdit_MqttPayload->toPlainText();
// Topic 不能为空
if (topic.isEmpty()) {
QMessageBox::warning(this, "提示", "请输入发布Topic。");
return;
}
// 消息内容不能为空
if (payload.trimmed().isEmpty()) {
QMessageBox::warning(this, "提示", "发布内容不能为空。");
return;
}
// 获取发布 QoS 和 Retain 标志
const quint8 qos =
static_cast<quint8>(ui->comboBox_MqttPubQos->currentText().toInt());
const bool retain = ui->checkBox_MqttRetain->isChecked();
// 发送 PUBLISH 报文
sendPublishPacket(topic, payload, qos, retain);
}
真正组装 PUBLISH 报文:
cpp
void FormMqttTester::sendPublishPacket(
const QString &topic,
const QString &payloadText,
quint8 qos,
bool retain)
{
QByteArray variableHeader;
// PUBLISH 报文中需要写入 Topic
appendMqttString(variableHeader, topic);
// 如果 QoS 大于 0,需要带上报文 ID
if (qos > 0) {
const quint16 packetId = nextPacketId();
variableHeader.append(char(packetId >> 8));
variableHeader.append(char(packetId & 0xFF));
}
// 消息内容转成 UTF-8 字节
const QByteArray payload = payloadText.toUtf8();
// 0x30 表示普通 PUBLISH 报文
quint8 firstByte = 0x30;
// 设置 QoS 标志位
firstByte |= static_cast<quint8>((qos & 0x03) << 1);
// 如果勾选 Retain,就设置最低位
if (retain) {
firstByte |= 0x01;
}
QByteArray packet;
// 构造固定报头
packet.append(buildFixedHeader(firstByte, variableHeader.size() + payload.size()));
// 拼接 Topic 和消息内容
packet.append(variableHeader);
packet.append(payload);
// 发送 PUBLISH 报文
m_socket->write(packet);
m_socket->flush();
appendLog(QString("[%1] [发布] Topic:%2,QoS:%3,内容:%4")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(topic)
.arg(qos)
.arg(payloadText));
}
这里可以顺便解释一下 QoS 和 Retain。
QoS 表示消息服务质量:
cpp
QoS 0:最多发送一次,不保证一定到达
QoS 1:至少发送一次,需要确认,可能重复
当前工具支持 QoS 0 和 QoS 1,这对基础测试已经够用。
Retain 表示 Broker 是否保留这个 Topic 的最后一条消息。如果勾选 Retain,后续新客户端订阅这个 Topic 时,可能会立刻收到 Broker 保留的最后一条消息。
接收消息则是通过 QTcpSocket::readyRead 触发的:
cpp
void FormMqttTester::onSocketReadyRead()
{
// 把 TCP 收到的数据追加到 MQTT 接收缓冲区
m_receiveBuffer.append(m_socket->readAll());
// 解析缓冲区中的 MQTT 报文
processReceiveBuffer();
}
因为 TCP 是字节流协议,一次 readyRead 不一定刚好收到一个完整 MQTT 报文,所以这里不能简单地 readAll() 后立刻当作完整消息处理,而是需要一个缓冲区 m_receiveBuffer。
解析时,先读取 MQTT 固定报头和剩余长度,确认缓冲区里有完整报文后,再取出一个完整报文处理。当前代码中的 processReceiveBuffer() 就是在循环处理接收缓冲区,解析出完整报文后交给 handleMqttPacket()。
cpp
void FormMqttTester::handleMqttPacket(
quint8 packetType,
quint8 flags,
const QByteArray &payload)
{
switch (packetType) {
case 2:
// CONNACK:连接确认
handleConnAck(payload);
break;
case 3:
// PUBLISH:收到发布消息
handlePublish(flags, payload);
break;
case 4:
// PUBACK:QoS 1 发布确认
appendLog("收到PUBACK确认");
break;
case 9:
// SUBACK:订阅确认
handleSubAck(payload);
break;
case 13:
// PINGRESP:心跳响应
appendLog("收到PINGRESP心跳响应");
break;
default:
appendLog(QString("收到MQTT报文类型:%1,长度:%2")
.arg(packetType)
.arg(payload.size()));
break;
}
}
比如收到 CONNACK 后,才说明 MQTT 真正连接成功:
cpp
void FormMqttTester::handleConnAck(const QByteArray &payload)
{
// 收到 CONNACK 后,不再等待连接确认
m_waitingConnAck = false;
// 停止超时定时器
if (m_connAckTimer) {
m_connAckTimer->stop();
}
// CONNACK 至少需要两个字节
if (payload.size() < 2) {
appendLog("CONNACK报文长度异常");
setMqttConnectedState(false);
return;
}
// 第二个字节是返回码
const quint8 returnCode = static_cast<quint8>(payload.at(1));
if (returnCode == 0x00) {
// 返回码为 0,表示连接成功
setMqttConnectedState(true);
appendLog(QString("[%1] MQTT连接成功")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")));
} else {
// 非 0 表示连接失败
setMqttConnectedState(false);
appendLog(QString("[%1] MQTT连接失败,返回码:%2")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(returnCode));
m_socket->disconnectFromHost();
}
}
收到 PUBLISH 消息时,需要从 Payload 里解析 Topic 和消息内容:
cpp
void FormMqttTester::handlePublish(quint8 flags, const QByteArray &payload)
{
int pos = 0;
// 读取 Topic
const QString topic = readMqttString(payload, pos);
if (topic.isEmpty() || pos > payload.size()) {
appendLog("PUBLISH报文Topic解析失败");
return;
}
// 从 flags 中解析 QoS
const quint8 qos = (flags & 0x06) >> 1;
quint16 packetId = 0;
// QoS 1 需要读取报文 ID
if (qos > 0) {
if (pos + 2 > payload.size()) {
appendLog("PUBLISH报文ID解析失败");
return;
}
packetId =
(static_cast<quint8>(payload.at(pos)) << 8) |
static_cast<quint8>(payload.at(pos + 1));
pos += 2;
}
// 剩下的内容就是消息体
const QByteArray messageData = payload.mid(pos);
const QString message = QString::fromUtf8(messageData);
appendLog(QString("[%1] [接收] Topic:%2,内容:%3")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(topic)
.arg(message));
// 如果 QoS 是 1,需要回复 PUBACK
if (qos == 1) {
sendPubAck(packetId);
}
}
这一段就是 MQTT 测试工具里"接收消息"的核心。
它的逻辑可以概括成:
cpp
TCP 收到字节数据
↓
放入接收缓冲区
↓
解析 MQTT 固定报头和剩余长度
↓
判断报文类型
↓
如果是 PUBLISH,就解析 Topic 和消息内容
↓
显示到通信日志中
总结
MQTT 和 HTTP、WebSocket 最大的区别在于:MQTT 不是简单的一问一答,也不是普通的双向长连接聊天,而是基于 Broker 的发布/订阅模型。
实现一个 Qt MQTT 测试工具,可以按照下面的思路来设计:
cpp
1. 使用 QTcpSocket 连接 MQTT Broker
2. TCP 连接成功后,手动发送 MQTT CONNECT 报文
3. 等待 Broker 返回 CONNACK,确认 MQTT 连接成功
4. 通过 SUBSCRIBE 报文订阅 Topic
5. 通过 PUBLISH 报文向 Topic 发布消息
6. 通过接收缓冲区解析 Broker 返回的 MQTT 报文
7. 将连接、订阅、发布、接收和错误信息写入日志区域
从代码结构上看,几个关键函数可以这样理解:
cpp
initUi():初始化 Broker、端口、ClientId、Topic 和日志区域
initConnections():绑定按钮和 QTcpSocket 信号
onConnectClicked():检查输入并连接 Broker
onSocketConnected():TCP 连接成功后发送 MQTT CONNECT
sendConnectPacket():构造并发送 CONNECT 报文
sendSubscribePacket():构造并发送 SUBSCRIBE 报文
sendPublishPacket():构造并发送 PUBLISH 报文
processReceiveBuffer():处理 TCP 接收缓冲区
handleMqttPacket():根据 MQTT 报文类型分发处理
handlePublish():解析收到的 Topic 和消息内容
如果只是想快速实现 MQTT 客户端,也可以使用 Qt MQTT 模块里的 QMqttClient。不过手动用 QTcpSocket 拼报文的好处是:能更清楚地理解 MQTT 的底层通信过程,比如 CONNECT、CONNACK、SUBSCRIBE、SUBACK、PUBLISH、PUBACK 这些报文到底是怎么流转的。
整体来看,这个 MQTT 模块补上了网络调试助手中"发布/订阅通信"的能力。HTTP 更适合接口测试,WebSocket 更适合实时双向通信,而 MQTT 更适合物联网设备、消息推送、主题订阅和轻量级消息传输场景。