Files
cs-note/hhs/MQ/03-协议与标准/6-MQTT-协议.md
T
2026-05-24 20:51:06 +08:00

171 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 适用场景与选型原则]]