--- tags: [MQ, MQTT, IoT, 协议] create time: 2026-05-24 19:52 --- # MQTT 协议 ## 概述 MQTT(Message Queuing Telemetry Transport)是专为物联网(IoT)场景设计的轻量级消息协议。它的设计哲学极其明确:在低带宽、高延迟、不可靠的网络环境下,用最小的资源开销实现设备间的消息传递。从智能家居传感器到工业 SCADA 系统,MQTT 已经成为 IoT 通信的事实标准。 ## 正文 ### 为什么需要 MQTT? 想象一个场景:成千上万个温度传感器部署在偏远地区,它们通过 2G/3G 网络上报数据。这些设备 CPU 弱、内存小、电量有限、网络时断时续。在这种约束下,HTTP 的冗长头部和请求-响应模型就成了灾难。MQTT 就是为解决这类问题而生的——它把协议开销压缩到极致(最小报文仅 2 字节),支持持久连接,提供灵活的消息可靠性保障。 ### 报文结构 MQTT 的每条报文由三部分组成: ``` +------------------+--------------------+----------+ | Fixed Header | Variable Header | Payload | +------------------+--------------------+----------+ ``` - **Fixed Header**(必选):包含报文类型(4 bit)、标志位(4 bit)和剩余长度(可变字节编码)。报文类型定义了 14 种操作,如 CONNECT、PUBLISH、SUBSCRIBE 等。 - **Variable Header**(可选):根据报文类型不同而变化,比如 PUBLISH 报文里放 Topic 名称,CONNECT 报文里放协议版本和连接标志。 - **Payload**(可选):实际消息内容。CONNECT 报文的 Payload 是 Client ID 和遗嘱消息,PUBLISH 报文的 Payload 就是应用数据。 > [!question] 为什么 Fixed Header 的"剩余长度"要用可变字节编码(Variable Byte Encoding)? > 因为 IoT 场景下大部分消息体很小(几十字节),如果用固定 4 字节表示长度就太浪费了。可变编码让 0-127 字节的长度只占 1 字节,128-16383 字节占 2 字节,以此类推——小消息用小开销,大消息也能表示。 ### QoS 等级:三种可靠性语义 MQTT 最精妙的设计之一就是三个 QoS(Quality of Service)等级,让发布者按需选择投递保证,而不是"一刀切"。 **QoS 0 - 最多一次(At Most Once)** fire-and-forget,发送即忘。没有确认机制,消息可能丢失。适合传感器周期性上报——丢一帧温度数据无所谓,下一帧马上就来。 **QoS 1 - 至少一次(At Least Once)** 发布者收到 PUBACK 后才确认发送完成。如果超时未收到 ACK,会重发消息(DUP 标志置 1)。这意味着消息不会丢,但可能重复投递——消费者需要自行处理幂等。 **QoS 2 - 恰好一次(Exactly Once)** 通过四次握手保证消息恰好送达一次,是最高等级但也最重的语义。 ```mermaid sequenceDiagram participant P as "Publisher" participant B as "Broker" participant C as "Consumer" Note over P,B: "QoS 0: fire and forget" P->>B: "PUBLISH QoS 0" B->>C: "PUBLISH QoS 0" Note over P,B: "QoS 1: at least once" P->>B: "PUBLISH QoS 1" B-->>P: "PUBACK" B->>C: "PUBLISH QoS 1" C-->>B: "PUBACK" Note over P,B: "QoS 2: exactly once" P->>B: "PUBLISH QoS 2" B-->>P: "PUBREC" P->>B: "PUBREL" B-->>P: "PUBCOMP" B->>C: "PUBLISH QoS 2" C-->>B: "PUBREC" B->>C: "PUBREL" C-->>B: "PUBCOMP" ``` > [!question] QoS 2 的四次握手会不会很慢?在什么场景下值得用? > 相比 QoS 0/1,QoS 2 确实多了额外的往返开销。但在需要精确计数的场景下(如扣费指令、库存变更、IoT 设备的命令下发),一条消息重复执行可能导致严重后果。实际部署中,很多系统选择 QoS 1 + 消费端幂等的组合来平衡性能和可靠性,只在真正"不能重复"的关键路径上才用 QoS 2。 ### 核心概念 #### Topic 与通配符 MQTT 使用层级结构的 Topic 进行消息路由,用 `/` 分隔层级,例如 `home/livingroom/temperature`。 订阅时支持两种通配符: - `+`:匹配单个层级。`home/+/temperature` 匹配 `home/livingroom/temperature` 和 `home/bedroom/temperature`。 - `#`:匹配多个层级(必须放在末尾)。`home/#` 匹配 `home/` 下所有 Topic。 #### 遗嘱消息(Will Message) 客户端在连接时可以设置一条"遗嘱消息"。如果客户端异常断线(没有发送 DISCONNECT),Broker 会自动将这条遗嘱消息发布到指定 Topic。这对 IoT 设备的在线状态监控非常有用——订阅遗嘱 Topic 就能实时知道哪些设备掉线了。 #### 保留消息(Retain) 当 PUBLISH 报文的 Retain 标志置 1 时,Broker 会保存这条消息。后续任何新订阅该 Topic 的客户端会立即收到这条保留消息,而不是等到下一次发布。典型场景:传感器发布当前温度为保留消息,新上线的监控端一订阅就能拿到最新值,不用等下一次上报。 #### Clean Session - **Clean Session = 1**:每次连接都是全新会话,Broker 不保留任何订阅关系和未确认消息。适合无状态客户端。 - **Clean Session = 0**:Broker 为客户端持久化会话(订阅列表 + QoS 1/2 的未确认消息)。客户端断线重连后能恢复之前的订阅并收到离线期间的消息。 ### MQTT 5.0 新特性 MQTT 5.0(2019 年发布)是一次重大升级,引入了多项企业级特性: - **共享订阅(Shared Subscriptions)**:多个客户端组成一个订阅组,消息在组内负载均衡分发。这解决了 MQTT 之前缺乏 Consumer Group 的痛点,类似 Kafka 的分区分配。 - **用户属性(User Properties)**:在报文头部附加自定义键值对,可用于传递链路追踪 ID、业务上下文等元信息,而不用修改消息体。 - **原因码(Reason Codes)**:所有响应报文都携带结构化的原因码,取代了之前模糊的返回码,方便客户端精确处理错误(如"Topic 名称不合法" vs "鉴权失败")。 - **Session Expiry Interval**:可以设置会话过期时间,不再只有"立即过期"和"永不过期"两种选择。 - **请求-响应模式**:引入 Response Topic 和 Correlation Data 属性,让 MQTT 原生支持请求-响应模式。 ### Go 代码示例 使用 Eclipse Paho MQTT Go 客户端实现消息的发布和订阅。 ```go package main import ( "fmt" "time" mqtt "github.com/eclipse/paho.mqtt.golang" ) func main() { // 创建客户端选项 opts := mqtt.NewClientOptions(). AddBroker("tcp://localhost:1883"). SetClientID("go-mqtt-demo"). SetCleanSession(true) // 设置连接成功后的订阅回调 opts.OnConnect = func(c mqtt.Client) { // 订阅 Topic,QoS 1 c.Subscribe("sensor/temperature", 1, func(_ mqtt.Client, msg mqtt.Message) { fmt.Printf("收到消息: topic=%s payload=%s\n", msg.Topic(), string(msg.Payload())) }) } client := mqtt.NewClient(opts) if token := client.Connect(); token.Wait() && token.Error() != nil { panic(token.Error()) } // 发布消息,QoS 1,Retain 标志设为 false token := client.Publish("sensor/temperature", 1, false, "23.5°C") token.Wait() time.Sleep(time.Second) client.Disconnect(250) } ``` 这段代码做了三件事:连接 Broker、订阅 `sensor/temperature` Topic、发布一条温度消息。`OnConnect` 回调确保断线重连后会重新订阅,这是 MQTT 客户端开发的最佳实践。 --- > [!tip] 实践建议 > - IoT 设备资源紧张时优先用 QoS 0,需要可靠投递用 QoS 1,关键控制指令才用 QoS 2。 > - 生产环境务必启用 TLS 加密。MQTT 默认端口 1883 是明文的,安全端口是 8883。 > - Client ID 必须全局唯一,否则会踢掉前一个同 ID 的连接。 ## 关联笔记 - [[03-协议与标准/4-MQ-消息协议总览|MQ 消息协议总览]] - [[03-协议与标准/5-AMQP-协议|AMQP 协议]] - [[01-基础概念/2-MQ-适用场景与选型原则|MQ 适用场景与选型原则]]