2、MQTT核心概念:发布/订阅模型、主题(Topic)、服务质量(QoS)、保留消息

好,咱们今天来啃MQTT最核心的几个概念。说实话,我当年刚接触MQTT时,觉得它就是个轻量级的消息协议,没什么大不了的。直到我在一个智能路灯项目里,因为搞混了QoS等级,导致路灯半夜集体“抽风”开关——嗯,从那以后我再也不敢小看这几个概念了。

2.1 发布/订阅模型:解耦的艺术

传统的客户端-服务器模型,说白了就是“点对点”聊天。客户端问一句,服务器答一句。但在物联网场景下,你想想看,一个温度传感器可能要给10个不同的应用发数据,难道要它跟每个应用都建立连接?太蠢了。

MQTT用的是发布/订阅模型。这个模型里有个中间人——Broker(代理服务器)。发布者只管把消息扔给Broker,订阅者只管从Broker拿消息。两边互不认识,彻底解耦。

核心好处:

  • 空间解耦:发布者和订阅者不需要知道对方IP、端口
  • 时间解耦:发布者发消息时,订阅者可以不在线;订阅者上线后还能收到(通过持久会话)
  • 同步解耦:发布者不用等订阅者处理完,发完就走

我在一个工厂设备监控项目里,就用这个模型解决了大问题。原来每个传感器都要跟多个监控系统直连,改一次配置要重启所有设备。换成MQTT后,传感器只管往Broker发数据,监控系统自己订阅就行。新增一个监控大屏?改Broker配置就行,传感器那边完全不用动。

2.2 主题(Topic):消息的“地址”

主题就是消息的分类标签。它是个层级结构的字符串,用斜杠/分隔。比如:

factory/floor1/temperature
factory/floor1/humidity
factory/floor2/temperature

我个人习惯把主题设计得像文件路径一样清晰。这样一看就知道消息来自哪里、是什么类型的数据。

2.2.1 通配符:批量订阅的利器

MQTT支持两种通配符,这个设计真的很巧妙:

通配符 名称 匹配规则 示例
+ 单层通配符 匹配一个层级 factory/+/temperature 匹配所有楼层的温度
# 多层通配符 匹配剩余所有层级 factory/# 匹配工厂所有数据

避坑指南:我曾经在项目里用#通配符订阅了所有主题,结果Broker直接把所有消息都推过来,包括一些调试日志。那段时间网络流量暴涨,差点把带宽打满。所以,通配符虽好,但别滥用。能用+就别用#,能精确匹配就别用通配符。

2.2.2 主题设计的最佳实践

主题设计得好,后面维护就轻松。我总结了几条经验:

  • 从通用到具体:比如 site/area/device/sensor
  • 避免以斜杠开头/factory/temp 这种写法容易引起歧义
  • 不要用中文:虽然MQTT协议支持UTF-8,但不同Broker对中文处理可能有差异
  • 控制层级深度:我一般不超过5层,太深了影响匹配性能

2.3 服务质量(QoS):消息可靠性的三个等级

QoS是MQTT里最容易踩坑的地方。它定义了消息传递的可靠性等级,一共三级:

QoS等级 名称 发送次数 可靠性 适用场景
0 至多一次 1次 最低 传感器数据(丢了也无所谓)
1 至少一次 ≥1次 中等 控制指令(必须到达,重复可接受)
2 恰好一次 2次握手 最高 支付、计费(不能丢也不能重复)

2.3.1 QoS 0:发完就忘

说白了就是“UDP模式”。发布者把消息扔给Broker,Broker转发给订阅者,没有任何确认。丢了就丢了。

我在一个环境监测项目里,温度数据每秒上报一次,用的就是QoS 0。为什么?因为丢一两条数据完全不影响整体趋势判断,而且QoS 0的吞吐量最高,延迟最低。

2.3.2 QoS 1:至少一次

发布者发消息后,必须等到Broker回复PUBACK才算完。如果超时没收到,就重发。订阅者也可能收到重复消息。

注意:QoS 1虽然保证了消息不丢,但可能重复。如果你的业务逻辑对重复敏感(比如“开灯”指令重复执行),那就要在应用层做去重。我曾经在一个智能家居项目里,因为没做去重,用户按一次开灯键,灯闪了三次——用户差点以为灯坏了。

2.3.3 QoS 2:恰好一次

这是最可靠的等级,也是开销最大的。它用了两次确认握手:

  1. 发布者发消息 → Broker回复PUBREC
  2. 发布者发PUBREL → Broker回复PUBCOMP

整个过程确保消息既不丢也不重复。但代价是延迟高、吞吐量低。我个人建议只在关键业务场景使用,比如支付确认、设备固件升级指令。

我的经验:很多初学者喜欢所有消息都用QoS 2,觉得“越可靠越好”。其实没必要。你想想看,一个温度传感器每秒上报一次数据,用QoS 2的话,Broker要处理大量的握手包,CPU和内存都扛不住。我一般遵循“够用就好”的原则:能接受丢失的用QoS 0,需要保证到达的用QoS 1,绝对不能出错的用QoS 2。

2.4 保留消息:新订阅者的“见面礼”

保留消息是MQTT一个很贴心的设计。当发布者发消息时,如果设置了Retained标志,Broker就会把这条消息存下来。以后任何新订阅者订阅这个主题时,Broker会立刻把这条保留消息推过去。

这有什么用?举个例子:

在一个智能路灯系统里,每个路灯都有一个“当前亮度”的主题。新加入的监控客户端订阅这个主题时,如果路灯没有实时发消息,客户端就不知道当前亮度是多少。但有了保留消息,客户端一订阅就能拿到最新的亮度值。

// 发布保留消息(伪代码)
client.publish("streetlight/001/brightness", "80%", 1, true);  // 最后一个参数是retained

保留消息的注意事项:

  • 每个主题只能保留一条消息,新保留消息会覆盖旧的
  • 保留消息会占用Broker内存,别滥用
  • 发送空消息(payload为空)并设置retained=true,可以清除该主题的保留消息

我记得有一次,一个同事把所有传感器数据都设成了保留消息。结果Broker内存爆了,因为每个传感器每5秒就发一条新数据,保留消息不断更新,但旧数据占用的内存没及时释放。嗯,后来我们规定:只有状态类数据(如开关状态、设备在线状态)才用保留消息,流式数据(如温度、湿度)不用。

2.5 综合实战:一个简单的温度监控系统

咱们把上面这些概念串起来,看一个完整的例子:

// 温度传感器(发布者)
// 主题:factory/floor1/temperature
// QoS:0(温度数据丢了无所谓)
// 保留消息:false(温度是实时数据,不需要保留)

// 温度告警服务(订阅者)
// 订阅主题:factory/+/temperature
// 通配符:+ 匹配所有楼层
// QoS:0(跟发布者保持一致)

// 设备状态服务(订阅者)
// 订阅主题:factory/floor1/status
// QoS:1(设备状态不能丢)
// 保留消息:true(新订阅者需要知道当前状态)

你看,通过合理组合发布/订阅模型、主题设计、QoS等级和保留消息,就能搭建出一个既灵活又可靠的物联网消息系统。说白了,MQTT的核心就这四个概念,但把它们用好、用对,才是真正的功夫。

下一章咱们聊聊MQTT的会话机制和心跳保活,这两个在实际项目中也是坑不少。到时候我把我踩过的坑都抖出来给你看。