3. MQTT核心原理(下)

上一章我们聊了MQTT的发布/订阅模型和QoS等级。今天继续深入,把剩下的三个核心机制讲透——保留消息、遗嘱消息和会话机制。这三个东西在实际项目中用得非常频繁,尤其是做物联网设备管理的时候,几乎天天打交道。

3.1 保留消息(Retained Message)

先问一个问题:如果一个新设备上线,它怎么知道当前系统的状态?比如温度传感器刚通电,它想知道最近一次的温度值是多少。如果没有保留消息,它只能等下一次温度发布才能拿到数据。这显然不够实时。

保留消息就是干这个用的。

当发布者发送消息时,把 Retained 标志位设为 true,Broker 就会把这条消息存下来。以后任何客户端订阅这个主题,Broker 会立刻把最后一条保留消息推送给它。

关键特性:

  • 每个主题只能保留一条消息
  • 新的保留消息会覆盖旧的
  • 发送空消息体(payload 长度为 0)且 Retained=true 可以清除保留消息
  • 保留消息与发布者无关,Broker 重启后仍然存在(取决于 Broker 实现)

我个人习惯在设备状态上报的场景里用保留消息。比如智能灯控系统,每个灯定期上报亮度、色温,新加入的网关订阅后立刻拿到当前状态,不用等一轮轮询。

使用建议:

保留消息适合存储设备的最终状态,不适合存储日志或时序数据。日志数据量大,保留消息只存一条,存日志没意义。

3.2 遗嘱消息(Will Message)

物联网设备经常掉线——电池没电、网络断连、设备死机。怎么让其他设备知道这个设备挂了?遗嘱消息就是干这个的。

客户端在连接 Broker 时,可以在 CONNECT 报文里带上遗嘱消息。如果客户端非正常断开(比如网络断开、心跳超时),Broker 就会帮它发布这条遗嘱消息。

注意:

客户端主动发送 DISCONNECT 报文正常断开,Broker 不会发布遗嘱消息。只有非正常断开才会触发。

我在一个智能门锁项目里用过这个机制。每个门锁连接时设置遗嘱消息:

# 伪代码示例
client.connect(
    host="broker.example.com",
    port=1883,
    keepalive=60,
    will_topic="device/lock/status",
    will_payload="offline",
    will_qos=1,
    will_retain=True
)

门锁正常工作时,定期发布 "online" 到同一个主题。一旦掉线,Broker 自动发布 "offline"。后台管理系统收到后立刻告警,通知物业去检修。

遗嘱消息的 QoS 和 Retain 标志可以单独设置。我建议遗嘱消息的 QoS 至少设为 1,确保消息能送达。Retain 设为 true 也不错,这样新订阅者也能知道设备已经离线。

3.3 会话机制(Clean Session)

会话机制是 MQTT 里最容易让人迷糊的概念。说白了,它决定了 Broker 要不要帮你记住订阅关系和离线消息。

客户端连接时,CONNECT 报文里有一个 Clean Session 标志位:

Clean Session 含义 适用场景
true(1) 每次连接都是全新会话,不恢复之前的订阅和离线消息 临时客户端、数据订阅端
false(0) 持久会话,Broker 保存订阅关系和离线消息 设备端、需要离线消息的客户端

举个例子。一个温度传感器每隔 10 秒上报一次数据,但后台监控系统偶尔断网。如果传感器用 Clean Session=false 连接,后台断网期间的消息会被 Broker 缓存起来,等后台重新连上后再推送。

持久会话的存储内容:

  • 客户端的订阅关系
  • QoS 1 和 QoS 2 的离线消息
  • 客户端离线期间错过的消息

这里有个坑。我曾经在一个项目里,所有设备都用 Clean Session=false,结果 Broker 内存暴涨。为什么?因为设备数量太多,每个设备都缓存了大量离线消息。后来我调整了策略:

  • 数据上报类设备:用 Clean Session=true,丢了就丢了,反正下一轮数据马上来
  • 控制指令类设备:用 Clean Session=false,确保指令不丢失
  • 后台监控系统:用 Clean Session=false,但设置合理的消息过期时间

3.4 三者协同工作

这三个机制不是孤立的。在实际系统中,它们经常配合使用。我画了一张图,帮你理清它们的关系:

MQTT 核心机制协同关系图 MQTT Broker 保留消息 遗嘱消息 会话机制 保留消息存状态,遗嘱消息报离线,会话机制管订阅 三者配合实现设备状态感知与离线消息可靠投递 设备状态同步 保留消息 + 持久会话 设备离线告警 遗嘱消息 + 保留消息 离线消息补发 持久会话 + QoS 1/2

3.5 避坑指南

做 MQTT 开发这几年,我踩过不少坑。挑几个典型的说说:

坑一:保留消息的“幽灵”问题

我曾经在一个项目里,设备更换后旧设备的保留消息还在。新设备上线后,订阅者收到的是旧状态。解决方案:设备上线时先发送空保留消息清除旧数据,再发布新状态。

坑二:遗嘱消息的 QoS 陷阱

遗嘱消息的 QoS 如果设为 0,Broker 可能丢消息。我建议至少用 QoS 1。另外,遗嘱消息的 topic 最好和正常状态上报的 topic 分开,避免逻辑混乱。

坑三:持久会话的内存爆炸

大量设备用 Clean Session=false,且 QoS 设为 2,Broker 内存会迅速膨胀。我建议:

  • 控制持久会话的数量
  • 设置消息过期时间(Message Expiry Interval)
  • 定期清理无效会话

3.6 小结

保留消息、遗嘱消息和会话机制,是 MQTT 协议里最实用的三个特性。保留消息让新订阅者快速获取状态,遗嘱消息让系统感知设备离线,会话机制保证消息不丢。三者配合,基本能覆盖物联网通信中 90% 的可靠性需求。