【MQTT】带着问题来看

7 阅读8分钟

* 几乎100%参考

MQTT协议

  • MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。

  • MQTT 是二进制协议 (http是文本协议)

  • MQTT最大优点在于,用极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。 作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。

发布和订阅

MQTT使用的发布/订阅消息模式,它提供了一对多的消息分发机制,从而实现与应用程序的解耦。

这是一种消息传递模式,消息不是直接从发送器发送到接收器(即点对点),而是由MQTT server(或称为 MQTT Broker)分发的。

Screenshot 2025-03-24 at 22.02.01.png

QoS(Quality of Service levels)

服务质量是 MQTT 的一个重要特性。当我们使用 TCP/IP 时,连接已经在一定程度上受到保护。但是在无线网络中,中断和干扰很频繁,MQTT 在这里帮助避免信息丢失及其服务质量水平。这些级别在发布时使用。如果客户端发布到 MQTT 服务器,则客户端将是发送者,MQTT 服务器将是接收者。当MQTT服务器向客户端发布消息时,服务器是发送者,客户端是接收者。

QoS  0

这一级别会发生消息丢失或重复,消息发布依赖于底层TCP/IP网络。即:<=1

Screenshot 2025-03-24 at 22.03.42.png QoS  1

QoS 1 承诺消息将至少传送一次给订阅者

Screenshot 2025-03-24 at 22.04.23.png QoS  2

使用 QoS 2,我们保证消息仅传送到目的地一次。为此,带有唯一消息 ID 的消息会存储两次,首先来自发送者,然后是接收者。QoS 级别 2 在网络中具有最高的开销,因为在发送方和接收方之间需要两个流。

Screenshot 2025-03-24 at 22.14.55.png

MQTT 消息头

  • 由 2~5 个字节 组成,包含 
    • 控制报文类型(Control Packet Type)
    • 标志位(Flags)  
    • 剩余长度(Remaining Length)
  • 剩余长度采用可变字节编码(Variable Byte Encoding),最多占 4 字节。
#include <stdint.h>
#include <stdbool.h>

// MQTT 控制报文类型(4 bits)
typedef enum {
    MQTT_CONNECT     = 1,  // 客户端请求连接
    MQTT_CONNACK     = 2,  // 服务端确认连接
    MQTT_PUBLISH     = 3,  // 发布消息
    MQTT_PUBACK      = 4,  // 发布确认
    MQTT_PUBREC      = 5,  // 发布收到(QoS 2)
    MQTT_PUBREL      = 6,  // 发布释放(QoS 2)
    MQTT_PUBCOMP     = 7,  // 发布完成(QoS 2)
    MQTT_SUBSCRIBE   = 8,  // 客户端订阅
    MQTT_SUBACK      = 9,  // 订阅确认
    MQTT_UNSUBSCRIBE = 10, // 取消订阅
    MQTT_UNSUBACK    = 11, // 取消订阅确认
    MQTT_PINGREQ     = 12, // 心跳请求
    MQTT_PINGRESP    = 13, // 心跳响应
    MQTT_DISCONNECT  = 14  // 断开连接
} MqttPacketType;

// PUBLISH 标志位(4 bits)
typedef struct {
    bool retain;    // 保留标志(服务器是否保存消息)
    uint8_t qos;    // QoS 等级(0/1/2)
    bool dup;       // 重复发送标志(重传时置 1)
} MqttPublishFlags;

// MQTT 固定头结构体
typedef struct {
    uint8_t packet_type;        // 控制报文类型(高 4 位)
    union {
        uint8_t flags;          // 通用标志位(低 4 位)
        MqttPublishFlags publish_flags; // PUBLISH 专用标志位
    };
    uint32_t remaining_length;   // 剩余长度(可变字节编码)
} MqttFixedHeader;

uint32_t remaining_length : 当前 MQTT 控制报文(从可变头开始到 payload 结束)的总剩余字节数。它的作用类似于 HTTP 中的 Content-Length,但设计更为紧凑(采用可变字节编码)

