第四十二章:MQTT

1.介绍

官网:https://www.emqx.com/zh
MQTT 教程——从入门到精通:https://www.emqx.com/zh/mqtt-guide

MQTT(Message Queuing Telemetry Transport)是一种轻量级、基于发布-订阅模式的消息传输协议,适用于资源受限的设备和低带宽、高延迟或不稳定的网络环境。

1.工作原理

要了解 MQTT 的工作原理,首先需要掌握以下几个概念:MQTT 客户端、MQTT Broker、发布-订阅模式、主题、QoS。

# MQTT 客户端
MQTT 客户端库: https://www.emqx.com/zh/mqtt-client-sdk
MQTT 测试工具: https://www.emqx.com/zh/blog/mqtt-client-tools

# MQTT Broker
2024 年最全面的 MQTT Broker 比较指南: https://www.emqx.com/zh/blog/the-ultimate-guide-to-mqtt-broker-comparison

# 发布-订阅模式
发布-订阅模式: https://www.emqx.com/zh/blog/mqtt-5-introduction-to-publish-subscribe-model

# 主题
主题: https://www.emqx.com/zh/blog/advanced-features-of-mqtt-topics
单层通配符:+
多层通配符:#
系统主题:$ 开头

# QoS
QoS: https://www.emqx.com/zh/blog/introduction-to-mqtt-qos
MQTT 的 QoS 等级 0、1 和 2,比较它们的性能,并提供实际用例,帮助您决定最适合您物联网项目的选项
QoS 0:消息最多传送一次。如果当前客户端不可用,它将丢失这条消息。
QoS 1:消息至少传送一次。
QoS 2:消息只传送一次。
代理可以配置为持久化(存储)消息,通常用于 QoS 1 和 QoS 2。
使用 QoS 0 可能丢失消息,使用 QoS 1 可以保证收到消息,但消息可能重复,使用 QoS 2 可以保证消息既不丢失也不重复

2.工作流程

在了解了 MQTT 的基本组件之后,让我们来看看它的一般工作流程:

  1. 客户端使用 TCP/IP 协议与 Broker 建立连接,可以选择使用 TLS/SSL 加密来实现安全通信。客户端提供认证信息,并指定会话类型(Clean Session 或 Persistent Session)。
  2. 客户端既可以向特定主题发布消息,也可以订阅主题以接收消息。当客户端发布消息时,它会将消息发送给 MQTT Broker;而当客户端订阅消息时,它会接收与订阅主题相关的消息。
  3. MQTT Broker 接收发布的消息,并将这些消息转发给订阅了对应主题的客户端。它根据 QoS 等级确保消息可靠传递,并根据会话类型为断开连接的客户端存储消息。

3.使用教程

MQTT 协议快速入门:https://www.emqx.com/zh/blog/the-easiest-guide-to-getting-started-with-mqtt

# 服务端
Broker 入门指南:https://www.emqx.com/zh/blog/the-ultimate-guide-to-mqtt-broker-comparison
Ubuntu 安装:https://www.emqx.com/zh/blog/how-to-install-emqx-mqtt-broker-on-ubuntu

# 客户端
在线客户端:https://mqttx.app/web-client/
桌面客户端:https://mqttx.app/zh

# EMQX
EMQX 文档:https://docs.emqx.com/zh/emqx/latest/deploy/install-ubuntu-ce.html

2.MQTT 编程

python paho Client 使用地址:https://www.emqx.com/zh/blog/how-to-use-mqtt-in-python

1.准备工作

python 版本

该项目在 Python 3.11 中开发和测试。请确认您安装了正确的 Python 版本,可以使用以下命令:

$ python3 --version             
Python 3.11.8

Paho Client

paho-mqtt 在 2024 年 2 月发布了 2.0.0 版本,相比 1.X 版本有一些重要更新。本文主要演示 1.X 版本的代码,同时也会提供 2.0.0 版本的相应代码,供读者选择合适的 paho-mqtt 版本。

# paho-mqtt 1.X
pip3 install "paho-mqtt<2.0.0"

# paho-mqtt 2.X
pip3 install paho-mqtt

2.示例代码

官方文档:https://www.emqx.com/zh/blog/how-to-use-mqtt-in-python

3.客户端瓶颈

1.网络延迟

  • 网络延迟与带宽
    若 Broker 与客户端之间的网络延迟高(尤其是非本地部署时)或带宽不足,会导致消息传输延迟。即使使用 127.0.0.1(本地回环),仍需确保 Broker 本身未过载。
  • TCP 连接限制
    MQTT 基于 TCP,默认的 TCP 缓冲区大小和操作系统网络栈配置可能限制吞吐量。可通过调整内核参数(如 net.core.somaxconn)优化。

