7.2.3 MQTT 协议
7.2.3 MQTT 协议
📚 本节导读
学习时长:约 35 分钟
难度级别:⭐⭐⭐⭐☆
前置知识:MQTT 协议基础、发布/订阅模型、第 7.2.1 节 Socket 基础
🎯 学习目标
- 理解 MQTT 协议的发布/订阅模型和 QoS 机制
- 掌握 Paho MQTT 客户端的使用(连接、发布、订阅、心跳)
- 掌握 Mini MQTT 轻量级客户端的使用
- 了解遗嘱消息(Last Will)的作用
- 掌握 MQTT 主题设计最佳实践
一、概述
MQTT(Message Queuing Telemetry Transport)是一种基于发布/订阅模式的轻量级物联网通信协议,具有报文小、带宽占用低、支持不稳定网络等特点,是物联网领域最广泛使用的协议之一。
OneOS 提供两种 MQTT 客户端实现:
| 实现 | 路径 | 特点 | 适用场景 |
|---|---|---|---|
| Paho MQTT | components/net/protocols/mqtt/pahomqtt-v1.1.0/ | 功能完整,支持 QoS 0/1/2,遗嘱消息,持久会话,可配 MQTT_TASK 后台线程 | 功能需求丰富的物联网设备 |
| Mini MQTT | components/net/protocols/mqtt/minimqtt/ | 代码精简,资源占用极小,API 简洁,OneOS 自研 | 资源极度受限的设备 |
二、MQTT 协议核心概念
2.1 发布/订阅模型
┌──────────┐ publish "sensor/temp" ┌──────────────┐
│ Publisher │ ─────────────────────────────► │ MQTT Broker │
│ (传感器) │ │ (消息中间件) │
└──────────┘ └──────┬───────┘
│
subscribe "sensor/temp"
│
┌────────▼──────┐
│ Subscriber │
│ (监控中心) │
└───────────────┘MQTT 协议的核心角色:
- Publisher(发布者):向指定主题发布消息
- Subscriber(订阅者):订阅感兴趣的主题,接收消息
- Broker(代理/服务器):负责消息路由和分发
- Topic(主题):消息的分类标识,支持层级通配符(
+匹配单层,#匹配多层)
2.2 QoS 服务质量
| QoS | 名称 | 描述 | 消息语义 |
|---|---|---|---|
| 0 | 最多一次(At most once) | 消息只发送一次,不确认,不重试 | 可能丢失 |
| 1 | 至少一次(At least once) | 消息保证送达,需要确认,可能重复 | 可能重复 |
| 2 | 恰好一次(Exactly once) | 消息保证恰好送达一次,四步握手 | 不丢不重 |
QoS 选择建议:
- QoS 0:适用于传感器高频上报、允许偶尔丢失的场景
- QoS 1:适用于大多数物联网场景,确保消息送达
- QoS 2:适用于关键控制指令,如远程开关机、固件升级触发等
三、Paho MQTT 详细说明
Paho MQTT 是 Eclipse Paho 的 OneOS 移植版,位于 pahomqtt-v1.1.0/。底层网络通过 MQTTOneOS.h 的 Network/Timer 结构抽象,连接流程为:网络初始化 → 客户端初始化 → 建网 → 连接 → 订阅/发布 → 周期性 MQTTYield。
3.1 客户端初始化(MQTTClientInit)
函数原型(MQTTClient.h):
DLLExport void MQTTClientInit(MQTTClient* client, Network* network, unsigned int command_timeout_ms,
unsigned char* sendbuf, size_t sendbuf_size, unsigned char* readbuf, size_t readbuf_size);参数说明:
| 参数 | 说明 |
|---|---|
client | 待初始化的 MQTTClient 结构体指针 |
network | 网络层句柄(Network,由 MQTTNetworkInit 初始化) |
command_timeout_ms | 命令超时时间(毫秒),如 COMMAND_TIMEOUT=30000 |
sendbuf/sendbuf_size | 发送缓冲及大小(如 unsigned char sendbuf[512]) |
readbuf/readbuf_size | 接收缓冲及大小 |
3.2 连接选项结构体(MQTTPacket_connectData)
完整源码(MQTTPacket.h):
typedef struct
{
/** The eyecatcher for this structure. must be MQTC. */
char struct_id[4];
/** The version number of this structure. Must be 0 */
int struct_version;
/** Version of MQTT to be used. 3 = 3.1 4 = 3.1.1 */
unsigned char MQTTVersion;
MQTTString clientID;
unsigned short keepAliveInterval;
unsigned char cleansession;
unsigned char willFlag;
MQTTPacket_willOptions will;
MQTTString username;
MQTTString password;
} MQTTPacket_connectData;字段详解:
| 字段 | 说明 |
|---|---|
MQTTVersion | MQTT 协议版本:3 = v3.1,4 = v3.1.1 |
clientID | 客户端唯一标识(类型 MQTTString,用 .cstring 赋值) |
keepAliveInterval | 心跳间隔(秒),建议 30~120 |
cleansession | 是否清理会话(1 清理) |
willFlag | 是否启用遗嘱消息(1 启用) |
will | 遗嘱消息详情(MQTTPacket_willOptions:主题/消息/QoS/retained) |
username/password | 认证用户名/密码(MQTTString) |
struct_id/struct_version | 结构体标识,由 MQTTPacket_connectData_initializer 自动填充 |
关键理解(重要):不要手动初始化该结构体的全部字段,应先用初始化宏
MQTTPacket_connectData_initializer赋初值,再按需覆盖MQTTVersion/keepAliveInterval/cleansession/clientID.cstring等。clientID类型是MQTTString,赋值用connectData.clientID.cstring = "xxx",不是const char*直接赋值。
3.3 遗嘱消息(Last Will and Testament)
当客户端非正常断开连接时(断网/掉电),Broker 会自动把遗嘱消息发布到指定主题,用于检测设备离线状态。Paho 中通过 willFlag=1 + will 结构体配置:
connectData.willFlag = 1;
connectData.will.topicName.cstring = "device/001/status";
connectData.will.message.cstring = "offline";
connectData.will.qos = QOS1;
connectData.will.retained = 1;关键理解:
will的topicName/message也是MQTTString(.cstring赋值)。设置willFlag=1后,若客户端非正常离线,Broker 会向will.topicName发布will.message。
3.4 连接/发布/订阅/心跳 API
核心函数原型(MQTTClient.h):
DLLExport int MQTTConnect(MQTTClient* client, MQTTPacket_connectData* options);
DLLExport int MQTTSubscribe(MQTTClient* client, const char* topicFilter, enum QoS, messageHandler);
DLLExport int MQTTUnsubscribe(MQTTClient* client, const char* topicFilter);
DLLExport int MQTTPublish(MQTTClient* client, const char*, MQTTMessage*);
DLLExport int MQTTSetMessageHandler(MQTTClient* c, const char* topicFilter, messageHandler messageHandler);
DLLExport int MQTTYield(MQTTClient* client, int time);
DLLExport int MQTTIsConnected(MQTTClient* client);
DLLExport int MQTTDisconnect(MQTTClient* client);
DLLExport void MQTTClientDeInit(MQTTClient* c);关键理解:
MQTTPublish的第三个参数是MQTTMessage*(消息结构体),不是裸的payload字符串;需要先填MQTTMessage message; message.qos=...; message.payload=...; message.payloadlen=...再传入;MQTTYield(client, time)必须周期性调用(非MQTT_TASK模式),它负责维持心跳、处理收包和派发到订阅回调;time为每次让出的毫秒数;- 若定义了
MQTT_TASK,则用MQTTStartTask/MQTTEndTask启动后台线程,此时不再调用MQTTYield; MQTTConnect需在网络已连接(MQTTNetworkConnect成功)后才可调用;- 返回码
SUCCESS(0)成功、FAILURE(-1)失败、BUFFER_OVERFLOW(-2)。
消息结构体 MQTTMessage(MQTTClient.h):
typedef struct MQTTMessage
{
enum QoS qos;
unsigned char retained;
unsigned char dup;
unsigned short id;
void *payload;
size_t payloadlen;
} MQTTMessage;3.5 Paho MQTT 完整示例
以下示例参考官方 MQTTClient-OneOS/samples/mqtt_sample.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <os_task.h>
#include <os_assert.h>
#include <dlog.h>
#include <oneos_config.h>
#include "MQTTOneOS.h"
#include "MQTTClient.h"
#define MQ_SAMPLE_TAG "MQ_SAMPLE"
#define TCP_SERVER_ADDR "test.mosquitto.org"
#define TCP_SERVER_PORT 1883
#define COMMAND_TIMEOUT 30000
typedef struct
{
Network network;
MQTTClient client;
} mqtt_context_t;
static void messageArrived(MessageData *data)
{
os_kprintf("Message arrived on topic %.*s: %.*s\r\n",
data->topicName->lenstring.len,
data->topicName->lenstring.data,
data->message->payloadlen,
data->message->payload);
}
void mqtt_sample_task_func(void *arg)
{
mqtt_context_t mqtt_context = {0};
unsigned char sendbuf[512] = {0};
unsigned char readbuf[512] = {0};
int rc = 0;
int count = 0;
char addr[] = TCP_SERVER_ADDR;
int port = TCP_SERVER_PORT;
MQTTPacket_connectData connectData = MQTTPacket_connectData_initializer;
/* 1. 初始化网络层与客户端 */
MQTTNetworkInit(&mqtt_context.network, addr, port, NULL);
MQTTClientInit(&mqtt_context.client,
&mqtt_context.network,
COMMAND_TIMEOUT,
sendbuf, sizeof(sendbuf),
readbuf, sizeof(readbuf));
/* 2. 建立 TCP 网络连接 */
rc = MQTTNetworkConnect(&mqtt_context.network);
if (0 != rc) {
LOG_E(MQ_SAMPLE_TAG, "Establish network failed, check IP and PORT");
return;
}
LOG_I(MQ_SAMPLE_TAG, "Establish network sucess");
/* 3. 设置连接选项并连接 Broker */
connectData.MQTTVersion = 4; /* 3 = 3.1 4 = 3.1.1 */
connectData.keepAliveInterval = 60;
connectData.cleansession = 1;
connectData.willFlag = 0;
connectData.clientID.cstring = "OneOS_MQTTClient";
if ((rc = MQTTConnect(&mqtt_context.client, &connectData)) != 0) {
LOG_E(MQ_SAMPLE_TAG, "MQTT connect failed, return code is %d", rc);
goto exit;
}
LOG_I(MQ_SAMPLE_TAG, "MQTT Connected");
/* 4. 订阅主题 */
if ((rc = MQTTSubscribe(&mqtt_context.client, "/public/TEST/mqtest", QOS1, messageArrived)) != 0) {
LOG_E(MQ_SAMPLE_TAG, "MQTT subscribe failed, return code is %d", rc);
goto exit;
}
LOG_I(MQ_SAMPLE_TAG, "MQTT Subscribed");
/* 5. 周期发布消息(并用 MQTTYield 维持心跳与收包) */
while (count < 10) {
MQTTMessage message;
char payload[128];
++count;
message.qos = QOS1;
message.retained = 0;
message.payload = payload;
sprintf(payload, "message number %d", count);
message.payloadlen = strlen(payload);
if ((rc = MQTTPublish(&mqtt_context.client, "/public/TEST/mqtest", &message)) != 0) {
LOG_E(MQ_SAMPLE_TAG, "MQTT publish failed, return code is %d", rc);
goto exit;
}
#if !defined(MQTT_TASK)
if ((rc = MQTTYield(&mqtt_context.client, 1000)) != 0) {
LOG_E(MQ_SAMPLE_TAG, "Return code from yield is %d", rc);
goto exit;
}
#endif
}
LOG_I(MQ_SAMPLE_TAG, "MQTT sample terminate.");
exit:
if (MQTTIsConnected(&mqtt_context.client) == 1)
MQTTDisconnect(&mqtt_context.client);
MQTTNetworkDisconnect(&mqtt_context.network);
MQTTClientDeInit(&mqtt_context.client);
}分步详解:
- 初始化:
MQTTNetworkInit(网络层)+MQTTClientInit(客户端,含 sendbuf/readbuf); - 建网:
MQTTNetworkConnect建立 TCP 连接; - 连接:用初始化宏初始化
connectData,覆盖MQTTVersion/keepAliveInterval/cleansession/clientID.cstring,再MQTTConnect; - 订阅:
MQTTSubscribe(client, topic, qos, handler),handler 收到消息回调; - 发布:填充
MQTTMessage后MQTTPublish,随后MQTTYield(1000)维持心跳并派发收包; - 清理:
MQTTIsConnected判断后MQTTDisconnect,再MQTTNetworkDisconnect+MQTTClientDeInit。
四、Mini MQTT
Mini MQTT 是 OneOS 自研的轻量级 MQTT 客户端,位于 minimqtt/,API 更简洁,源码注释为中文(以下结构体直接引用源码)。
4.1 参数/连接/消息结构体
完整源码(m_mqtt_client.h + m_mqtt_def.h):
typedef struct
{
uint32_t timeout; /* 消息超时时间,毫秒 */
size_t sendbuf_size; /* mqtt实例发送区缓存大小 */
size_t recvbuf_size; /* mqtt实例接收区缓存大小 */
} m_mqtt_param;
typedef struct
{
char *server_addr; /* 服务器地址 */
char *port; /* 服务器端口 */
uint32_t keep_alive; /* 保活时间,秒 */
char *id; /* 客户端ID*/
char *username; /* 用户名 */
char *password; /* 密码 */
} m_mqtt_connect_param;
typedef struct
{
m_mqtt_qos qos; /* qos等级 */
void *payload; /* 消息主体 */
size_t payloadlen; /* 消息长度 */
} m_mqtt_message;QoS 与错误码枚举(m_mqtt_def.h):
typedef enum
{
mqtt_qos0,
mqtt_qos1,
mqtt_qos2,
mqtt_sub_fail = 0x80
} m_mqtt_qos;
typedef enum
{
ERR_M_MQTT_SUCCESS = 0, /* 成功 */
ERR_M_MQTT_GENERAL = -1, /* 通用错误码 */
ERR_M_MQTT_PARAM = -2, /* 输入错误 */
ERR_M_MQTT_OUT_OF_MEMORY = -3, /* 内存不足 */
ERR_M_MQTT_NET = -4, /* 网络错误 */
ERR_M_MQTT_TX_BUFF_SHORT = -5, /* TX缓存不足 */
ERR_M_MQTT_RX_DATA = -6, /* 接收数据读取错误 */
ERR_M_MQTT_SERVER = -7, /* 服务端返回错误 */
ERR_M_MQTT_NOT_SUPPORT_TYPE = -8, /* 不支持的服务类型 */
ERR_M_MQTT_NOT_CONNECT = -9, /* 未连接到服务器 */
ERR_M_MQTT_NOT_SUPPORT_QOS2 = -10, /* 不支持QoS2 */
} m_mqtt_err_code;关键理解:
m_mqtt_qos取值mqtt_qos0=0、mqtt_qos1=1、mqtt_qos2=2。注意 Mini MQTT 返回ERR_M_MQTT_NOT_SUPPORT_QOS2(-10),说明其可能不支持 QoS2;错误码均 ≤ 0,ERR_M_MQTT_SUCCESS(0)成功。
4.2 核心 API 参考
完整源码(头文件声明):
void *m_mqtt_create(const m_mqtt_param *param);
void m_mqtt_destory(void *mqtt_handle);
int m_mqtt_connect(void *mqtt_handle, const m_mqtt_connect_param *connect_param);
void m_mqtt_disconnect(void *mqtt_handle);
int m_mqtt_subscribe(void *mqtt_handle,
const char *topic_filter,
m_mqtt_qos qos,
m_mqtt_message_handler message_handler);
int m_mqtt_unsubscribe(void *mqtt_handle, const char *topic_filter);
int m_mqtt_publish(void *mqtt_handle, const char *topic, m_mqtt_message *message);
int m_mqtt_ping_request(void *mqtt_handle);
int m_mqtt_get_state(void *mqtt_handle);
#if !defined(NET_USING_MINI_MQTT_AUTO_YIELD)
int m_mqtt_yield(void *mqtt_handle);
#endif /* NET_USING_MINI_MQTT_AUTO_YIELD */消息回调类型(m_mqtt_def.h):
typedef void (*m_mqtt_message_handler)(void *mqtt_handle, const char *topic_name, void *payload, size_t payload_len);关键理解:
- 句柄式 API:
m_mqtt_create返回void *mqtt_handle,后续所有操作都传句柄(不是结构体指针); m_mqtt_publish的第三个参数是m_mqtt_message*(含qos/payload/payloadlen),不是裸字符串;m_mqtt_yield(handle)必须周期性调用(除非定义NET_USING_MINI_MQTT_AUTO_YIELD自动让出),否则无法收包/维持心跳;m_mqtt_ping_request主动发心跳;m_mqtt_get_state返回mqtt_not_connected/mqtt_connected。
4.3 Mini MQTT 使用示例
#include <string.h>
#include <os_kernel.h>
#include <oneos_config.h>
#include "m_mqtt_client.h"
static void message_handler(void *mqtt_handle, const char *topic_name, void *payload, size_t payload_len)
{
os_kprintf("[MiniMQTT] topic: %s\r\n", topic_name);
os_kprintf("[MiniMQTT] payload: %.*s\r\n", (int)payload_len, (char *)payload);
}
void minimqtt_example(void)
{
void *handle = NULL;
int ret;
/* 1. 创建实例(配置超时与缓冲大小) */
m_mqtt_param param = {0};
param.timeout = 5000; /* 消息超时,毫秒 */
param.sendbuf_size = 512; /* 发送区缓存大小 */
param.recvbuf_size = 512; /* 接收区缓存大小 */
handle = m_mqtt_create(¶m);
if (handle == NULL) {
os_kprintf("[MiniMQTT] create failed\r\n");
return;
}
/* 2. 连接 Broker */
m_mqtt_connect_param connect_param = {0};
connect_param.server_addr = "broker.emqx.io";
connect_param.port = "1883";
connect_param.keep_alive = 60;
connect_param.id = "oneos_mini_001";
connect_param.username = NULL;
connect_param.password = NULL;
ret = m_mqtt_connect(handle, &connect_param);
if (ret != ERR_M_MQTT_SUCCESS) {
os_kprintf("[MiniMQTT] connect failed: %d\r\n", ret);
m_mqtt_destory(handle);
return;
}
os_kprintf("[MiniMQTT] connected!\r\n");
/* 3. 订阅主题 */
ret = m_mqtt_subscribe(handle, "test/topic", mqtt_qos1, message_handler);
if (ret != ERR_M_MQTT_SUCCESS) {
os_kprintf("[MiniMQTT] subscribe failed: %d\r\n", ret);
}
/* 4. 发布消息 */
m_mqtt_message message;
message.qos = mqtt_qos1;
message.payload = (void *)"Hello MiniMQTT";
message.payloadlen = strlen("Hello MiniMQTT");
ret = m_mqtt_publish(handle, "test/topic", &message);
if (ret != ERR_M_MQTT_SUCCESS) {
os_kprintf("[MiniMQTT] publish failed: %d\r\n", ret);
}
/* 5. 周期让出(收包 + 心跳) */
while (1) {
ret = m_mqtt_yield(handle);
if (ret != ERR_M_MQTT_SUCCESS) {
os_kprintf("[MiniMQTT] yield error: %d, connection lost\r\n", ret);
break;
}
os_task_tsleep(500);
}
/* 6. 断开与销毁 */
m_mqtt_disconnect(handle);
m_mqtt_destory(handle);
}分步详解:
m_mqtt_param配超时与缓冲大小,m_mqtt_create创建句柄;m_mqtt_connect_param填服务器地址/端口(字符串"1883")/保活/客户端 ID,m_mqtt_connect连接;m_mqtt_subscribe(handle, topic, qos, handler)订阅,回调签名(handle, topic, payload, payload_len);- 填
m_mqtt_message后m_mqtt_publish; m_mqtt_yield周期性让出处理收发;- 结束后
m_mqtt_disconnect+m_mqtt_destory释放句柄。
五、MQTT 主题设计最佳实践
主题命名规范:
{产品}/{设备ID}/{功能}
示例:
sensor/temp001/data -- 温度传感器数据上报
sensor/temp001/cmd -- 温度传感器命令下发
gateway/001/status -- 网关状态
device/+/event -- 所有设备的事件(单层通配符)
device/# -- 所有设备的所有消息(多层通配符)设计原则:
- 主题层级不宜过深(建议不超过 5 层)
- 不要以
/开头或结尾 - 使用可读的层级名称,避免缩写混淆
- 区分数据上报(data)和命令下发(cmd)主题
- 为每个设备预留状态主题(status),配合遗嘱消息实现在线/离线检测
📝 本节小结
核心要点回顾
- 两种实现:Paho MQTT(功能完整、IBM 移植版,支持 QoS 0/1/2、遗嘱、
MQTT_TASK后台线程)与 Mini MQTT(OneOS 自研、句柄式、资源极小)。 - Paho 初始化:
MQTTNetworkInit+MQTTClientInit(含 sendbuf/readbuf)→MQTTNetworkConnect→ 配置connectData→MQTTConnect。 - 连接选项:
MQTTPacket_connectData用初始化宏MQTTPacket_connectData_initializer初始化,字段为MQTTVersion/clientID(MQTTString,.cstring赋值)/keepAliveInterval/cleansession/willFlag/will/username/password。 - 消息封装:
MQTTPublish/m_mqtt_publish都要求把 payload 封装进消息结构体(MQTTMessage/m_mqtt_message),不是传裸字符串。 - 心跳关键:Paho 用
MQTTYield(client, time)(或MQTT_TASK下用MQTTStartTask),Mini 用m_mqtt_yield(handle)(除非NET_USING_MINI_MQTT_AUTO_YIELD)——必须周期性调用才能收包和维持连接。 - 遗嘱消息:Paho
willFlag=1+will结构体(topicName/message/qos/retained),客户端非正常离线时 Broker 自动发布遗嘱到指定主题,用于检测离线。 - 返回码:Paho
SUCCESS(0)/FAILURE(-1);MiniERR_M_MQTT_SUCCESS(0),负数对应具体错误(且可能不支持 QoS2)。 - 清理:Paho
MQTTDisconnect+MQTTNetworkDisconnect+MQTTClientDeInit;Minim_mqtt_disconnect+m_mqtt_destory。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
用 MQTTClient_init/connect/publish/cycle 等 API | 这些 API 不存在 → 用 MQTTClientInit/MQTTConnect/MQTTPublish/MQTTYield |
clientID 直接赋 const char* | clientID 是 MQTTString → 用 connectData.clientID.cstring = "xxx" |
忘记 MQTTPacket_connectData_initializer | 结构体含 struct_id/struct_version 必须初始化 → 用初始化宏再覆盖字段 |
| 把 payload 直接传给 publish | MQTTPublish 第三参是 MQTTMessage* → 先填 message.qos/payload/payloadlen 再传 &message |
忘记周期调用 MQTTYield/m_mqtt_yield | 不 yield 则收不到订阅消息、心跳会断 → 主循环周期性调用 |
混淆 MQTT_TASK 模式 | 定义 MQTT_TASK 后要用 MQTTStartTask,不再调 MQTTYield |
| Mini MQTT 端口传整数 | m_mqtt_connect_param.port 是 char* → 传 "1883" 字符串 |
用 mqtt_connect/publish/loop 等 API | Mini MQTT 是 m_mqtt_create/connect/publish/yield → 注意 m_mqtt_ 前缀 |
Mini MQTT 返回判 >= 0 成功 | 错误码是负数、成功是 ERR_M_MQTT_SUCCESS(0) → 用 == ERR_M_MQTT_SUCCESS |
| Mini MQTT 用 QoS2 | 可能返回 ERR_M_MQTT_NOT_SUPPORT_QOS2(-10) → 确认组件是否支持 QoS2 |
下一步
- 7.2.4 CoAP 协议:MQTT 是发布/订阅的物联网协议,下一节学习面向资源受限设备、基于 REST 风格的 CoAP 协议,掌握 OneOS CoAP 客户端的请求/响应与资源发现,针对超轻量场景做选型。