### 可变字节编码(Remaining Length 解析)

// 从字节流解析 Remaining Length(返回解析的字节数)
int mqtt_parse_remaining_length(const uint8_t *buf, uint32_t *result) {
    uint32_t multiplier = 1;
    *result = 0;
    int bytes = 0;
    
    do {
        *result += (buf[bytes] & 0x7F) * multiplier;
        multiplier *= 128;
        bytes++;
    } while ((buf[bytes - 1] & 0x80) != 0 && bytes < 4);

    return bytes;
}

// 将 Remaining Length 编码到字节流(返回写入的字节数)
int mqtt_encode_remaining_length(uint32_t length, uint8_t *buf) {
    int bytes = 0;
    
    do {
        uint8_t byte = length % 128;
        length /= 128;
        if (length > 0) byte |= 0x80; // 设置继续位
        buf[bytes++] = byte;
    } while (length > 0 && bytes < 4);

    return bytes;
}

解析 MQTT 固定头

// 解析 MQTT 固定头(返回总字节数)
int mqtt_parse_fixed_header(const uint8_t *buf, MqttFixedHeader *header) {
    // 解析报文类型和标志位
    header->packet_type = (buf[0] >> 4) & 0x0F;  // 高 4 位
    header->flags = buf[0] & 0x0F;               // 低 4 位

    // 解析 Remaining Length
    int len_bytes = mqtt_parse_remaining_length(buf + 1, &header->remaining_length);
    return 1 + len_bytes; // 固定头总字节数
}

// 示例:打印 PUBLISH 消息头
void print_publish_header(const MqttFixedHeader *header) {
    printf("Packet Type: PUBLISH (%d)\n", header->packet_type);
    printf("Flags: retain=%d, qos=%d, dup=%d\n",
           header->publish_flags.retain,
           header->publish_flags.qos,
           header->publish_flags.dup);
    printf("Remaining Length: %d\n", header->remaining_length);
}

关键点

  1. 固定头结构

    • 第 1 字节:高 4 位是报文类型,低 4 位是标志位(PUBLISH 有特殊含义)。
    • 后续字节:remaining_length(可变字节编码)。
  2. 特殊处理

    • PUBLISH 的 flags 需要单独解析为 retainqosdup
    • remaining_length 最大支持 268,435,455 字节(4 字节编码)。

MQTT 消息体(Packet Structure)

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

// MQTT 控制报文类型(高4位)
typedef enum {
    MQTT_CONNECT     = 1,
    MQTT_CONNACK     = 2,
    MQTT_PUBLISH     = 3,
    // ... 其他类型见前文
} MqttPacketType;

// PUBLISH 标志位(低4位)
typedef struct {
    bool retain;
    uint8_t qos;
    bool dup;
} MqttPublishFlags;

// 固定头(Fixed Header)
typedef struct {
    uint8_t packet_type;        // 高4位:报文类型
    union {
        uint8_t flags;          // 低4位:通用标志
        MqttPublishFlags publish_flags; // PUBLISH 专用标志
    };
    uint32_t remaining_length;  // 剩余长度(已解码为uint32_t)
} MqttFixedHeader;

// 可变头(Variable Header) - 以 PUBLISH 为例
typedef struct {
    uint16_t topic_len;         // 主题名长度
    char *topic;                // 主题名(需动态分配)
    uint16_t packet_id;         // QoS > 0 时的 Packet ID
} MqttPublishVarHeader;

// 有效载荷(Payload) - 以 PUBLISH 为例
typedef struct {
    uint8_t *data;              // 消息内容(二进制安全)
    uint32_t data_len;          // 消息长度
} MqttPayload;

// 完整的 MQTT PUBLISH 消息
typedef struct {
    MqttFixedHeader fixed_header;
    MqttPublishVarHeader var_header;
    MqttPayload payload;
} MqttPublishPacket;

重点

  • 有效载体
 // 有效载荷(Payload) - 以 PUBLISH 为例