2.消息处理回调效率

on_message 回调复杂度
on_message 函数内部逻辑复杂(如数据库写入、同步 I/O 操作、密集计算),会阻塞主线程,导致消息积压。例如:

def on_message(client, userdata, msg):
    # 同步阻塞操作(如 requests.post() 或 time.sleep())
    save_to_database(msg.payload)  # 假设是同步数据库操作

优化方案

  • 将耗时操作异步化(如使用 asyncio 或多线程)。
  • 使用消息队列(如 RabbitMQ)缓冲消息,解耦处理逻辑。

3.线程模型与 GIL 限制

client.loop_start() 的线程模型
loop_start() 启动的后台线程负责网络 I/O,但 Python 的全局解释器锁(GIL)会限制多线程并发效率。若 on_message 是 CPU 密集型任务,多线程可能无法充分利用多核 CPU。
优化方案

  • 使用多进程替代多线程(如 multiprocessing 模块)。
  • 改用异步框架(如 asyncio-mqttHBMQTT)。

4.Broker 性能限制

Broker 的并发处理能力
若 Broker(如 Mosquitto、EMQX)配置不当(如最大连接数、内存限制),或硬件资源(CPU、内存)不足,会成为整体系统的瓶颈。
优化方案

  • 监控 Broker 的资源使用情况(如 tophtop)。
  • 调整 Broker 配置(如 Mosquitto 的 max_connections)。

5.QoS 与消息确认机制

QoS 级别的影响
QoS 1 或 2 需等待 Broker 确认,增加往返时延(RTT)。例如:

client.publish("topic", payload, qos=1)  # QoS 1 的发布速度慢于 QoS 0

优化方案

  • 若无严格可靠性要求,使用 QoS 0。
  • 若需高可靠性,可批量确认消息(如累积确认)。

6.客户端资源限制

  • CPU/内存占用
    若客户端所在设备的 CPU 或内存资源不足,会导致消息处理延迟。可通过工具(如 psutil)监控资源使用。
  • 文件描述符限制
    高并发场景下可能触发系统的文件描述符上限。通过 ulimit -n 调整限制。

7.消息频率与大小

高频小消息 vs 低频大消息
高频小消息(如传感器数据)可能导致频繁的线程切换和上下文开销;大消息(如图像数据)可能占用过多网络带宽或内存。
优化方案

  • 合并小消息为批量传输(如每 100 条发送一次)。
  • 压缩大消息(如使用 zliblz4)。

8.优化

  • 优化方向

    1. 异步化或并行化消息处理逻辑。
    2. 调整 QoS 和消息传输策略。
    3. 监控并优化 Broker 及客户端资源。
    4. 合理设计消息频率与大小。
  • 异步化 on_message 处理

    # 线程或协程
    
    import asyncio
    from concurrent.futures import ThreadPoolExecutor
    
    executor = ThreadPoolExecutor()
    
    def on_message(client, userdata, msg):
        # 将阻塞操作提交到线程池
        loop = asyncio.get_event_loop()
        loop.run_in_executor(executor, process_message, msg.payload)
    
    async def process_message(payload):
        # 异步处理消息(如调用异步数据库驱动)
        await async_db.save(payload)
    
  • 使用异步 MQTT 客户端

    import asyncio
    from asyncio_mqtt import Client
    
    async def main():
        async with Client("127.0.0.1") as client:
            await client.subscribe("topic")
            async with client.messages() as messages:
                async for msg in messages:  # 异步迭代消息
                    await process_message(msg.payload)
    
    asyncio.run(main())
    

4.执行循序

client.on_connect = on_connect  # 连接 broker 时 broker 响应的回调
client.on_message = on_message  # 接收到订阅消息时的回调

result = client.connect("127.0.0.1", 1883, 60)  # 连接到broker

# 方法一:使用 loop_start() 
# 启动后台线程,主线程可以继续其他操作,以下使用 while 保持主线程的运行
client.loop_start()
while (True):
    time.sleep(1)

# 方法一:使用 loop_forever()
# 阻塞主线程,持续处理消息
# client.loop_forever() 

5.示例

# run.py
import paho.mqtt.client as mqtt
import time
import json
import MQTTClientHandel


client = mqtt.Client()
client.username_pw_set("test1", password="test1")


