MQTT 是一种基于 "发布 / 订阅(Publish/Subscribe)" 模式的异步通信协议,所有设备通过一个中央服务器(Broker)中转消息,实现多设备之间灵活、低功耗、可靠的数据传输。
我用最通俗、最完整、最容易理解 的方式,把 MQTT 工作原理一次性讲透,看完你就彻底懂了👇
MQTT 工作原理(核心一句话)
MQTT 是一种基于"发布/订阅(Publish/Subscribe)"模式的 异步通信 协议,所有设备通过一个中央服务器(Broker)中转消息,实现多设备之间灵活、低功耗、可靠的数据传输。
一、三大核心角色
-
Broker(服务器/代理)
- 消息中转站,负责接收、存储、转发所有消息
-
Publisher(发布者)
- 向某个主题发送消息的设备
-
Subscriber(订阅者)
- 订阅某个主题,等待接收消息的设备
一个设备可以既是发布者,也是订阅者。
二、核心机制:发布 / 订阅模型
发布者 → 发布消息到 Topic → Broker ↓ 订阅者 ← 从 Topic 接收消息 ← Broker
特点:
-
发布者和订阅者完全 解耦(不知道对方存在)
-
一对多、多对多天然支持
-
只要主题相同,就能通信
三、关键概念:Topic(主题)
-
主题是消息的地址/分类标签
-
格式类似路径:
sensor/temperature/room1 device/esp32/cmd -
支持通配符:
-
+:匹配一级 -
#:匹配多级(只能放最后)
-
四、消息可靠保障:QoS 等级
MQTT 定义了 3 种消息送达质量:
-
QoS 0:最多一次
- 发完不管,最快、最轻量
-
QoS 1:至少一次
- 确保收到,可能重复
-
QoS 2: exactly once( exactly-once )
- 确保只收到一次,最可靠
五、保活机制:Keep Alive
-
客户端定时发心跳包(PINGREQ)
-
服务器回复 PINGRESP
-
超时未收到则判定断开连接
六、异常保障:遗嘱消息 LWT
-
设备异常掉线时,Broker 自动发布预设的"遗嘱消息"
-
通知其他设备该设备已离线
七、MQTT 5.0 新增高级原理
-
会话过期
-
消息过期
-
用户属性
-
共享订阅
-
请求响应模式
-
更完善的错误码
八、完整通信流程(极简版)
-
设备连接 Broker
-
设备订阅 Topic
-
另一设备发布消息到该 Topic
-
Broker 转发给所有订阅者
-
订阅者收到并处理消息
-
心跳维持连接
-
异常掉线触发 LWT
九、原理
MQTT 通过发布/订阅模式,让设备只和 Broker 打交道,Broker 负责全部转发,配合 QoS 、心跳、遗嘱等机制,实现轻量、可靠、灵活的 物联网 通信。
注意:MQTT数据包在窗口传输的过程中,数据会进行加密和封包,将数据层层封装为MQTT格式的数据包,避免网络传输的过程当中数据丢失或者被破坏和盗取,
十、举一个例子:
设备打算发送一个temp:25数据,那么这个数据是怎么在网络当中传输,加密和封装,然后在由服务器透传给订阅者的呢?
传输流程:
应用层数据(temp:25) → MQTT 报文封装 → TCP 分段 → IP 打包 → 以太网 / Wi‑Fi 发送 → 路由器 → 互联网 → Broker 服务器 → 解包 → 转发 → 订阅者收到 → 剥离出 temp:25
代码当中的封装:
cpp
esp_mqtt_client_publish(client, "device/sensor", "temp:25", 8, 0, 0);
第一步:封装数据
完整 数据帧 :
|-----------------------|--------------------------|----------------------|
| Fixed header(固定头) | Variable Header(可变头) | Payload (载荷) |
| 1~2 字节 | Topic 长度 + Topic 字符串 | temp:25 |
1)Fixed Header(固定头部)
-
报文类型:**PUBLISH (0x30)**
-
QoS:0
-
保留标志:0
-
剩余长度:计算后面所有数据长度
7 6 5 4 3 2 1 0
+---------+-------+-------+
| 报文类型 | 标志位 | 保留位 |
+---------+-------+-------+
2)Variable Header(可变头部)
- 主题长度:`0x00 0x0D`(device/sensor 共13字节) - 主题内容:`device/sensor`
3)Payload(真正的数据)
- 内容:`temp:25`
第二步:MQTT 包再被 TCP/IP 层层封装
1. MQTT 报文 → 交给 TCP
TCP 加上: - 源端口 - 目的端口(1883) - 序号 - 确认号 → 变成 **TCP 段**
2. TCP 段 → 交给 IP
IP 加上: - 源 IP(ESP32:192.168.31.137) - 目的 IP(Broker 服务器) → 变成 **IP 数据报**
3. IP 数据报 → 交给 Wi‑Fi/以太网
加上 MAC 头: - 源 MAC - 路由 MAC → 变成 **无线帧**,通过天线发送出去
第三步:在互联网上传输
这里可以不用管
第四步:服务器(Broker)
服务器收到 Wi ‑ Fi 帧 → 逐层解包:
- 剥离 MAC → 得到 IP 2. 剥离 IP → 得到 TCP 3. 剥离 TCP → 得到 **MQTT 报文**
Broker 解析 MQTT 报文:
- 读到主题:`device/sensor` - 读到数据:`temp:25`
Broker 做的事情:
**查找所有订阅了 device/sensor 的客户端,把这个 MQTT 包转发给它们。**
第五步:订阅者收到数据
订阅者收到网络包后: 1. 解 Wi‑Fi 帧 2. 解 IP 3. 解 TCP 4. 得到 **MQTT PUBLISH 报文** ESP-MQTT 库自动帮你: - 剥离 Fixed Header - 剥离 Variable Header - 提取出 **Payload = temp:25** 最后进入 回调 :
cpp
case MQTT_EVENT_DATA: printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
在实际的工作当中,其实最主要的就是第一步和第五步,就是如何连接服务器,从服务器上获取数据,那么在这个流程当中,从官方示例可以知道,以esp32为例:
实际工作中 90% 的场景就只关心两件事:
-
怎么连上服务器
-
怎么从服务器收发数据
最重要的就是:让设备连接网络,Wi-Fi,以太网,4g网络都可以,只要能够上网然后,
第0步:准备工作(底层网络)
cpp
nvs_flash_init(); esp_netif_init();
esp_event_loop_create_default();
example_connect(); // 连 Wi‑Fi
作用:让设备能上网。
第一步:创建 MQTT 对象(初始化)
cpp
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = "mqtt://mqtt.eclipseprojects.io",
};
esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
作用:
-
创建 MQTT 客户端实例
-
分配内存、内部状态
-
相当于"造好一辆车"
第二步:注册事件回调(最重要!)
cpp
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
作用:
-
告诉 MQTT 库:有事(连上、收到数据、出错)就调用这个函数
-
所有收发、连接状态都在这里处理
第三步:启动 MQTT 客户端(开始连接)
cpp
esp_mqtt_client_start(client);
作用:
-
启动后台任务
-
自动去连接服务器
-
自动重连
-
相当于"点火开车"
第四步:连接成功后 → 订阅主题
在事件回调里:
cpp
case MQTT_EVENT_CONNECTED:
esp_mqtt_client_subscribe(client, "device/sensor", 0);
作用:
-
告诉服务器:我要听这个主题的消息
-
服务器以后会把消息推给你
第五步:接收数据(你最关心的)
服务器发来数据 → 自动进入:
cpp
case MQTT_EVENT_DATA: // 这里已经是剥完所有协议头的纯数据!
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
第六步:发送数据
cpp
esp_mqtt_client_publish(client, "device/sensor", "temp:25", 0, 0, 0);
作用:发消息给服务器,服务器再转发给所有订阅者。
1. 连 Wi‑Fi 2. mqtt_client_init → 创建对象 3. mqtt_client_register_event → 注册回调 4. mqtt_client_start → 启动并连接服务器 5. 收到 CONNECTED 事件 → 订阅主题 6. 收到 DATA 事件 → 处理数据(业务核心) 7. 随时 publish → 发送数据
官方最简单的TCP代码示例:https://github.com/espressif/esp-idf/tree/v5.5.3/examples/protocols/mqtt/tcp
cpp
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/* MQTT (over TCP) Example with custom outbox
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "esp_log.h"
#include "mqtt_client.h"
static const char *TAG = "MQTT_EXAMPLE";
static void log_error_if_nonzero(const char *message, int error_code)
{
if (error_code != 0) {
ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code);
}
}
/*
* @brief Event handler registered to receive MQTT events
*
* This function is called by the MQTT client event loop.
*
* @param handler_args user data registered to the event.
* @param base Event base for the handler(always MQTT Base in this example).
* @param event_id The id for the received event.
* @param event_data The data for the event, esp_mqtt_event_handle_t.
*/
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id);
esp_mqtt_event_handle_t event = event_data;
esp_mqtt_client_handle_t client = event->client;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
msg_id = esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT_EVENT_DATA");
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err);
log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err);
log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno);
ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno));
}
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
static void mqtt_app_start(void)
{
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = CONFIG_BROKER_URL,
};
#if CONFIG_BROKER_URL_FROM_STDIN
char line[128];
if (strcmp(mqtt_cfg.broker.address.uri, "FROM_STDIN") == 0) {
int count = 0;
printf("Please enter url of mqtt broker\n");
while (count < 128) {
int c = fgetc(stdin);
if (c == '\n') {
line[count] = '\0';
break;
} else if (c > 0 && c < 127) {
line[count] = c;
++count;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
mqtt_cfg.broker.address.uri = line;
printf("Broker url: %s\n", line);
} else {
ESP_LOGE(TAG, "Configuration mismatch: wrong broker url");
abort();
}
#endif /* CONFIG_BROKER_URL_FROM_STDIN */
esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
/* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
/*Let's enqueue a few messages to the outbox to see the allocations*/
int msg_id;
msg_id = esp_mqtt_client_enqueue(client, "/topic/qos1", "data_3", 0, 1, 0, true);
ESP_LOGI(TAG, "Enqueued msg_id=%d", msg_id);
msg_id = esp_mqtt_client_enqueue(client, "/topic/qos2", "QoS2 message", 0, 2, 0, true);
ESP_LOGI(TAG, "Enqueued msg_id=%d", msg_id);
/* Now we start the client and it's possible to see the memory usage for the operations in the outbox. */
esp_mqtt_client_start(client);
}
void app_main(void)
{
ESP_LOGI(TAG, "[APP] Startup..");
ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
esp_log_level_set("*", ESP_LOG_INFO);
esp_log_level_set("mqtt_client", ESP_LOG_VERBOSE);
esp_log_level_set("MQTT_EXAMPLE", ESP_LOG_VERBOSE);
esp_log_level_set("TRANSPORT_BASE", ESP_LOG_VERBOSE);
esp_log_level_set("esp-tls", ESP_LOG_VERBOSE);
esp_log_level_set("TRANSPORT", ESP_LOG_VERBOSE);
esp_log_level_set("custom_outbox", ESP_LOG_VERBOSE);
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
/* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
* Read "Establishing Wi-Fi or Ethernet Connection" section in
* examples/protocols/README.md for more information about this function.
*/
ESP_ERROR_CHECK(example_connect());
mqtt_app_start();
}