MQTT 协议:从发布/订阅到 QoS,一篇讲清楚

· 约 4 分钟读完

它解决什么问题

传统的 HTTP 是请求-响应模型:客户端主动问,服务端被动答。这在浏览器场景没问题,但换到物联网就不太合适了——成千上万个传感器需要持续上报数据,设备端算力和带宽都很有限,HTTP 的头部开销和连接管理成本太高。

MQTT(Message Queuing Telemetry Transport)就是为这种场景设计的。它是一个轻量级的消息传输协议,基于 TCP,最小报文只有 2 字节。OASIS 标准组织维护,当前主流版本是 MQTT 3.1.1 和 MQTT 5.0。

发布/订阅模型

MQTT 的核心是发布/订阅(Pub/Sub)模式,整个通信涉及三个角色:

  • Publisher(发布者):发消息的一方,比如温度传感器
  • Subscriber(订阅者):收消息的一方,比如监控面板
  • Broker(代理):中间人,负责接收消息并转发给订阅者

发布者和订阅者之间完全解耦——它们不需要知道对方的存在,甚至不需要同时在线。所有消息都通过 Broker 中转。

常见的开源 Broker 有 Mosquitto(轻量,适合开发和小规模部署)、EMQX(Erlang 写的,主打大规模集群)、NanoMQ(边缘计算场景)。

主题(Topic)

消息靠主题来路由。主题是一个 UTF-8 字符串,用 / 分层,类似文件路径:

home/living-room/temperature
home/bedroom/humidity
factory/line-1/machine-3/status

订阅时可以用通配符:

  • + 匹配单层:home/+/temperature 匹配 home/living-room/temperaturehome/bedroom/temperature,但不匹配 home/floor-2/bedroom/temperature
  • # 匹配多层(只能放末尾):home/# 匹配 home 下所有层级的所有主题

主题不需要预先创建,发布者往一个主题发消息,Broker 自动处理路由。

QoS 等级

MQTT 定义了三个服务质量等级,用来在可靠性和性能之间做取舍:

QoS名称语义报文交互
0At most once最多送达一次,可能丢PUBLISH(单次,不确认)
1At least once至少送达一次,可能重复PUBLISH → PUBACK
2Exactly once恰好送达一次PUBLISH → PUBREC → PUBREL → PUBCOMP

QoS 0 最快、开销最小,适合高频上报且偶尔丢一条无所谓的场景(比如每秒一次的温度数据)。QoS 2 最可靠但最慢,四步握手的开销不小,只在关键指令(比如远程开关阀门)时才值得用。

实际选型中 QoS 1 用得最多——重复消息在业务层做幂等处理,比 QoS 2 的四步握手划算。

连接与保活

MQTT 客户端通过 TCP 连接到 Broker,第一步是发 CONNECT 报文:

CONNECT 报文关键字段:
├── Client ID       → 客户端唯一标识
├── Clean Session   → 是否清除上次的会话状态
├── Keep Alive      → 心跳间隔(秒)
├── Username        → 可选,用于认证
├── Password        → 可选,用于认证
└── Will Topic/Msg  → 可选,遗嘱消息

Broker 收到后回 CONNACK,连接建立。

Keep Alive 是心跳机制:客户端在这个间隔内如果没有发送任何报文,就必须发一个 PINGREQ,Broker 回 PINGRESP。如果 Broker 在 1.5 倍 Keep Alive 时间内没收到任何报文,就认为客户端掉线。

遗嘱消息(Last Will)

这是 MQTT 比较巧妙的一个设计。客户端在 CONNECT 时可以预设一条”遗嘱”——指定一个主题和消息内容。如果客户端异常断开(没有发 DISCONNECT 就断了 TCP),Broker 会自动把这条遗嘱消息发布出去。

典型用法是做在线状态通知:

Will Topic: device/sensor-42/status
Will Message: offline
Will QoS: 1
Will Retain: true

设备正常工作时定期往 device/sensor-42/statusonline。一旦设备断网或崩溃,Broker 自动发布 offline 到同一主题。监控系统订阅了这个主题,就能实时感知设备离线。

保留消息(Retained Message)

普通的 MQTT 消息是”阅后即焚”——Broker 转发给当前在线的订阅者就完事了。但如果设置了 Retain 标志,Broker 会保存这条消息。后续有新的客户端订阅这个主题时,会立刻收到最后一条保留消息,不用等到下次发布。

适合发布不频繁但新订阅者需要立刻知道当前值的场景,比如设备的配置信息、当前运行状态。

报文结构

MQTT 的报文设计很紧凑:

固定头(2 字节起)
├── 报文类型(4 bit):CONNECT / PUBLISH / SUBSCRIBE 等共 14 种
├── 标志位(4 bit):DUP / QoS / Retain
└── 剩余长度(1-4 字节):变长编码,最大表示 256 MB

可变头(视报文类型而定)
├── Packet ID(QoS > 0 时才有)
└── 协议特定字段

载荷(Payload)
└── 消息内容

剩余长度用的是变长编码:每个字节的低 7 位是有效数据,最高位是延续标志。1 个字节最大表示 127,2 个字节最大 16383,以此类推,最多 4 字节,上限约 256 MB。

这意味着一条 QoS 0 的短消息,协议头开销可以压到只有 2 字节——对比 HTTP 动辄几百字节的 header,差距很明显。

MQTT 5.0 的几个变化

MQTT 5.0 在 3.1.1 基础上加了不少实用特性:

  • 属性系统:报文可以携带键值对属性,比如消息过期时间、响应主题、内容类型
  • 共享订阅:多个订阅者组成一个组,消息在组内负载均衡分发(3.1.1 只能广播)
  • 原因码:CONNACK、PUBACK 等回复报文带上具体的错误原因,不再只有”成功/失败”
  • 主题别名:用一个短整数代替长主题字符串,减少重复传输的带宽消耗
  • 流量控制:客户端和 Broker 可以协商同时在途的最大消息数

如果是新项目,直接上 5.0。但要确认 Broker 和客户端库都支持——部分嵌入式 SDK 还停留在 3.1.1。

和其他协议的对比

MQTTHTTPWebSocketCoAP
传输层TCPTCPTCPUDP
模型Pub/Sub请求/响应双向流请求/响应
最小头开销2 字节几百字节2-14 字节4 字节
适用场景IoT 设备通信Web 应用实时 Web受限网络的 IoT

MQTT 和 CoAP 经常被拿来比。简单说:MQTT 基于 TCP,可靠性由协议层保证,适合网络相对稳定的场景;CoAP 基于 UDP,更轻量,适合网络极差、丢包率高的受限环境(比如 NB-IoT)。