def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))  
    print(client.is_connected())
    client.subscribe(mqttPrefix+"/Data/#")
    
    
def on_message(client, userdata, msg):
  	# topic:以下是一个 5 层的 topic
    # ['Test', 'Data', '80000001', 'DataType', '876']
    topicstrs = msg.topic.split('/')
    res = MQTTClientHandel.handel(msg.topic, topicstrs[3], msg.payload)
    # if True:
    #     client.publish(res.tpoic, qos=0, payload=res.payload)
    print(msg.topic + " " + json.dumps(res.__dict__))


client.on_connect = on_connect  # 连接 broker 时 broker 响应的回调
client.on_message = on_message  # 接收到订阅消息时的回调
result = client.connect("127.0.0.1", 1883, 60)  # 连接到broker
client.loop_start()

while (True):
    time.sleep(1)
# MQTTClientHandel.py
def handel(topic, cmd, payload):
    respTopic = topic.replace("/Data", "")
    status = "1"  # 默认状态设置为1

    res = respDto(respTopic, "")
    try:
        res.payload = handels().case_to_function(cmd)(payload)
    except:
        status = "0"  # 失败了 将状态设置为0
    res.tpoic += "/" + status
    return res


class respDto:
    tpoic = ""
    payload = []
    isReplay = True

    def __init__(self, topic, payload) -> None:
        self.tpoic = topic
        self.payload = payload
        pass


class handels:
    def case_to_function(self, cmd):
        fun_name = "handel_" + str(cmd)
        # ['IndexData', 'WaveData']
        method = getattr(self, fun_name)
        return method

    def handel_IndexData(self, payload):
        """
        接收指标数据,转存 tdengine
        """
        print(payload)
        return ""

    def handel_WaveData(self, payload):
        """
        接收波形数据, 转存 minio
        """
        print(payload)
        return ""

6.asyncio-mqtt

官方地址:https://pypi.org/project/asyncio-mqtt/

1.安装

# 它需要 Python 3.7+ 才能运行, 唯一的依赖项是paho-mqtt
pip install asyncio-mqtt

2.示例

import json
import asyncio
import asyncio_mqtt
import multiprocessing
import MQTTClientHandel_async
from utils import settings
from utils.kafka_driver import KafkaDriver
from utils.minio_api import MinioUtil
from utils.codes import code_dict, prefixes


def split_group(group_num=5):
    codes = ['Doton/Online/81000001/#', ]
    topics = [f'Doton/Online/80000{i}' for i in range(1, 9)]
    group_list = [codes[i::group_num] for i in range(group_num)]
    return group_list


async def mqtt_connect_handle(topics, kafka_producer, minio_producer, rtd):
    """
    协程主函数
    连接MQTT:处理接受信息
    topic: Doton/Online/81000001/IndexData/3526
    """
    try:
        async with asyncio_mqtt.Client(
            "127.0.0.1",
            port=1883,
            username='test',
            password='test'
        ) as client:
            await client.subscribe(topic=','.join(topics))
            async with client.messages() as message:
                async for msg in message:
                    topic_name = msg.topic.value
                    topicstrs = topic_name.split('/')
                    res = await MQTTClientHandel_async.handel(
                        topic_name, topicstrs[3], msg.payload, topicstrs[2],
                        kafka_producer, minio_producer, rtd
                    )
                    print(topic_name + " " + json.dumps(res.__dict__))
    except asyncio_mqtt.MqttError as e:
        print(f"MQTT 错误: {e}")
        await asyncio.sleep(5)  # 等待后重连
        await mqtt_connect_handle(topics, kafka_producer, minio_producer, rtd)


def run(topics):
    # 进程内初始化 Kafka
    kafka_producer = KafkaDriver(settings.KAFKA_ALGO_PUSH_SETTING["kafka_servers"])
    # 初始化 Minio
    minio_producer = MinioUtil()
    asyncio.run(mqtt_connect_handle(topics, kafka_producer, minio_producer, rtd))


if __name__ == '__main__':
    print('数据转存启动...')
    multiprocessing.set_start_method("spawn")
    group_list = split_group()
    run(group_list[0])
    processes = []
    for topics in group_list:
        p = multiprocessing.Process(
            target=run,
            args=(topics,),
            name=f'Process-{group_list.index(topics)}'
        )
        processes.append(p)
        p.start()

    for pro in processes:
        pro.join()

posted @ 2025-04-09 09:47  亦双弓  阅读(105)  评论(0)    收藏  举报