☰
【ESP32笔记】MQTT协议知识点
2026/10/15 19:06:02 网站建设 项目流程

本节目标:搞懂发布/订阅模型、Topic、QoS、Keep Alive、遗嘱消息等概念,理解 MQTT 为什么这么设计,为第 19 节写代码打基础

一、本节要解决的问题

用 HTTP 做物联网通信有几个别扭的地方:

  • HTTP 是"客户端请求一次、服务器回答一次"的模型,服务器不能主动给设备推消息,设备想看新数据只能不停轮询;
  • 每次请求都要带一大堆头部信息,报文开销大;
  • 设备数量一多,轮询会让服务器压力巨大。

MQTT 面向的正是这种场景:设备多、带宽小、网络不稳定、功耗敏感。它把通信模型从"请求/响应"换成了"发布/订阅",服务器(Broker)负责中转,任何一方都能主动发消息。

本节不写代码,先把协议本身讲清楚——第 19 节的每一行 ESP-IDF 代码,背后都是本节的一个概念。

二、相关知识

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议):

  • 由 IBM 在 1999 年发布;
  • 构建在TCP/IP 之上,属于应用层协议,和 HTTP、FTP 是同一层;
  • 基于发布/订阅(publish/subscribe)模式;
  • 最大优点是"以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务"——低开销、低带宽占用,非常适合物联网、小型设备、移动应用。

第 19 节的工程用 ESP-IDF 自带的 esp-mqtt 组件实现,sdkconfig中CONFIG_MQTT_PROTOCOL_311=y,即MQTT 3.1.1版本协议。协议本身与芯片无关,换成 STM32、树莓派也是同一套协议。

三、知识点

3.1 发布/订阅模型与 Broker

角色职责
发布者 Publisher把消息发布到某个 Topic
订阅者 Subscriber向 Broker 订阅自己关心的 Topic
Broker消息服务器,保存订阅关系,收到消息后转发给所有匹配的订阅者

三者完全解耦:发布者不知道谁在收,订阅者不知道消息从哪来,新增一个观察者不需要改动发布者。

教程的例子:手机、电脑先订阅了主题temperature;温度传感器向temperature发布一条内容为21℃的消息;Broker 收到后查出有哪些客户端订阅了该主题,再把消息转发给它们。

3.2 Topic 主题与通配符

Topic(主题)是消息传递的基础,代表消息的流向;本质是一串字符串,可以用正斜杠/分级。

  • 订阅/编程知识/嵌入式/C语言,只会收到这个主题的消息;别人发的是/编程知识/嵌入式/C++,因为主题不匹配,收不到;
  • 通配符(只能用于订阅,不能用于发布):
    • +只能匹配当前一层,例如/编程知识/+/C语言;
    • #能匹配往下所有层,只能出现在末尾,例如订阅/编程知识/嵌入式/#,可以收到/编程知识/嵌入式/C++、/编程知识/嵌入式/C、/编程知识/嵌入式/C/函数指针用法等。

教程口诀:+只能匹配当前一层,#能匹配往下所有。

注意:/test/topic1和test/topic1是两个不同的主题——开头带/意味着第一个层级是空的。发布和订阅两边的字符串必须一字不差(大小写敏感),本工程用的就是带前导斜杠的/test/topic1、/test/topic2。

3.3 MQTT 报文长什么样

每个报文 =固定包头 + 可变包头 + 载荷(后两部分不一定有,由报文类型决定)。

固定包头第 1 字节:

位含义
7~4报文类型(1~14)
3~0该报文的特殊标志位

对PUBLISH报文,标志位是最有用的:bit0 = RETAIN、bit1~2 = QoS、bit3 = DUP。其余报文大多填 0000,SUBSCRIBE/UNSUBSCRIBE/PUBREL 固定填 0010;接收方收到不可用的标志位,必须关闭连接。

  • DUP:发布消息的副本标志,置 1 表示这是一次重发(只对 QoS>0 的消息有意义),用于保证可靠传输,但不能用来检测消息是否重复;
  • QoS:发布消息的服务质量,见 3.8;
  • RETAIN:置 1 时 Broker 会为该主题保存这条消息,新订阅者一订阅就立刻收到,见 3.9。

从第 2 字节开始是剩余长度:可变包头 + 载荷的总长度,最多允许 4 个字节。每个字节只有低 7 位有效,最高位起"延续位"作用(为 1 表示后面还有长度字节),所以最后一个字节最大只能是0x7F(127);4 个字节能表示的最大长度是 268 435 455 字节。

教程里的解码例子,三个字节0xE7 0x8D 0x11:

第1字节 0xE7:最高位=1,取低7位 0x67 第2字节 0x8D:最高位=1,取低7位 0x0D 第3字节 0x11:最高位=0,长度结束,取低7位 0x11 剩余长度 = 0x67 + (0x0D << 7) + (0x11 << 14) = 103 + 1664 + 278528 = 280295

3.4 报文类型一览

编号报文作用
1CONNECT客户端请求连接
2CONNACK服务端确认连接
3PUBLISH发布消息
4PUBACKQoS1 发布确认
5PUBRECQoS2:发布收到
6PUBRELQoS2:发布释放
7PUBCOMPQoS2:发布完成
8SUBSCRIBE订阅主题
9SUBACK订阅确认
10UNSUBSCRIBE取消订阅
11UNSUBACK取消订阅确认
12PINGREQ心跳请求
13PINGRESP心跳响应
14DISCONNECT客户端主动断开

教程里这张表以图片给出,正文没有编号。上表按 MQTT 3.1.1 标准补充,方便对照抓包与理解后文。

3.5 CONNECT / CONNACK:建立连接

