第四十二章: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 的基本组件之后,让我们来看看它的一般工作流程:
- 客户端使用 TCP/IP 协议与 Broker 建立连接,可以选择使用 TLS/SSL 加密来实现安全通信。客户端提供认证信息,并指定会话类型(Clean Session 或 Persistent Session)。
- 客户端既可以向特定主题发布消息,也可以订阅主题以接收消息。当客户端发布消息时,它会将消息发送给 MQTT Broker;而当客户端订阅消息时,它会接收与订阅主题相关的消息。
- 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-mqtt或HBMQTT)。
4.Broker 性能限制
Broker 的并发处理能力
若 Broker(如 Mosquitto、EMQX)配置不当(如最大连接数、内存限制),或硬件资源(CPU、内存)不足,会成为整体系统的瓶颈。
优化方案:
- 监控 Broker 的资源使用情况(如
top、htop)。 - 调整 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 条发送一次)。
- 压缩大消息(如使用
zlib或lz4)。
8.优化
-
优化方向:
- 异步化或并行化消息处理逻辑。
- 调整 QoS 和消息传输策略。
- 监控并优化 Broker 及客户端资源。
- 合理设计消息频率与大小。
-
异步化
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()

浙公网安备 33010602011771号