MQTT 物联网消息协议
MQTT 物联网消息协议
一、概述
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是专为物联网设计的发布/订阅消息协议,基于 TCP,报文小巧、带宽占用低、支持不稳定网络,是物联网最广泛使用的协议。
一句话核心:设备(Publisher)向 Broker(中间件) 的某个主题(Topic)发布消息,其他设备(Subscriber)订阅该主题即可收到——发布者和订阅者互不感知,通过 Broker 解耦。
二、发布/订阅模型
发布者(Publisher) Broker 订阅者(Subscriber)
│ │ │
├─publish "temp/25"───────────►│ │
│ ├────────publish─────────►│
│ │ (Broker转发给订阅者) │
│ │ │
│ │◄─subscribe "temp/#"────│| 角色 | 作用 |
|---|---|
| Publisher(发布者) | 向主题发布消息 |
| Subscriber(订阅者) | 订阅主题接收消息 |
| Broker(代理/服务器) | 接收、路由、分发消息(如 Mosquitto、EMQX) |
关键理解:发布者不需要知道谁在订阅,订阅者不需要知道谁在发布。Broker 负责把消息路由给所有匹配主题的订阅者。这种解耦让系统扩展性极强。
三、主题(Topic)与通配符
主题是层级结构,用 / 分隔:
例子: sensor/temp
home/livingroom/light
device/001/status通配符:
| 通配符 | 匹配规则 | 例子 |
|---|---|---|
+ | 匹配单层 | sensor/+/temp 匹配 sensor/1/temp、sensor/2/temp |
# | 匹配多层(必须放末尾) | sensor/# 匹配 sensor/temp、sensor/1/temp |
数值演算(主题匹配):
订阅 "sensor/+/temp" :
✓ sensor/1/temp
✓ sensor/2/temp
✗ sensor/1/humidity (层级不符)
✗ sensor/1/2/temp (+只匹配一层)
订阅 "sensor/#" :
✓ sensor/temp
✓ sensor/1/temp
✓ sensor/1/2/3/temp (#匹配多层)关键理解:
+匹配恰好一层,#匹配剩余所有层且只能在主题末尾。主题设计要注意层次规范(如设备ID/数据类型/数据名)。
四、QoS(服务质量)
MQTT 提供三级 QoS,控制消息可靠性与开销:
4.1 QoS 0(最多一次)
消息发一次,不确认不重发,可能丢失。
发布者 ──publish──► Broker ──publish──► 订阅者
(无确认)4.2 QoS 1(至少一次)
保证送达,靠 PUBACK 确认,可能重复。
发布者 ──publish(含msgid)──► Broker
发布者 ◄──── PUBACK ────────
(未收到 PUBACK 则重发,可能重复)4.3 QoS 2(恰好一次)
四步握手,不丢不重,开销最大。
发布者 ──publish(含msgid)──► Broker
发布者 ◄──── PUBREC ────────
发布者 ──PUBREL──────────►
发布者 ◄──── PUBCOMP ──────数值演算(QoS2 四步握手):假设发布者向主题 dev-001/data 发布负载 "25.5",本次分配消息标识 msgid = 10,完整报文交互如下:
① PUBLISH(类型3, msgid=10, 主题=dev-001/data, 负载="25.5")
发布者 ───────────────────────────────► Broker
(Broker 收到后记录 msgid=10 待确认,但此时不转发给订阅者)
② PUBREC(类型5, msgid=10) 【PUBlish RECeived 已收到】
发布者 ◄─────────────────────────────── Broker
(Broker 确认已收到该消息,等待发布者的释放指令)
③ PUBREL(类型6, msgid=10) 【PUBlish RELease 释放】
发布者 ───────────────────────────────► Broker
(Broker 收到 PUBREL 后才把消息转发给订阅者)
④ PUBCOMP(类型7, msgid=10) 【PUBlish COMPlete 完成】
发布者 ◄─────────────────────────────── Broker
(握手完成,双方都丢弃 msgid=10 的握手状态,本次投递结束)关键理解(为什么"恰好一次"):
- 因为 PUBREL 才触发 Broker 转发,所以即使
PUBREC或PUBCOMP丢失,发布者重发 PUBREL 也不会导致消息被重复投递给订阅者——Broker 只会在收到第一个 PUBREL 时转发一次,后续重复的 PUBREL 直接忽略。 - 握手过程中双方都要保存
msgid状态,消息需要经历 4 个报文,因此 QoS2 开销最大、吞吐最低,一般只用于"漏发/重发都不可接受"的关键指令。
具体代码示例(QoS2 发布):
/* 关键控制指令用 QoS2,确保恰好执行一次 */
uint16_t msgid = broker_next_id(); // 分配唯一 msgid,例如 10
mqtt_publish(broker, "dev-001/data", "25.5",
qos = 2, msgid = msgid); // ① 发 PUBLISH
/* 库内部自动完成 ②PUBREC → ③PUBREL → ④PUBCOMP 握手
* 只有收到 PUBCOMP 才算发布成功 */
while (!mqtt_pubcomp_received(broker, msgid)) {
if (timeout) {
mqtt_pubrel(broker, msgid); // 超时重发 PUBREL,而非重发 PUBLISH
}
}易错点:QoS2 超时后应重发 PUBREL(而非重发 PUBLISH)。若重发 PUBLISH,会再次触发一次完整的四步握手,Broker 无法判断前后是否为同一条消息,可能造成重复执行。
QoS 选择:
| QoS | 可靠性 | 开销 | 场景 |
|---|---|---|---|
| 0 | 可能丢 | 最小 | 高频传感、容忍丢失 |
| 1 | 可能重 | 中 | 大多数物联网(推荐) |
| 2 | 不丢不重 | 大 | 关键控制指令 |
注意:发布 QoS 和订阅 QoS 可不同。Broker 向订阅者转发的实际 QoS = min(发布 QoS, 订阅 QoS)。
五、MQTT 报文格式
MQTT 报文由固定头(Fixed Header) + 可变头(Variable Header) + 负载(Payload)三段组成,其中固定头是每个报文都必须有的,可变头和负载视报文类型而定:
一个 MQTT 报文 = 固定头 + 可变头 + 负载
┌────────────────────────┬─────────────────────┬──────────────┐
│ 固定头 │ 可变头 │ 负载 │
│ Fixed Header │ Variable Header │ Payload │
├────────────────────────┼─────────────────────┼──────────────┤
│ ①字节1: 报文类型+标志 │ 随报文类型而不同: │ 消息内容 │
│ ②字节2+: 剩余长度(变长) │ msgid、主题、遗嘱、 │ (可空) │
│ (每个报文必有) │ 保留/遗嘱标志等 │ │
└────────────────────────┴─────────────────────┴──────────────┘
↑ 固定头拆开看:
字节1 字节2 ... 字节 N
┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌──────┬──────┬──────┐
│报文类型│ │D│Q│R│ │ 剩余长度(变长编码, 每字节低7位) │
└─┴─┴─┴─┴─┴─┴─┴─┘ └──────┴──────┴──────┘
(高4位: 1~14报文类型) (最高位=1 表示还有后续字节)三段各自内容说明:
| 段 | 是否必有 | 内容 |
|---|---|---|
| 固定头 | ✅ 每个报文必有 | 报文类型 + 标志(字节1)、剩余长度(字节2起,变长) |
| 可变头 | 视类型 | msgid、主题名、遗嘱/保留标志等,随报文类型变化 |
| 负载 | 视类型 | 消息正文,PUBLISH 为数据内容,可空 |
报文类型(字节1 高 4 位):
| 类型 | 值 | 作用 |
|---|---|---|
| CONNECT | 1 | 连接请求 |
| CONNACK | 2 | 连接确认 |
| PUBLISH | 3 | 发布消息 |
| PUBACK/PUBREC/PUBREL/PUBCOMP | 4~7 | QoS 确认 |
| SUBSCRIBE | 8 | 订阅请求 |
| SUBACK | 9 | 订阅确认 |
| UNSUBSCRIBE/UNSUBACK | 10/11 | 取消订阅 |
| PINGREQ/PINGRESP | 12/13 | 心跳 |
| DISCONNECT | 14 | 断开 |
剩余长度用变长编码(每字节低 7 位,最高位表续)。
PUBLISH 报文:
[类型3][剩余长度][主题长度2B][主题][msgid(若QoS>0)][负载]数值演算(PUBLISH 报文大小):发布 QoS0,主题 temp,负载 25.5:
固定头: 1 字节类型 + 1 字节长度 = 2
可变头: 主题长度(2) + 主题"temp"(4) = 6
负载: 4 ("25.5")
总大小: 2 + 6 + 4 = 12 字节具体示例一:剩余长度变长编码。剩余长度表示"可变头 + 负载"的字节数。规则是每字节低 7 位存数值,最高位(第 8 位)=1 表示还有后续字节:
例A: 剩余长度 = 5 (较小)
二进制 0000101 → 1 个字节: 0x05
(因为 <128,一个字节即可,最高位=0)
例B: 剩余长度 = 300 (超过 1 字节能表示的 127)
300 = 2×128 + 44
第1字节: 44 | 0x80 = 0xAC (低7位存44, 置最高位表示还有后续)
第2字节: 2 (低7位存2, 最高位=0 表示结束)
故编码为: [0xAC][0x02]
验证: 44 + 2×128 = 44 + 256 = 300 ✓演算要点:先把剩余长度除以 128,余数放进当前字节并置最高位=1,商继续作为下一轮,直到商<128 时放入末字节并置最高位=0。
具体示例二:完整 PUBLISH 报文(十六进制)。发布 QoS1、主题 temp、负载 25.5、msgid=10,逐字节构造如下:
固定头: [0x32] 类型3(QoS1) → 高4位=0011, DUP=0, QoS=01, Retain=0 → 0x32
[0x0C] 剩余长度 = 12 (见下方核算)
可变头: [0x00][0x04] 主题长度 = 4
[0x74][0x65][0x6D][0x70] 主题 "temp" (ASCII: t=0x74,e=0x65,m=0x6D,p=0x70)
[0x00][0x0A] msgid = 10
负载: [0x32][0x35][0x2E][0x35] "25.5" (ASCII: 2=0x32,5=0x35, .=0x2E,5=0x35)
完整字节流:
0x32 0x0C | 0x00 0x04 0x74 0x65 0x6D 0x70 0x00 0x0A | 0x32 0x35 0x2E 0x35
└─固定头─┘ └────────────── 可变头(8B) ─────────────┘ └─负载(4B)─┘
剩余长度核算:
可变头 = 主题长度2 + 主题"temp"4 + msgid2 = 8
负载 = 4 ("25.5")
剩余长度 = 可变头8 + 负载4 = 12 → 编码为 0x0C易错点:剩余长度只统计可变头 + 负载,不包含固定头本身。本例若把固定头的 2 字节也算进去写成 14 就错了。另外,QoS1 的 PUBLISH 必须携带
msgid,因此其剩余长度比同样的 QoS0 报文多 2 字节(msgid 占 2 字节)。
六、保留消息(Retained)
发布时设 Retain=1,Broker 会保存该主题的最后一条消息,新订阅者一订阅就立即收到这条保留消息:
发布者 publish(topic="status", "online", retain=1)
→ Broker 保存 "status"="online"
新订阅者 subscribe("status") → 立即收到 "online" (无需等下次发布)应用:设备状态、版本号等"最后状态"适合用保留消息,新客户端上线立即同步状态。
七、遗嘱消息(Last Will)
客户端异常断开(断网/掉电)时,Broker 自动帮它发布遗嘱消息到指定主题:
客户端 connect(设 will=1, willTopic="dev/1/status", willMsg="offline")
→ 正常连接
客户端异常断线(无 DISCONNECT)
→ Broker 自动 publish "dev/1/status" = "offline"应用:在线/离线检测——设备上线发 online,正常下线发 DISCONNECT(Broker 不发布遗嘱),异常掉线由 Broker 发 offline。订阅方据此判断设备状态。
八、心跳(Keep Alive)
客户端在连接时设定 Keep Alive(秒),周期性发 PINGREQ,Broker 回 PINGRESP:
客户端 ──PINGREQ──► Broker
客户端 ◄─PINGRESP──
(Keep Alive 周期内无任何报文,就发 PINGREQ 保活)- Broker 在 1.5×Keep Alive 内未收到任何包,判定客户端离线(触发遗嘱);
- Keep Alive 设 0 表示不启用。
数值演算(心跳):设 Keep Alive = 60s:
客户端每 60s 内若无其它报文,发 PINGREQ
Broker 在 90s(1.5×60) 内未收到任何包 → 判离线九、MQTT 连接流程
客户端 Broker
1. CONNECT(含clientID, 用户名/密码, 遗嘱, keepalive)
2. ◄─ CONNACK(0=成功)
3. SUBSCRIBE/PUBLISH 交互
4. (周期性 PINGREQ/PINGRESP 心跳)
5. DISCONNECT (正常断开)ClientID 要求:同一 Broker 下 ClientID 唯一(重复会导致旧连接被踢)。
十、MQTT 与 HTTP 对比
| 维度 | MQTT | HTTP |
|---|---|---|
| 模型 | 发布/订阅 | 请求/响应 |
| 传输 | TCP | TCP |
| 服务端推送 | 支持(订阅即推送) | 需轮询/长连接 |
| 报文大小 | 小 | 较大 |
| 保持连接 | 心跳 | 无内置 |
| 场景 | 物联网实时双向 | Web/REST API |
关键理解:HTTP 是"客户端主动问",MQTT 是"订阅即被动收"——MQTT 天然支持服务端向设备实时推送(无需设备频繁轮询),更适合物联网。
十一、常见坑与排查
| 问题 | 可能原因 | 排查 |
|---|---|---|
| 连接被拒绝 | ClientID 重复/认证失败 | 换唯一 ClientID,核对用户名密码 |
| 收不到订阅消息 | 主题/通配符不匹配、QoS=0 | 核对主题,注意 QoS |
| QoS2 卡住 | 不完整握手 | 检查是否支持 QoS2 |
| 异常断线 | KeepAlive 太短/网络差 | 调大 KeepAlive,检查网络 |
| 重复收到消息 | QoS1 重发 | 用 QoS2 或做去重 |
| 离线检测不准 | 未设遗嘱/保留 | 配好遗嘱+保留消息 |
| 消息乱码 | 编码/JSON 格式 | 统一 UTF-8 和 JSON 格式 |