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

246 lines
12 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 3.1 | 2010 | 协议于 1999 年由 IBM 设计,此版本首次公开发布,奠定发布-订阅、QoS、遗嘱消息等核心设计 |
| MQTT 3.1.1 | 2014 | OASIS 标准化,清理协议细节(如 CONNACK 返回码规范化),成为最广泛部署的版本 |
| MQTT 5.0 | 2019 | 企业级升级:共享订阅、用户属性、原因码、Topic Alias、增强认证等,补齐生产环境短板 |
### 发布-订阅架构
MQTT 采用以 Broker 为中心的发布-订阅模型,发布者和订阅者完全解耦:
```mermaid
graph TB
P1["Publisher 1"] -->|publish| B["Broker"]
P2["Publisher 2"] -->|publish| B
B -->|push| S1["Subscriber A"]
B -->|push| S2["Subscriber B"]
B -->|push| S3["Subscriber C"]
```
### 报文结构
MQTT 的每条报文由三部分组成:
```mermaid
graph LR
A["Fixed Header (必选)"] --> B["Variable Header (可选)"] --> C["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 字节,以此类推——小消息用小开销,大消息也能表示。
### 连接生命周期
一个 MQTT 客户端从连接到断开的完整流程如下:
```mermaid
sequenceDiagram
participant C as "Client"
participant B as "Broker"
C->>B: "CONNECT (ClientID, CleanSession, KeepAlive, Will)"
B-->>C: "CONNACK (return code)"
Note over C,B: "正常通信阶段"
C->>B: "PUBLISH / SUBSCRIBE / UNSUBSCRIBE"
B-->>C: "PUBACK / SUBACK / UNSUBACK"
Note over C,B: "Keep Alive 心跳"
C->>B: "PINGREQ"
B-->>C: "PINGRESP"
C->>B: "DISCONNECT"
Note over C,B: "正常断开, Broker 不发送遗嘱消息"
```
**Keep Alive 心跳**:客户端在 CONNECT 中声明一个心跳间隔(秒)。如果在 1.5 倍间隔内没有任何报文发送,Broker 会认为客户端已断线并触发遗嘱消息。这个机制既保证了连接活性检测,又在通信频繁时避免了无意义的心跳包。
### 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。
> [!warning] QoS 降级规则
> Broker 转发消息时,实际投递的 QoS 取 **发布者 QoS** 和**订阅者 QoS 的较小值**,即 `effective QoS = min(publisher QoS, subscriber QoS)`。例如发布者用 QoS 2 发送,但订阅者只订阅了 QoS 1,那 Broker 只保证 QoS 1 的语义。这是新手最容易忽略的细节。
### 核心概念
#### 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 的未确认消息)。客户端断线重连后能恢复之前的订阅并收到离线期间的消息。
> [!tip] MQTT 5.0 的变化
> MQTT 5.0 将 Clean Session 拆分为两个独立概念:**Clean Start**(连接时是否创建新会话)和 **Session Expiry Interval**(会话保留时长)。这比 3.1.1 的非黑即白灵活得多——你可以"连接时用新会话,但断线后保留 1 小时"这种组合。
### 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 原生支持请求-响应模式。
- **Topic Alias**:用数字 ID 替代重复的 Topic 字符串,大幅减少高频发布场景下的带宽消耗。
- **Subscription Identifier**:订阅时携带一个数字标识,Broker 转发消息时会附带该标识,客户端可以快速区分消息来自哪个订阅,无需解析 Topic。
- **增强认证(Enhanced Authentication)**:支持 SASL 风格的多步骤认证流程(如 SCRAM、Kerberos),取代 3.1.1 仅有的用户名/密码。
### 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 客户端开发的最佳实践。
### 安全与认证
MQTT 的安全性通常在三个层面构建:
1. **传输层加密**:使用 TLS(端口 8883)加密通信链路,防止中间人窃听和篡改。资源极度受限的设备也可以用 TLS-PSK(预共享密钥)降低握手开销。
2. **身份认证**:
- **用户名/密码**:CONNECT 报文中携带,简单但密码以明文传输(必须配合 TLS)。
- **客户端证书**:通过 mTLS 双向认证,Broker 验证客户端证书的合法性,安全性更高。
- **MQTT 5.0 增强认证**:支持 SASL 多步骤挑战-响应机制,适用于企业级 SSO 集成。
3. **授权(ACL)**:Broker 根据客户端身份(通常是 Client ID 或用户名)控制其可以发布/订阅哪些 Topic。ACL 规则格式因 Broker 实现而异,但核心思想一致——最小权限原则。
### 主流 Broker 简介
| Broker | 语言 | 特点 |
|--------|------|------|
| **EMQX** | Erlang/Elixir | 高性能分布式 Broker,支持百万级并发连接,插件生态丰富,企业级首选 |
| **Mosquitto** | C | Eclipse 基金会维护,轻量单机,非常适合嵌入式网关和开发测试 |
| **HiveMQ** | Java | 商业 Broker,提供 MQTT 5.0 全量支持和 Kafka/企业系统桥接 |
| **VerneMQ** | Erlang | 开源分布式 Broker,基于 LevelDB 持久化,适合中小规模集群 |
---
> [!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 适用场景与选型原则]]