MQTT 协议基础:发布/订阅模型、报文结构与 QoS

大家好,我是老周。今天咱们聊聊 MQTT 协议最核心的三个东西:发布/订阅模型、报文结构,还有那个让人又爱又恨的 QoS。说实话,搞车联网这几年,我见过太多人在这上面栽跟头了。你想想看,车在高速上跑着,一条指令没发到位,那可不是闹着玩的。

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

传统的客户端/服务器模型,说白了就是点对点通信。客户端直接找服务器要数据,耦合得很紧。MQTT 不一样,它用的是发布/订阅模型。我刚开始接触的时候也觉得绕,后来在项目里用顺手了,才发现这东西是真香。

核心角色有三个:

  • 发布者(Publisher):只管发消息,不管谁收
  • 订阅者(Subscriber):只管收消息,不管谁发
  • 代理(Broker):中间人,负责转发

举个例子。车上的 T-Box 采集到车速数据,它只需要把数据发到 vehicle/speed 这个主题上。至于谁在订阅这个主题——是中控屏要显示,还是后台要记录,T-Box 完全不用关心。这就是解耦。

我在做某个车厂的项目时,遇到过一个问题:T-Box 和手机 App 直接通信,结果 T-Box 固件一升级,App 也得跟着改。后来改成 MQTT 的发布/订阅模型,两边都只跟 Broker 打交道,再也不用互相迁就了。

2.2 主题(Topic)与通配符

主题是 MQTT 里的消息分类标签。它是个层级结构,用 / 分隔。比如:

vehicle/vin123456/speed
vehicle/vin123456/engine/temperature
vehicle/vin123456/gps/latitude

订阅的时候可以用通配符,省事儿不少:

  • +:匹配单层。比如 vehicle/+/speed 能匹配所有车的车速
  • #:匹配多层。比如 vehicle/vin123456/# 能匹配这辆车的所有数据

注意:通配符只能用订阅,不能用发布。我曾经见过有人用 vehicle/+/speed 发消息,结果 Broker 直接拒绝了。嗯,这个坑我替你们踩过了。

2.3 MQTT 报文结构:14 种报文类型

MQTT 的报文结构其实挺简洁的。固定头部只有 2 个字节,后面跟着可变头部和有效载荷。我画了张图,你们一看就明白。

MQTT 报文通用结构 固定头部 2 字节 可变头部 长度可变 有效载荷 长度可变 固定头部细节 Bit 7-4: 报文类型 Bit 3: DUP 标志 Bit 2-1: QoS 等级 Bit 0: RETAIN 标志 第 2 字节: 剩余长度 14 种报文类型(部分) CONNECT / CONNACK / PUBLISH / PUBACK PUBREC / PUBREL / PUBCOMP / SUBSCRIBE SUBACK / UNSUBSCRIBE / UNSUBACK PINGREQ / PINGRESP / DISCONNECT AUTH(MQTT 5.0 新增)

固定头部的第一个字节高 4 位表示报文类型。比如 0x30 就是 PUBLISH 报文。第二个字节是剩余长度,表示后面还有多少字节。这个设计很巧妙,能支持最大 256 MB 的消息体。

小技巧:调试的时候,我习惯用 Wireshark 抓包看 MQTT 报文。固定头部的第一个字节一看,就知道是什么类型的报文,排查问题特别快。

2.4 QoS 服务质量等级:三个档位

QoS 是 MQTT 里最容易被误解的概念。说白了,它就是消息送达的可靠性保证。MQTT 定义了三个等级:

QoS 等级 名称 发送次数 可靠性 适用场景
0 至多一次 1 次 最低 传感器数据、日志
1 至少一次 ≥1 次 中等 控制指令、状态更新
2 恰好一次 2 次(四步握手) 最高 关键指令、OTA 升级

QoS 0:至多一次

发出去就不管了,不确认,不重发。说白了就是「尽力而为」。我在项目中一般把 GPS 位置、车速这种高频但允许丢几条的数据用 QoS 0。省带宽,省流量。

QoS 1:至少一次

发送后会等待 PUBACK 确认。如果超时没收到,就重发。这样能保证消息到达,但可能重复。比如车门解锁指令,重复发一次问题不大,但必须得让车收到。

避坑指南:我曾经在某个项目中,把车窗控制指令设成了 QoS 1。结果网络抖动,指令重复发了三次,车窗电机来回动作了三回。嗯,从那以后,凡是执行类指令,我都在应用层加了个去重逻辑。

QoS 2:恰好一次

这是最可靠的等级,用了四步握手:PUBLISH → PUBREC → PUBREL → PUBCOMP。能保证消息既不丢失也不重复。代价就是流量大、延迟高。我一般只在 OTA 升级包、远程点火这种关键操作上用。

来看个 QoS 2 的完整流程:

发布者                  Broker                 订阅者
  |                       |                       |
  |---- PUBLISH -------->|                       |
  |                       |---- PUBLISH -------->|
  |<---- PUBREC ---------|<---- PUBREC ----------|
  |---- PUBREL --------->|                       |
  |                       |---- PUBREL --------->|
  |<---- PUBCOMP --------|<---- PUBCOMP ---------|
  |                       |                       |

2.5 如何选择 QoS?

很多新手上来就想用 QoS 2,觉得越可靠越好。其实不是这样的。你想想看,车上有几十个传感器,每秒发几百条数据,全用 QoS 2,Broker 不崩才怪。

我个人的经验是:

  • 数据采集类(车速、温度、电压):QoS 0,丢了就丢了,下一条补上
  • 状态上报类(车门状态、续航里程):QoS 1,允许重复,但不能丢
  • 控制指令类(远程锁车、OTA 升级):QoS 2,必须精确一次

记住一句话:QoS 不是越高越好,而是够用就好。在车联网场景下,带宽和延迟都是稀缺资源,别浪费在不需要的地方。

2.6 保留消息与遗嘱消息

这两个特性在车联网里特别实用,我单独拎出来说一下。

保留消息(Retained Message):发布者发消息时把 RETAIN 标志置 1,Broker 就会存下这条消息的最新版本。新订阅者上线后,Broker 会立即推送给它。比如车辆的最后一次 GPS 位置,新设备连上来就能拿到,不用等下一次上报。

遗嘱消息(Will Message):客户端连接时可以在 CONNECT 报文里设置遗嘱。如果客户端异常断开(比如 T-Box 掉电),Broker 就会帮它发布这条遗嘱消息。我在项目中用这个来检测 T-Box 离线,比心跳超时判断快多了。

实战经验:遗嘱消息的主题最好和正常消息分开,比如用 vehicle/vin123456/status 表示在线,vehicle/vin123456/will 表示离线。这样订阅者逻辑清晰,不容易搞混。

好了,MQTT 协议基础就聊到这儿。发布/订阅模型让你解耦,报文结构让你看懂数据,QoS 让你控制可靠性。这三个东西搞明白了,后面讲安全的时候你才能理解为什么有些攻击能得逞,有些防护措施又为什么有效。