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

12 KiB
Raw Blame History

tags, create time
tags create time
MQ
MQTT
IoT
协议
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 为中心的发布-订阅模型,发布者和订阅者完全解耦:

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 的每条报文由三部分组成:

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 客户端从连接到断开的完整流程如下:

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)

通过四次握手保证消息恰好送达一次,是最高等级但也最重的语义。

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 客户端实现消息的发布和订阅。

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 的连接。

关联笔记