typedef struct { 
    uint8_t *data; // 消息内容(二进制安全) 
    uint32_t data_len; // 消息长度 
} MqttPayload;
  • 完整的MQTT publish 消息
// 完整的 MQTT PUBLISH 消息
typedef struct {
    MqttFixedHeader fixed_header;
    MqttPublishVarHeader var_header;
    MqttPayload payload;
} MqttPublishPacket;

MQTT 的保留消息(Retained Message)是什么?有什么用途?

  • 服务器保存主题的最后一条消息,新订阅者立即收到。用途:设备上线后快速获取最新状态。

MQTT 的 CONNECT 报文包含哪些关键字段?

-  `clientId``cleanSession``username/password``keepAlive``willTopic/willMessage`(遗嘱消息)。

Payload消息体

Payload消息体是MQTT数据包的第三部分,CONNECT、SUBSCRIBE、SUBACK、UNSUBSCRIBE四种类型的消息 有消息体:

  • CONNECT,消息体内容主要是:客户端的ClientID、订阅的Topic、Message以及用户名和密码
  • SUBSCRIBE,消息体内容是一系列的要订阅的主题以及QoS
  • SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的主题及QoS进行确认和回复。
  • UNSUBSCRIBE,消息体内容是要订阅的主题。

MQTT 的 Clean Session 标志位作用是什么?

    -   `cleanSession=true`:服务器不保存会话状态(无持久化订阅和未确认消息)。
    -   `cleanSession=false`:恢复会话(QoS 1/2 消息重传)。

MQTT 的 Keep Alive 机制如何工作?

-客户端定期发送 PINGREQ,服务器响应 PINGRESP。若超时(通常 1.5×keepAlive),服务器断开连接。

MQTT 5.0 相比 3.1.1 有哪些重要改进?

    -   新增:原因码(Reason Code)、共享订阅(`$share/group/topic`)、消息过期、流量控制。
    -   增强:会话过期(Session Expiry)、用户属性(User Properties)。

MQTT 的 Topic 通配符有哪些?如何使用?

    -   `+`:单层匹配(如 `sensor/+/temperature`)。
    -   `#`:多层匹配(如 `sensor/#` 匹配所有子主题)。

MQTT 的遗嘱消息(Last Will)是什么?应用场景?

  - 客户端异常断开时,服务器自动发布预设消息。场景:设备离线告警。

如何解决 MQTT 消息堆积问题?

    -   服务端:限制消息保留数量/时间(MQTT 5.0 的 `Message Expiry`)。
    -   客户端:提高消费速度或使用 QoS 0。

MQTT 在弱网络环境下如何保证可靠性?

    -   使用 QoS 1/2 + 持久化会话(`cleanSession=false`)。
    -   合理设置 `keepAlive` 和重试机制。 **MQTT 的遗嘱消息(Last Will)是什么?应用场景?**

-   答案:客户端异常断开时,服务器自动发布预设消息。场景:设备离线告警。

如何解决 MQTT 消息堆积问题?

    -   服务端:限制消息保留数量/时间(MQTT 5.0 的 `Message Expiry`)。
    -   客户端:提高消费速度或使用 QoS 0。

MQTT 在弱网络环境下如何保证可靠性?

    -   使用 QoS 1/2 + 持久化会话(`cleanSession=false`)。
    -   合理设置 `keepAlive` 和重试机制。 **MQTT 的遗嘱消息(Last Will)是什么?应用场景?**

-   答案:客户端异常断开时,服务器自动发布预设消息。场景:设备离线告警。

如何解决 MQTT 消息堆积问题?

    -   服务端:限制消息保留数量/时间(MQTT 5.0 的 `Message Expiry`)。
    -   客户端:提高消费速度或使用 QoS 0。

MQTT 在弱网络环境下如何保证可靠性?

    -   使用 QoS 1/2 + 持久化会话(`cleanSession=false`)。
    -   合理设置 `keepAlive` 和重试机制。