mqttxx 2.0.2__py3-none-any.whl → 3.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mqttxx/__init__.py CHANGED
@@ -3,21 +3,25 @@
3
3
  提供:
4
4
  - MQTTClient: MQTT 连接管理(自动重连、订阅队列化、TLS/SSL 支持)
5
5
  - RPCManager: 双向对等 RPC 调用(超时控制、权限检查、并发限制)
6
+ - EventChannelManager: 高吞吐事件广播通道(单向、无返回值)
6
7
  - 配置对象: MQTTConfig, TLSConfig, AuthConfig, RPCConfig 等
7
- - 协议定义: RPCRequest, RPCResponse
8
+ - 协议定义: RPCRequest, RPCResponse, EventMessage
8
9
  - 异常系统: 统一错误码和异常层次
9
10
  """
10
11
 
11
- __version__ = "2.0.0"
12
+ __version__ = "3.1.0"
12
13
  __author__ = "MQTTX Team"
13
14
 
14
15
  # 核心客户端
15
- from .client import MQTTClient
16
+ from .client import MQTTClient, RawAPI
16
17
 
17
18
  # RPC 管理器
18
19
  from .rpc import RPCManager
19
20
  from .conventions import ConventionalRPCManager
20
21
 
22
+ # Event Channel 管理器
23
+ from .events import EventChannelManager, EventMessage
24
+
21
25
  # 配置对象
22
26
  from .config import (
23
27
  MQTTConfig,
@@ -31,7 +35,7 @@ from .config import (
31
35
  from .protocol import (
32
36
  RPCRequest,
33
37
  RPCResponse,
34
- parse_message,
38
+ # parse_message 是内部 API,不再导出
35
39
  )
36
40
 
37
41
  # 异常系统
@@ -54,9 +58,13 @@ from .exceptions import (
54
58
  __all__ = [
55
59
  # MQTT 客户端
56
60
  "MQTTClient",
61
+ "RawAPI", # Raw API (逃生通道)
57
62
  # RPC 管理器
58
63
  "RPCManager",
59
64
  "ConventionalRPCManager", # 约定式 RPC(强约束系统)
65
+ # Event Channel 管理器
66
+ "EventChannelManager",
67
+ "EventMessage",
60
68
  # 配置对象
61
69
  "MQTTConfig",
62
70
  "TLSConfig",
@@ -66,7 +74,7 @@ __all__ = [
66
74
  # 协议定义
67
75
  "RPCRequest",
68
76
  "RPCResponse",
69
- "parse_message",
77
+ # parse_message 已移除 - 内部 API
70
78
  # 异常系统
71
79
  "ErrorCode",
72
80
  "MQTTXError",
mqttxx/client.py CHANGED
@@ -1,15 +1,166 @@
1
1
  # aiomqtt 高级封装 - 基于纯 async/await 架构
2
2
 
3
3
  import asyncio
4
- import json
4
+ import os
5
5
  import ssl
6
- from typing import Callable, Optional
6
+ from typing import Callable, Optional, Any, TYPE_CHECKING
7
7
  from loguru import logger
8
8
  import aiomqtt
9
+ from paho.mqtt.matcher import MQTTMatcher
9
10
 
10
11
  from .config import MQTTConfig
11
- from .exceptions import MessageError
12
- from .protocol import parse_message
12
+
13
+ if TYPE_CHECKING:
14
+ from .client import MQTTClient
15
+
16
+
17
+ class RawAPI:
18
+ """Raw MQTT API (逃生通道)
19
+
20
+ 绕过协议层,直接操作 bytes。
21
+
22
+ 警告:
23
+ 这是逃生通道,仅在以下场景使用:
24
+ - 自定义协议 (非 JSON/RPC)
25
+ - 性能敏感场景 (避免编解码)
26
+ - 与第三方系统集成
27
+
28
+ 示例:
29
+ # 订阅原始 bytes
30
+ client.raw.subscribe("sensor/+", lambda t, b: process(b))
31
+
32
+ # 发布原始 bytes
33
+ await client.raw.publish("topic", b"\\x01\\x02\\x03")
34
+ """
35
+
36
+ def __init__(self, client: "MQTTClient"):
37
+ """初始化 Raw API
38
+
39
+ Args:
40
+ client: MQTTClient 实例
41
+ """
42
+ self._client = client
43
+
44
+ def subscribe(self, pattern: str, handler: Callable[[str, bytes], Any]):
45
+ """订阅原始消息 (bytes)
46
+
47
+ Args:
48
+ pattern: MQTT topic pattern (支持通配符 +/#)
49
+ handler: 回调函数
50
+ - 签名: async def handler(topic: str, payload: bytes)
51
+ - topic: 实际收到消息的 topic
52
+ - payload: 原始 bytes 数据 (未解码)
53
+
54
+ 并发行为:
55
+ - 同一 pattern 的多个 handlers 按注册顺序**顺序调用**(非并发)
56
+ - 如需并发处理,请在 handler 内部使用 asyncio.create_task()
57
+
58
+ 示例:
59
+ # 基础用法
60
+ async def on_message(topic: str, payload: bytes):
61
+ data = json.loads(payload.decode('utf-8'))
62
+ print(f"收到消息: {topic} -> {data}")
63
+
64
+ client.raw.subscribe("sensor/+/data", on_message)
65
+
66
+ # 顺序处理(默认)
67
+ async def handler_seq(topic: str, payload: bytes):
68
+ await process_data(payload) # 阻塞后续 handlers
69
+
70
+ # 并发处理(手动)
71
+ async def handler_async(topic: str, payload: bytes):
72
+ asyncio.create_task(process_async(payload)) # 不阻塞
73
+ """
74
+ # 追踪 handlers (辅助结构)
75
+ if pattern not in self._client._raw_handlers:
76
+ self._client._raw_handlers[pattern] = []
77
+ self._client._raw_handlers[pattern].append(handler)
78
+
79
+ # 同步到 MQTTMatcher (核心匹配引擎)
80
+ self._client._raw_matcher[pattern] = self._client._raw_handlers[pattern]
81
+
82
+ # 订阅 MQTT topic
83
+ self._client._subscriptions.add(pattern)
84
+ if self._client._client:
85
+ asyncio.create_task(self._client._do_subscribe(pattern))
86
+ else:
87
+ logger.info(f"Raw 订阅已队列化 (等待连接) - pattern: {pattern}")
88
+
89
+ logger.debug(f"Raw 订阅成功 - pattern: {pattern}")
90
+
91
+ async def publish(self, topic: str, payload: bytes, qos: int = 0):
92
+ """发布原始消息 (bytes)
93
+
94
+ Args:
95
+ topic: 目标主题
96
+ payload: 消息载荷 (bytes)
97
+ qos: QoS 等级 (0/1/2)
98
+
99
+ 异常:
100
+ aiomqtt.MqttError: 发布失败
101
+ """
102
+ if not self._client._client:
103
+ logger.warning(f"发布失败:客户端未连接 - topic: {topic}")
104
+ return
105
+
106
+ try:
107
+ await self._client._client.publish(topic, payload, qos=qos)
108
+ except aiomqtt.MqttError as e:
109
+ logger.error(f"发布失败 - topic: {topic}, error: {e}")
110
+
111
+ def unsubscribe(self, pattern: str, handler: Optional[Callable] = None):
112
+ """取消订阅(修复 P0-B)
113
+
114
+ Args:
115
+ pattern: MQTT topic pattern
116
+ handler: 要移除的 handler(None = 移除所有)
117
+
118
+ 示例:
119
+ # 移除特定 handler
120
+ client.raw.unsubscribe("test/+", my_handler)
121
+
122
+ # 移除所有 handler
123
+ client.raw.unsubscribe("test/+")
124
+ """
125
+ if pattern not in self._client._raw_handlers:
126
+ logger.debug(f"Raw 取消订阅失败:pattern 不存在 - {pattern}")
127
+ return
128
+
129
+ if handler is None:
130
+ # 移除所有 handler
131
+ del self._client._raw_handlers[pattern]
132
+ del self._client._raw_matcher[pattern]
133
+ self._client._subscriptions.discard(pattern)
134
+
135
+ # 发送 MQTT UNSUBSCRIBE
136
+ if self._client._client:
137
+ asyncio.create_task(self._client._client.unsubscribe(pattern))
138
+
139
+ logger.debug(f"Raw 取消订阅(全部)- pattern: {pattern}")
140
+ else:
141
+ # 移除特定 handler
142
+ handlers = self._client._raw_handlers[pattern]
143
+ if handler in handlers:
144
+ handlers.remove(handler)
145
+
146
+ # 如果没有 handler 了,完全取消订阅
147
+ if not handlers:
148
+ del self._client._raw_handlers[pattern]
149
+ del self._client._raw_matcher[pattern]
150
+ self._client._subscriptions.discard(pattern)
151
+
152
+ if self._client._client:
153
+ asyncio.create_task(self._client._client.unsubscribe(pattern))
154
+
155
+ logger.debug(
156
+ f"Raw 取消订阅(全部,最后一个 handler)- pattern: {pattern}"
157
+ )
158
+ else:
159
+ logger.debug(
160
+ f"Raw 取消订阅(部分)- pattern: {pattern}, 剩余 {len(handlers)} 个 handler"
161
+ )
162
+ else:
163
+ logger.debug(f"Raw 取消订阅失败:handler 不存在 - pattern: {pattern}")
13
164
 
14
165
 
15
166
  class MQTTClient:
@@ -19,6 +170,35 @@ class MQTTClient:
19
170
  - aiomqtt 基于 paho-mqtt 封装,成熟稳定
20
171
  - 不自动重连,需要手动实现重连循环(官方推荐模式)
21
172
  - 使用 `async for message in client.messages` 异步迭代器
173
+
174
+ 设计约束:
175
+ - **单 Event Loop 设计**: 所有方法(subscribe/unsubscribe/handler)
176
+ 必须在同一个 asyncio event loop 中调用
177
+ - **不支持多线程/多 loop 并发**: 如果需要在多个线程中使用,
178
+ 请为每个线程创建独立的 MQTTClient 实例
179
+
180
+ 并发安全:
181
+ - ✅ 单 loop 内并发调用 subscribe/unsubscribe:安全
182
+
183
+ 示例:
184
+ # ✅ 正确:单 loop
185
+ async def main():
186
+ client = MQTTClient(config)
187
+ await client.connect()
188
+ client.raw.subscribe("topic", handler1) # 安全
189
+ client.raw.subscribe("topic", handler2) # 安全
190
+
191
+ # ❌ 错误:多线程
192
+ def thread1():
193
+ client.raw.subscribe("topic", handler1) # 不安全
194
+
195
+ def thread2():
196
+ client.raw.subscribe("topic", handler2) # 不安全
197
+
198
+ # ✅ 正确:多线程各自创建实例
199
+ def thread1():
200
+ client1 = MQTTClient(config)
201
+ asyncio.run(client1.connect())
22
202
  """
23
203
 
24
204
  def __init__(self, config: MQTTConfig):
@@ -33,12 +213,29 @@ class MQTTClient:
33
213
  self.config = config
34
214
  self._client: Optional[aiomqtt.Client] = None
35
215
  self._subscriptions: set[str] = set() # 订阅列表(用于重连恢复)
36
- self._message_handlers: dict[str, Callable] = {} # topic → handler 映射
216
+
217
+ # Raw 订阅(使用 MQTTMatcher 进行通配符匹配)
218
+ # _raw_matcher 是核心匹配引擎(用于 iter_match)
219
+ # _raw_handlers 是辅助追踪结构(因 MQTTMatcher 不支持 getitem/contains)
220
+ self._raw_matcher = MQTTMatcher()
221
+ self._raw_handlers: dict[str, list[Callable]] = {}
222
+
37
223
  self._running = False
38
224
  self._connected = False # 修复 P0-2:真实连接状态标志
39
225
  self._reconnect_task: Optional[asyncio.Task] = None
40
226
  self._message_task: Optional[asyncio.Task] = None
41
227
 
228
+ # 消息处理队列和 Workers(修复 P0-A)
229
+ self._message_queue: asyncio.Queue = asyncio.Queue(
230
+ maxsize=config.message_queue_maxsize
231
+ )
232
+ self._workers: list[asyncio.Task] = []
233
+ # Worker 数量:默认 CPU核数×2(IO-bound 最优)
234
+ self._num_workers = config.num_workers or (os.cpu_count() or 1) * 2
235
+
236
+ # Raw API (逃生通道)
237
+ self._raw_api = RawAPI(self)
238
+
42
239
  async def connect(self):
43
240
  """连接到 MQTT Broker
44
241
 
@@ -51,6 +248,12 @@ class MQTTClient:
51
248
 
52
249
  self._running = True
53
250
 
251
+ # 启动消息处理
252
+ for i in range(self._num_workers):
253
+ worker = asyncio.create_task(self._worker(i), name=f"mqtt_worker_{i}")
254
+ self._workers.append(worker)
255
+ logger.info(f"MQTT 已启动 {self._num_workers} 个消息处理 worker")
256
+
54
257
  self._reconnect_task = asyncio.create_task(
55
258
  self._reconnect_loop(), name="mqtt_reconnect"
56
259
  )
@@ -60,6 +263,15 @@ class MQTTClient:
60
263
  self._running = False
61
264
  self._connected = False # 修复 P0-2:标记为未连接
62
265
 
266
+ # 等待 workers 处理完队列(修复 P0-A)
267
+ if self._workers:
268
+ # 等待所有 workers 完成(最多 5 秒)
269
+ _, pending = await asyncio.wait(self._workers, timeout=5.0)
270
+ # 取消未完成的 workers
271
+ for w in pending:
272
+ w.cancel()
273
+ self._workers.clear()
274
+
63
275
  # 取消后台任务
64
276
  if self._reconnect_task:
65
277
  self._reconnect_task.cancel()
@@ -68,15 +280,18 @@ class MQTTClient:
68
280
  except asyncio.CancelledError:
69
281
  pass
70
282
 
71
- # 取消消息处理任务
283
+ # 取消消息处理任务(修复 P0-D)
284
+ # 这会导致 _reconnect_loop 中的 async with 块退出,aiomqtt 自动清理连接
72
285
  if self._message_task:
73
286
  self._message_task.cancel()
74
287
  try:
75
288
  await self._message_task
76
289
  except asyncio.CancelledError:
77
290
  pass
78
- if self._client:
79
- self._client = None
291
+
292
+ # aiomqtt.Client 使用 async with 管理连接,退出时自动清理
293
+ # 不需要显式调用 disconnect()(该方法不存在)
294
+ self._client = None
80
295
 
81
296
  logger.info("MQTT 客户端已断开")
82
297
 
@@ -190,14 +405,14 @@ class MQTTClient:
190
405
  await asyncio.sleep(interval)
191
406
 
192
407
  async def _message_loop(self):
193
- """消息处理循环(aiomqtt 核心模式)
408
+ """消息接收循环(修复 P0-A:只负责接收,不处理)
194
409
 
195
- 使用 async for 迭代消息,替代 gmqtt 的 on_message 回调
410
+ 使用 async for 迭代消息,将消息放入队列由 workers 处理
196
411
 
197
- 关键特性:
198
- - 非阻塞:异步迭代器自动 yield 控制权
199
- - 顺序处理:每条消息按顺序处理
200
- - 并发处理:每条消息在独立 Task 中处理(不阻塞迭代器)
412
+ 关键改进:
413
+ - 接收和处理分离:避免高吞吐下任务爆炸
414
+ - 有界队列:maxsize 可配置(默认 100k),作为保险丝
415
+ - 队列满时阻塞:形成自然背压,CPU/延迟会变明显(扩容信号)
201
416
 
202
417
  异常处理:
203
418
  - asyncio.CancelledError:任务被取消,退出循环
@@ -208,19 +423,9 @@ class MQTTClient:
208
423
  return
209
424
 
210
425
  try:
211
- # region 调用代码溯源(aiomqtt.Client.messages)
212
- # aiomqtt.Client.messages 是一个异步迭代器
213
- # 内部调用:
214
- # paho.mqtt.client.Client._handle_on_message()
215
- # → 将消息放入队列
216
- # → aiomqtt 从队列中取出消息
217
- # GitHub:https://github.com/sbtinstruments/aiomqtt/blob/main/aiomqtt/client.py#L400
218
- # endregion
219
426
  async for message in self._client.messages:
220
- # 异步处理消息(不阻塞迭代器)
221
- asyncio.create_task(
222
- self._handle_message(message), name=f"handle_{message.topic}"
223
- )
427
+ # 将消息放入队列(阻塞等待,形成自然背压)
428
+ await self._message_queue.put(message)
224
429
  except asyncio.CancelledError:
225
430
  logger.info("消息处理任务已取消")
226
431
  raise
@@ -230,120 +435,80 @@ class MQTTClient:
230
435
  except Exception as e:
231
436
  logger.exception(f"消息循环异常: {e}")
232
437
 
233
- async def _handle_message(self, message: aiomqtt.Message):
234
- """处理单条消息
235
- Args:
236
- message: aiomqtt.Message 对象
438
+ async def _worker(self, worker_id: int):
439
+ """消息处理 Worker
237
440
 
238
- 消息处理流程:
239
- 1. 检查 payload 大小
240
- 2. 解码 UTF-8
241
- 3. 解析 JSON
242
- 4. 使用 protocol.parse_message() 验证和解析
243
- 5. 路由到对应处理器
244
- """
441
+ 从队列中取消息并处理,支持并发控制
245
442
 
246
- try:
247
- # 检查 payload 大小(防御 DoS)
248
- if len(message.payload) > self.config.max_payload_size:
249
- logger.warning(
250
- f"Payload 过大,已忽略 - "
251
- f"topic: {message.topic}, size: {len(message.payload)}"
252
- )
253
- return
443
+ Args:
444
+ worker_id: Worker ID(用于日志)
254
445
 
446
+ 设计要点:
447
+ - 并发上限可控(默认 16 个 worker)
448
+ - 异常隔离(worker 崩溃不影响其他 worker)
449
+ - 优雅退出(_running=False 时退出)
450
+ """
451
+ while self._running:
255
452
  try:
256
- text = message.payload.decode("utf-8")
257
- data = json.loads(text)
258
- except UnicodeDecodeError as e:
259
- logger.error(
260
- f"UTF-8 解码失败 - topic: {message.topic}, error: {e!r}, "
261
- f"preview: {message.payload[:100]}"
262
- )
263
- return
264
- except json.JSONDecodeError as e:
265
- logger.error(
266
- f"JSON 解析失败 - topic: {message.topic}, error: {e!r}, "
267
- f"preview: {text[:100]}"
268
- )
269
- return
270
-
271
- # 解析消息类型(使用 dataclass 替代 Box)
272
- msg = parse_message(data)
273
-
274
- # 路由到对应处理器
275
- topic_str = str(message.topic)
276
- handler = self._message_handlers.get(topic_str)
277
-
278
- if handler:
279
- # 调用处理器
280
- if asyncio.iscoroutinefunction(handler):
281
- await handler(topic_str, msg)
282
- else:
283
- handler(topic_str, msg)
284
- else:
285
- # 修复 P0-3:RPC 请求无处理器时,立即返回错误响应(防止调用方超时)
286
- from .protocol import RPCRequest, RPCResponse
287
- if isinstance(msg, RPCRequest):
288
- logger.warning(
289
- f"收到 RPC 请求但无处理器 - topic: {topic_str}, method: {msg.method}"
290
- )
291
- # 立即发送错误响应
292
- error_response = RPCResponse(
293
- request_id=msg.request_id,
294
- error=f"No RPC handler registered for topic: {topic_str}"
295
- )
296
- await self.publish(msg.reply_to, json.dumps(error_response.to_dict()), qos=1)
297
- else:
298
- logger.debug(f"收到消息(无处理器)- topic: {topic_str}")
299
-
300
- except MessageError as e:
301
- logger.error(f"消息解析失败: {e}")
302
- except Exception as e:
303
- logger.exception(f"消息处理失败: {e}")
304
-
305
- def subscribe(self, topic: str, handler: Optional[Callable] = None):
306
- """订阅主题
307
-
308
- 修复点:
309
- - ✅ P0-1: 未连接时不崩溃,队列化订阅
453
+ # 从队列取消息(带超时,避免无法退出) asyncio.Queue 是原子安全的
454
+ message = await asyncio.wait_for(self._message_queue.get(), timeout=1.0)
455
+ # 处理消息
456
+ await self._handle_message(message)
457
+ except asyncio.TimeoutError:
458
+ # 队列为空,继续等待
459
+ continue
460
+ except asyncio.CancelledError:
461
+ logger.debug(f"Worker {worker_id} 被取消")
462
+ break
463
+ except Exception as e:
464
+ logger.exception(f"Worker {worker_id} 异常: {e}")
465
+ # 继续运行,不退出
310
466
 
311
- Args:
312
- topic: MQTT 主题(支持通配符 +/#)
313
- handler: 消息处理器
314
- 签名:async def handler(topic: str, message: RPCRequest | RPCResponse)
315
- 或:def handler(topic: str, message: RPCRequest | RPCResponse)
467
+ @property
468
+ def raw(self) -> RawAPI:
469
+ """访问 Raw MQTT API (逃生通道)
316
470
 
317
- 设计决策:
318
- - 记录订阅到 _subscriptions set(用于重连恢复)
319
- - 如果已连接,立即订阅
320
- - 如果未连接,等待连接后自动订阅
471
+ Returns:
472
+ RawAPI 实例
321
473
 
322
474
  示例:
323
- # 同步处理器
324
- def my_handler(topic, message):
325
- print(f"收到消息: {topic} - {message}")
475
+ client.raw.subscribe("topic", handler)
476
+ await client.raw.publish("topic", b"data")
477
+ """
478
+ return self._raw_api
326
479
 
327
- client.subscribe("test/topic", my_handler)
480
+ async def _handle_message(self, message: aiomqtt.Message):
481
+ """处理单条消息(传输层,只处理 bytes)
328
482
 
329
- # 异步处理器
330
- async def my_async_handler(topic, message):
331
- await process_message(message)
483
+ 核心改变:
484
+ 1. 不再解析 JSON/协议
485
+ 2. 将 payload (bytes) 分发给 raw handlers
486
+ 3. 不再区分 RPC/Event(让上层处理)
332
487
 
333
- client.subscribe("test/topic", my_async_handler)
488
+ Args:
489
+ message: aiomqtt.Message 对象
334
490
  """
335
- # 记录订阅(用于重连恢复)
336
- self._subscriptions.add(topic)
491
+ topic_str = str(message.topic)
492
+ payload = message.payload # bytes 类型
337
493
 
338
- # 注册处理器
339
- if handler:
340
- self._message_handlers[topic] = handler
494
+ # 检查 payload 大小(防御 DoS)
495
+ if len(payload) > self.config.max_payload_size:
496
+ logger.warning(
497
+ f"Payload 过大,已忽略 - topic: {topic_str}, size: {len(payload)}"
498
+ )
499
+ return
341
500
 
342
- # 如果已连接,立即订阅
343
- if self._client:
344
- asyncio.create_task(self._do_subscribe(topic))
345
- else:
346
- logger.info(f"订阅已队列化(等待连接)- topic: {topic}")
501
+ # === 唯一职责:将 bytes 分发给所有匹配的 handlers ===
502
+ # MQTTMatcher.iter_match 返回值列表,不是 (pattern, handlers) 元组
503
+ for handlers in self._raw_matcher.iter_match(topic_str):
504
+ for handler in handlers:
505
+ try:
506
+ if asyncio.iscoroutinefunction(handler):
507
+ await handler(topic_str, payload) # 传递 bytes!
508
+ else:
509
+ handler(topic_str, payload)
510
+ except Exception as e:
511
+ logger.exception(f"Handler 异常 - topic: {topic_str}, error: {e}")
347
512
 
348
513
  async def _do_subscribe(self, topic: str):
349
514
  """执行订阅(内部方法)
@@ -372,37 +537,15 @@ class MQTTClient:
372
537
  if not self._subscriptions:
373
538
  return
374
539
 
375
- logger.info(f"恢复 {len(self._subscriptions)} 个订阅...")
540
+ # 创建快照,避免遍历时被修改(修复 P0-C)
541
+ topics = list(self._subscriptions)
542
+ logger.info(f"恢复 {len(topics)} 个订阅...")
376
543
 
377
- for topic in self._subscriptions:
544
+ for topic in topics:
378
545
  await self._do_subscribe(topic)
379
546
 
380
547
  logger.success("订阅恢复完成")
381
548
 
382
- async def publish(self, topic: str, payload: str, qos: int = 0):
383
- """发布消息
384
-
385
- 修复点:
386
- - ✅ P1-2: 未连接时记录警告
387
-
388
- Args:
389
- topic: 目标主题
390
- payload: 消息载荷(字符串)
391
- qos: QoS 等级(0/1/2)
392
-
393
- 异常:
394
- aiomqtt.MqttError: 发布失败
395
- """
396
- if not self._client:
397
- logger.warning(f"发布失败:客户端未连接 - topic: {topic}")
398
- return
399
-
400
- try:
401
- await self._client.publish(topic, payload, qos=qos)
402
- logger.debug(f"消息已发布 - topic: {topic}, qos: {qos}")
403
- except aiomqtt.MqttError as e:
404
- logger.error(f"发布失败 - topic: {topic}, error: {e}")
405
-
406
549
  def _create_tls_context(self) -> ssl.SSLContext:
407
550
  """创建 TLS 上下文
408
551
 
mqttxx/config.py CHANGED
@@ -119,6 +119,16 @@ class MQTTConfig:
119
119
  reconnect: 重连配置
120
120
  max_queued_messages: 最大排队消息数(0=无限)
121
121
  max_payload_size: 最大消息载荷大小(字节,防止 DoS 攻击)
122
+ message_queue_maxsize: 消息队列大小限制(默认 100,000)
123
+ - 语义:"几乎无限",仅作保险丝防止 OOM
124
+ - 队列满时行为:阻塞等待(背压信号)
125
+ - 触发背压时:CPU/延迟升高 → 扩容信号
126
+ - Python 字面量分隔符:100_000 = 100000(下划线仅用于可读性)
127
+ num_workers: Worker 数量(默认 None = CPU核数×2)
128
+ - None(默认):CPU核数 × 2(适合 IO-bound 负载)
129
+ - 自定义值:根据 handler 类型调整
130
+ - CPU-bound handler:设为 CPU核数
131
+ - IO-bound handler:设为 CPU核数 × 2~4
122
132
  log_level: 日志级别(DEBUG|INFO|WARNING|ERROR)
123
133
 
124
134
  示例:
@@ -165,6 +175,10 @@ class MQTTConfig:
165
175
  max_queued_messages: int = 0 # 0 = 无限
166
176
  max_payload_size: int = 1024 * 1024 # 1MB
167
177
 
178
+ # 消息处理(修复 P0-A)
179
+ message_queue_maxsize: int = 100_000 # 10W 条 队列最大容量(保险丝)
180
+ num_workers: Optional[int] = None # Worker 数量(None = CPU核数×2)
181
+
168
182
  # 日志级别
169
183
  log_level: str = "INFO"
170
184
 
mqttxx/conventions.py CHANGED
@@ -1,11 +1,12 @@
1
1
  # 约定式 RPC 管理器 - 去角色设计
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any, Optional, Type
4
4
  from loguru import logger
5
5
 
6
6
  from .client import MQTTClient
7
7
  from .config import RPCConfig
8
8
  from .rpc import RPCManager, AuthCallback
9
+ from .protocol import Codec, JSONCodec
9
10
 
10
11
 
11
12
  class ConventionalRPCManager(RPCManager):
@@ -50,6 +51,7 @@ class ConventionalRPCManager(RPCManager):
50
51
  my_topic: str,
51
52
  config: Optional[RPCConfig] = None,
52
53
  auth_callback: Optional[AuthCallback] = None,
54
+ codec: Type[Codec] = JSONCodec, # 新增
53
55
  ):
54
56
  """初始化约定式 RPC 管理器
55
57
 
@@ -58,9 +60,10 @@ class ConventionalRPCManager(RPCManager):
58
60
  my_topic: 本节点的 topic(自动订阅,自动注入到 reply_to)
59
61
  config: RPC 配置(可选)
60
62
  auth_callback: 权限检查回调(可选)
63
+ codec: 编解码器(默认 JSONCodec)
61
64
 
62
65
  自动行为:
63
- - 自动订阅 my_topic
66
+ - 自动订阅 my_topic(接收请求和响应)
64
67
  - 自动绑定消息处理器
65
68
 
66
69
  示例:
@@ -76,12 +79,12 @@ class ConventionalRPCManager(RPCManager):
76
79
  # 多层级
77
80
  rpc = ConventionalRPCManager(client, my_topic="region/zone/device")
78
81
  """
79
- super().__init__(client, config, auth_callback)
82
+ super().__init__(client, config, auth_callback, codec)
80
83
 
81
84
  self._my_topic = my_topic
82
85
 
83
- # 自动订阅
84
- client.subscribe(my_topic, self.handle_rpc_message)
86
+ # 自动设置(订阅响应主题)
87
+ self.setup(my_topic)
85
88
 
86
89
  logger.info(f"ConventionalRPCManager 已初始化 - my_topic: {my_topic}")
87
90