3、MQTT协议详解:发布/订阅模型、主题、服务质量(QoS)、保留消息、遗嘱消息
3.1 核心原理:从“点对点”到“发布/订阅”
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)最核心的设计思想,是将传统的“客户端直连客户端”通信,解耦为“客户端通过 Broker 中转”的发布/订阅模型。在 HTTP 或 TCP 直连模型中,如果设备 A 想给设备 B 发数据,A 必须知道 B 的 IP 地址和端口,并且 B 必须在线监听。这在物联网场景中极不现实——设备可能休眠、IP 可能变化、NAT 穿透困难。
MQTT 引入了一个中心节点 Broker(消息代理)。所有客户端(Publisher 发布者、Subscriber 订阅者)只与 Broker 建立 TCP 连接。发布者将消息发给 Broker,Broker 根据消息的主题(Topic),将消息转发给所有订阅了该主题的订阅者。发布者不需要知道订阅者是谁、在哪里、是否在线;订阅者也不需要知道发布者是谁。这种解耦带来了三个关键优势:
- 空间解耦:双方不需要知道对方的网络地址。
- 时间解耦:双方不需要同时在线,Broker 可暂存消息(配合持久会话)。
- 同步解耦:发布者发送后无需阻塞等待订阅者处理。
client.publish("topic", "payload") 和 client.subscribe("topic") 即可。3.2 主题(Topic)与通配符
主题是 MQTT 消息路由的核心寻址机制,采用 UTF-8 字符串,以正斜杠 / 作为层级分隔符。主题不需要提前在 Broker 上创建,发布者直接发布即可。设计良好的主题结构,能让订阅端用最少的通配符获取所需数据。
| 主题示例 | 含义 | 适用场景 |
|---|---|---|
sensor/room1/temperature | 1号房间温度 | 精确订阅单个传感器 |
sensor/+/temperature | 所有房间的温度 | + 匹配单层任意值 |
sensor/# | 所有传感器数据 | # 匹配多层任意值 |
device/status | 设备在线状态 | 配合遗嘱消息使用 |
+ 只能匹配一层,而 # 必须放在主题末尾,匹配剩余所有层。例如 sensor/# 能匹配 sensor/temp 和 sensor/room1/hum,但 sensor/+ 只能匹配前者。此外,主题不能包含空层级(如 sensor//temp),且 # 和 + 不能作为主题的第一层字符。3.3 服务质量(QoS)——消息可达性保障
MQTT 提供三种 QoS 等级,用于控制消息在发布者与 Broker、Broker 与订阅者之间传输的可靠性。注意:QoS 是端到端逐跳的,发布时设置的 QoS 与订阅时设置的 QoS 会取两者中的最大值作为实际转发 QoS。
- QoS 0(至多一次):消息直接发出,不等待确认,不重发。适用于传感器高频数据(如每 2 秒上报一次温度),丢失一帧影响不大。
- QoS 1(至少一次):发送后等待
PUBACK确认,超时未收到则重发。接收方可能收到重复消息,需要应用层做去重。 - QoS 2(恰好一次):通过四步握手(
PUBLISH → PUBREC → PUBREL → PUBCOMP)确保消息不重不漏。适用于控制指令、计费数据等关键场景。
// PubSubClient 中设置 QoS 示例
client.publish("sensor/temp", "25.5", true); // 第三个参数 true 表示保留消息
client.subscribe("cmd/led", 1); // 第二个参数 1 表示 QoS 1
3.4 保留消息(Retained Message)与遗嘱消息(LWT)
保留消息:发布者发布消息时若将 retain 标志置 1,Broker 会将该消息存储为主题的最新状态。当有新的订阅者订阅该主题时,Broker 会立即将这条保留消息推送给新订阅者,而不是等待下一次发布。这非常适合设备状态、配置参数等“需要立即获取当前值”的场景。
遗嘱消息(Last Will and Testament, LWT):客户端在连接 Broker 时,可以预先设置一条遗嘱消息(包含主题和 payload)。如果客户端异常断开(如断电、网络中断、心跳超时),Broker 会自动代发这条遗嘱消息,通知其他订阅者该设备已离线。正常断开(发送 DISCONNECT)时不会触发遗嘱。
// PubSubClient 设置遗嘱消息示例
client.setWill("device/status", "offline", true); // 遗嘱主题、内容、保留标志
client.connect("NodeMCU_01"); // 连接成功后,若异常断开,Broker 自动发布遗嘱
| 特性 | 保留消息 | 遗嘱消息 |
|---|---|---|
| 触发时机 | 新订阅者订阅时立即推送 | 客户端异常断开时由 Broker 代发 |
| 典型用途 | 设备最新状态、配置参数 | 设备离线通知、状态监控 |
| 设置方式 | 发布时 retain=true | 连接时 setWill() |
| 清除方式 | 发布空 payload 的保留消息 | 客户端正常断开后自动清除 |
3.5 案例:智能温控系统消息设计
假设你正在用 NodeMCU 制作一个智能温控面板,需要上报温度、接收控制指令、监控设备在线状态。合理的消息设计如下:
- 温度上报:NodeMCU 每 5 秒发布
sensor/room1/temp,QoS 0,retain=false。数据格式为 JSON:{"temp":25.5,"hum":60}。 - 控制指令:手机 App 发布
cmd/room1/ac,QoS 1,payload 为{"power":"on","mode":"cool","set":24}。NodeMCU 订阅该主题并执行。 - 设备状态:NodeMCU 连接时设置遗嘱
device/room1/status=offline,同时发布保留消息online。这样 App 启动时能立即看到所有设备状态,设备掉线时也能及时感知。
// NodeMCU 端核心代码逻辑
void callback(char* topic, byte* payload, unsigned int length) {
if (strcmp(topic, "cmd/room1/ac") == 0) {
// 解析 JSON 并控制空调继电器
}
}
void loop() {
if (!client.connected()) reconnect();
client.loop();
static unsigned long lastPub = 0;
if (millis() - lastPub > 5000) {
float t = dht.readTemperature();
char msg[50];
snprintf(msg, 50, "{\"temp\":%.1f}", t);
client.publish("sensor/room1/temp", msg);
lastPub = millis();
}
}
3.6 常见误区与复盘
connect() 前未调用 setWill(),导致 Broker 无法代发离线通知。修复后,监控端能在 10 秒内(心跳超时时间)收到离线消息,及时触发告警。这提醒我们:遗嘱消息是物联网设备“最后的声音”,必须在连接前配置好。