3. MQTT协议核心原理:发布/订阅模型、主题与通配符、服务质量、保留消息与遗嘱消息、报文结构解析
MQTT 这玩意儿,说白了就是物联网世界的「消息快递」。我做了这么多年嵌入式网关,跟它打交道最多。今天咱们就把它的核心原理掰开揉碎,讲清楚。
3.1 发布/订阅模型:解耦的艺术
传统的客户端-服务器模型,就像打电话——双方必须同时在线。MQTT 不一样,它用的是发布/订阅模型。你可以把它想象成一个微信群:有人发消息(发布者),有人看消息(订阅者),中间有个群聊服务器(Broker)负责转发。
这样做的好处很明显:
- 空间解耦:发布者和订阅者不需要知道对方在哪
- 时间解耦:发布者发消息时,订阅者可以不在线
- 同步解耦:双方不需要同时阻塞等待
核心角色:
- Publisher(发布者):发送消息的客户端,比如温度传感器
- Subscriber(订阅者):接收消息的客户端,比如监控平台
- Broker(代理服务器):负责消息路由和转发,比如 Mosquitto、EMQX
我在项目中遇到过一个问题:有个同事把传感器直接跟云端服务器做 TCP 长连接,结果服务器一重启,所有设备全断了。换成 MQTT Broker 之后,设备只管发消息,服务器重启也不影响数据上报。这就是解耦的价值。
3.2 主题(Topic)与通配符:消息的地址系统
主题就是消息的「地址」。它用斜杠分层,比如:
sensor/temperature/room1
sensor/humidity/room2
actuator/light/room1/switch
主题的命名规则很简单:
- 区分大小写:
Sensor/Temp和sensor/temp是两个不同的主题 - 不能以
$开头(系统保留) - 建议用有意义的层级结构
通配符是 MQTT 的杀手锏。订阅者可以用它一次订阅多个主题:
| 通配符 | 含义 | 示例 | 匹配结果 |
|---|---|---|---|
+ |
匹配单层 | sensor/+/temp |
匹配 sensor/room1/temp、sensor/room2/temp |
# |
匹配多层 | sensor/# |
匹配 sensor/temp、sensor/temp/room1 等所有以 sensor/ 开头的主题 |
注意:# 通配符只能放在主题末尾。比如 sensor/#/temp 是无效的。我曾经见过有人写 +/# 想匹配所有主题,结果 Broker 直接报错。
你想想看,如果网关要采集所有温度数据,只需要订阅 sensor/+/temperature 就够了。不用为每个传感器单独写订阅逻辑。
3.3 服务质量(QoS 0/1/2):消息可靠性的三档选择
MQTT 定义了三个服务质量等级。说白了就是:你愿意花多少代价保证消息送到。
| QoS | 名称 | 发送次数 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 0 | 至多一次 | 1 次 | 最低 | 传感器数据、日志(丢了无所谓) |
| 1 | 至少一次 | ≥1 次 | 中等 | 控制指令、告警(必须到,重复可接受) |
| 2 | 恰好一次 | 2 次握手 | 最高 | 计费、关键状态(不能丢也不能重复) |
QoS 0 最简单:发出去就不管了。适合温度、湿度这种连续上报的数据,丢一帧无所谓。
QoS 1 会保证消息至少到达一次。如果 Broker 没回复 PUBACK,发布者会重发。但可能重复。我在项目中用 QoS 1 下发控制指令,配合去重逻辑,效果很好。
QoS 2 最复杂,需要四次交互(PUBLISH → PUBREC → PUBREL → PUBCOMP)。开销大,但保证不丢不重。说实话,我在实际项目中很少用 QoS 2,除非是计费系统。
我的建议:
- 传感器数据上报:QoS 0
- 控制指令下发:QoS 1
- 关键状态变更:QoS 1 + 应用层去重
- 计费/交易:QoS 2
3.4 保留消息与遗嘱消息:两个容易被忽略的利器
这两个功能,我刚开始做 MQTT 时完全没在意,后来才发现它们太有用了。
保留消息(Retained Message)
发布者发消息时可以设置 retain=1。Broker 会存下这条消息。新订阅者上线时,Broker 会立即把保留消息推给它。
有什么用?比如网关的状态信息:
# 发布保留消息
mosquitto_pub -t "gateway/status" -m "online" -r
# 新订阅者上线后立即收到
mosquitto_sub -t "gateway/status"
# 输出: online
这样新设备上线就能立刻知道网关状态,不用等下一次心跳。
遗嘱消息(Will Message)
客户端连接 Broker 时可以设置遗嘱。如果客户端异常断开(比如掉电、断网),Broker 会代它发布这条消息。
配置遗嘱的 CONNECT 报文结构:
// 伪代码示例
connect_packet = {
will_flag: true,
will_topic: "device/001/status",
will_message: "offline",
will_qos: 1,
will_retain: true
}
我在项目中用遗嘱消息做设备离线检测。设备正常关闭时会发 offline 消息,异常断开则由 Broker 代发。监控平台订阅 device/+/status,就能实时知道所有设备的状态。
避坑指南:我曾经把遗嘱消息的 QoS 设成 0,结果网络抖动时遗嘱丢了,设备显示一直在线。后来改成 QoS 1 + retain,问题解决。
3.5 MQTT报文结构解析:从字节层面理解协议
MQTT 报文结构很简洁。每个报文都包含三部分:
- 固定头部:所有报文都有,2 字节起
- 可变头部:某些报文有,比如 CONNECT、SUBSCRIBE
- 有效载荷:消息内容
固定头部的格式:
| Bit | 7-4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|
| Byte 1 | 报文类型 | DUP | QoS | QoS | RETAIN |
| Byte 2+ | 剩余长度(可变字节编码) | ||||
报文类型有 14 种,常用的就几个:
- CONNECT (1):客户端请求连接
- CONNACK (2):连接确认
- PUBLISH (3):发布消息
- SUBSCRIBE (8):订阅主题
- SUBACK (9):订阅确认
- PINGREQ/PINGRESP (12/13):心跳
- DISCONNECT (14):断开连接
剩余长度使用可变字节编码。每个字节的最高位表示是否还有后续字节。比如:
// 剩余长度 0~127:1 个字节
0x00 ~ 0x7F
// 剩余长度 128~16383:2 个字节
0x80 0x01 // 128
0xFF 0x7F // 16383
嗯,这里要注意:解析报文时一定要正确处理剩余长度。我见过有人直接读一个字节,结果消息超过 127 字节就解析错了。
下面这张图展示了 MQTT 报文的整体结构:
以 CONNECT 报文为例,它的可变头部包含:
- 协议名:
0x00 0x04 0x4D 0x51 0x54 0x54("MQTT") - 协议版本:
0x04(MQTT 3.1.1)或0x05(MQTT 5.0) - 连接标志:用户名、密码、遗嘱、保留等标志位
- 心跳间隔:单位秒,如
0x00 0x3C(60秒)
有效载荷则包含客户端 ID、用户名、密码、遗嘱主题和消息。
调试技巧:用 Wireshark 抓包看 MQTT 报文,比看文档直观多了。我每次排查协议问题,第一件事就是抓包。
好了,MQTT 的核心原理就这些。发布/订阅模型让你解耦,主题和通配符让你灵活订阅,QoS 让你控制可靠性,保留消息和遗嘱消息解决状态同步问题,报文结构让你从底层理解协议。这些基础打牢了,后面做网关集成就顺手了。