在发布、订阅之前,客户端必须先发CONNECT报文,Broker 验证信息后回CONNACK告知结果,得到肯定回应才能进行后续操作。

CONNECT 的可变包头由协议名、协议等级、连接标志、Keep Alive 秒数构成;载荷按固定顺序排列:客户端标识符、遗嘱主题、遗嘱消息、用户名、密码(只有连接标志里指示存在的字段才会出现)。

连接标志里的关键位:

标志含义
Clean Session0 = 建立持久会话,Broker 保存该客户端的订阅和未接收消息;1 = 不保存,并在连接时清除该客户端之前保存的会话数据
Will Flag是否使用遗嘱消息 LWT
Will QoS发布遗嘱消息时的 QoS
Will Retain遗嘱消息的 Retain 标识
User Name / Password载荷中是否带用户名、密码

CONNACK 的可变包头只有两个字节:连接确认标志 + 返回码。当 CONNECT 中 Clean Session=1 时,确认标志里的"当前会话标志"(Session Present)为 0。

返回码的含义:

返回码含义
0x00连接接受
0x01不支持的协议版本
0x02客户端标识符被拒绝
0x03服务器不可用
0x04用户名或密码错误
0x05未授权

调试思路:如果 ESP32 反复连不上 Broker,抓包看到 CONNACK 返回 0x04/0x05,就是账号密码问题而不是网络问题。

3.6 Keep Alive 心跳保活

TCP 连接"看起来还在",不代表对方还活着(拔网线、设备断电时,本机 TCP 不会立刻察觉)。Keep Alive 用来检测这种"半死"连接:

  • 建连接时客户端在 CONNECT 里承诺一个时间(秒),比如 60 秒;
  • 空闲(没有其他报文)时,客户端按这个周期发送PINGREQ,Broker 用PINGRESP应答;
  • Broker 如果在1.5 倍 Keep Alive时间内没有收到该客户端的任何报文,就认为它掉线,关闭连接——如果客户端注册过遗嘱消息,这时就会发布遗嘱(见 3.7)。

在 ESP-IDF 中对应session.keepalive,默认120 秒,并且客户端会按大约一半的时间主动通信,留出重试余地;心跳由 esp-mqtt 组件后台自动完成,应用代码不用管。本工程没有显式配置,用的就是默认值。

小坑:在 esp-mqtt 里把session.keepalive设为 0 并不会关闭心跳,而是回退到默认值;真想关闭要用disable_keepalive。Keep Alive 设得越长,Broker 判定掉线越慢,遗嘱消息也就越晚发出。

3.7 遗嘱消息 Will(LWT)

设备可能突然断电、断网,自己没机会说"我下线了"。遗嘱消息就是提前写好交给 Broker 的遗言:

  • 连接时(Will Flag=1)把"遗嘱主题 + 遗嘱内容 + Will QoS + Will Retain"告诉 Broker;
  • 当 Broker 检测到客户端非正常断开(心跳超时、TCP 异常断开)时,代替客户端把这条消息发布到遗嘱主题;
  • 如果客户端主动发 DISCONNECT 正常断开,Broker 不会发布遗嘱;
  • 典型用法:设备上线时在/status主题发布online,同时把遗嘱设为在/status发布offline,订阅者就能实时知道设备掉线。

3.8 QoS:三个消息等级

QoS语义消息流程特点
0接收方最多收到一次PUBLISH最快,无确认,可能丢消息;适合高频、可丢的数据
1接收方最少收到一次PUBLISH → PUBACK不丢,但对方没回 PUBACK 就重发,可能收到重复
2接收方只能收到一次PUBLISH → PUBREC → PUBREL → PUBCOMP最可靠,四次握手开销最大;适合计费、开关控制等

MQTT 还规定:消息最终按"发布 QoS 和订阅 QoS 中较小的那个"投递。例如发布用 QoS2、订阅用 QoS1,最终按 QoS1 交付。所以 QoS 需要发布、订阅两端配合设置,而不是"订阅端写得高就一定更可靠"。

3.9 RETAIN 保留消息

  • PUBLISH 报文里 RETAIN=1 时,Broker 会为该主题保存最后一条retained 消息;
  • 之后有新的订阅者订阅该主题,Broker 会立即把这条消息发给它,不用等下一次发布;
  • 常与遗嘱配合:设备状态用 retained 消息发布,新接入的观察者一订阅就能看到当前状态,而不是干等状态变化。

3.10 这些概念在 ESP-IDF 里对应什么

第 19 节工程的配置里,本节的概念都能找到对应字段(以 ESP-IDF 5.x 的esp_mqtt_client_config_t分组结构为准):

MQTT 概念ESP-IDF 配置字段 / 使用位置
Broker 地址与端口broker.address.uri、broker.address.port
客户端 IDcredentials.client_id
用户名 / 密码credentials.username/credentials.authentication.password
QoS发布:esp_mqtt_client_publish(..., qos, ...);订阅:subscribe 的参数
RETAINesp_mqtt_client_publish的 retain 参数
Keep Alivesession.keepalive(默认 120 秒)
遗嘱消息session.last_will.topic/.msg/.qos/.retain
Clean Sessionsession.disable_clean_session(默认 false,即使用 Clean Session)

本工程(esp32-board/mqtt)只显式配置了前三行——地址、客户端 ID、用户名密码;QoS 和 RETAIN 在调用发布/订阅 API 时传入;Keep Alive、遗嘱、Clean Session 都没写,走组件默认值。

另外:ESP-IDF 4.x 的配置字段是扁平写法(.uri、.client_id、.keepalive、.lwt_topic),5.x 才改成分组结构。对照网上老教程抄代码时,先确认自己用的版本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询