mqttxx 2.0.3__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.
@@ -0,0 +1,910 @@
1
+ Metadata-Version: 2.1
2
+ Name: mqttxx
3
+ Version: 3.1.2
4
+ Summary: 基于 aiomqtt 的高级 MQTT 客户端和 RPC 框架
5
+ Author: MQTTX Team
6
+ License: MIT
7
+ Keywords: mqtt,rpc,async,iot,messaging
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: aiomqtt <3.0.0,>=2.0.0
11
+ Requires-Dist: loguru >=0.7.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest ; extra == 'dev'
14
+ Requires-Dist: pytest-asyncio ; extra == 'dev'
15
+ Requires-Dist: ruff ; extra == 'dev'
16
+ Requires-Dist: build ; extra == 'dev'
17
+ Requires-Dist: twine ; extra == 'dev'
18
+
19
+ # MQTTX
20
+
21
+ [![PyPI version](https://img.shields.io/badge/version-3.0.0-blue.svg)](https://pypi.org/project/mqttxx/)
22
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
23
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
24
+
25
+ 基于 [aiomqtt](https://github.com/sbtinstruments/aiomqtt) 的高级 MQTT 客户端和 RPC 框架。
26
+
27
+ ## 核心特性
28
+
29
+ - ✅ **纯 async/await** - 无回调,代码清晰
30
+ - ✅ **高性能消息处理** - Queue + Worker 模式,可控并发,背压机制
31
+ - ✅ **自动重连** - 订阅队列化,断线重连
32
+ - ✅ **双向对等 RPC** - 带权限控制、超时管理
33
+ - ✅ **约定式 RPC** - 零配置,自动订阅、自动注入 reply_to
34
+ - ✅ **Event Channel** - 高吞吐事件广播,支持通配符订阅
35
+ - ✅ **TLS/SSL 支持** - 安全连接、双向认证
36
+ - ✅ **完善异常系统** - 统一错误码、清晰的异常层次
37
+ - ✅ **可插拔编解码器** - 支持 JSON/MessagePack/Protobuf 等自定义协议
38
+ - ✅ **生产级可靠性** - 修复所有 P0 缺陷(任务泄漏、资源泄漏、并发竞态)
39
+
40
+ ---
41
+
42
+ ## 目录
43
+
44
+ - [架构原则](#架构原则)
45
+ - [安装](#安装)
46
+ - [快速开始](#快速开始)
47
+ - [MQTT 基础用法](#1-mqtt-基础用法)
48
+ - [约定式 RPC(推荐)](#2-约定式-rpc零配置)
49
+ - [Event Channel(事件广播)](#3-event-channel事件广播)
50
+ - [传统 RPC](#4-rpc-基础用法传统模式)
51
+ - [RPC 权限控制](#5-rpc-权限控制)
52
+ - [TLS/SSL 和认证](#6-tlsssl-和认证)
53
+ - [API 速查](#api-速查)
54
+ - [配置对象](#配置对象)
55
+ - [异常系统](#异常系统)
56
+ - [RPC 消息协议](#rpc-消息协议)
57
+ - [版本变更](#v200-重大变更)
58
+ - [开发](#开发)
59
+ - [示例项目](#示例项目)
60
+ - [常见问题](#常见问题)
61
+ - [贡献](#贡献)
62
+
63
+ ---
64
+
65
+ ## 架构原则
66
+
67
+ MQTTX 遵循严格的分层架构,确保职责清晰、代码简洁、易于扩展:
68
+
69
+ ### 1. MQTTClient(传输层)
70
+ - **职责**:只负责 bytes 传输
71
+ - **约束**:
72
+ - 不导入 `json` 或 `protocol` 模块
73
+ - 不理解消息内容
74
+ - `publish()` 只接受 `bytes`
75
+ - `subscribe_raw()` handler 接收 `bytes`
76
+
77
+ ### 2. Protocol(协议层)
78
+ - **职责**:定义消息格式和编解码
79
+ - **核心**:
80
+ - `encode()` 方法:object → bytes
81
+ - `decode()` 方法:bytes → object
82
+ - JSON 只在这里出现
83
+
84
+ ### 3. RPCManager/EventChannelManager(应用层)
85
+ - **职责**:业务逻辑处理
86
+ - **约束**:
87
+ - 内部永远使用类型化对象(RPCRequest/RPCResponse)
88
+ - 调用 `encode()`/`decode()` 处理编解码
89
+ - 不直接使用 `json.dumps()`/`json.loads()`
90
+
91
+ ### 可插拔编解码器
92
+
93
+ 支持自定义编解码器(例如 MessagePack):
94
+
95
+ ```python
96
+ import msgpack
97
+ from mqttxx.protocol import Codec
98
+
99
+ class MessagePackCodec:
100
+ @staticmethod
101
+ def encode(obj) -> bytes:
102
+ if hasattr(obj, 'to_dict'):
103
+ data = obj.to_dict()
104
+ else:
105
+ data = obj
106
+ return msgpack.packb(data)
107
+
108
+ @staticmethod
109
+ def decode(data: bytes) -> dict:
110
+ return msgpack.unpackb(data, raw=False)
111
+
112
+ # 使用
113
+ from mqttxx import RPCManager
114
+ rpc = RPCManager(client, codec=MessagePackCodec)
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 安装
120
+
121
+ ```bash
122
+ pip install mqttxx
123
+ ```
124
+
125
+ **要求**:
126
+ - Python >= 3.10
127
+ - aiomqtt >= 2.0.0
128
+ - loguru >= 0.7.0
129
+
130
+ ---
131
+
132
+ ## 快速开始
133
+
134
+ ### 1. MQTT 基础用法
135
+
136
+ ```python
137
+ import asyncio
138
+ from mqttxx import MQTTClient, MQTTConfig
139
+
140
+ async def main():
141
+ config = MQTTConfig(
142
+ broker_host="localhost",
143
+ broker_port=1883,
144
+ client_id="device_123"
145
+ )
146
+
147
+ async with MQTTClient(config) as client:
148
+ # 订阅主题
149
+ def on_message(topic, message):
150
+ print(f"{topic}: {message}")
151
+
152
+ client.subscribe("sensors/#", on_message)
153
+
154
+ # 发布消息
155
+ await client.publish("sensors/temperature", "25.5", qos=1)
156
+
157
+ await asyncio.sleep(60)
158
+
159
+ asyncio.run(main())
160
+ ```
161
+
162
+ ---
163
+
164
+ ### 2. 约定式 RPC(零配置)
165
+
166
+ **推荐**:使用 `ConventionalRPCManager`,自动订阅 + 自动注入 reply_to。
167
+
168
+ ```python
169
+ from mqttxx import MQTTClient, MQTTConfig, ConventionalRPCManager
170
+
171
+ # 边缘设备
172
+ async def edge_device():
173
+ client_id = "device_123"
174
+ config = MQTTConfig(
175
+ broker_host="localhost",
176
+ client_id=client_id,
177
+ )
178
+
179
+ async with MQTTClient(config) as client:
180
+ # 自动订阅 edge/device_123
181
+ rpc = ConventionalRPCManager(client, my_topic=f"edge/{client_id}")
182
+
183
+ @rpc.register("get_status")
184
+ async def get_status(params):
185
+ return {"status": "online"}
186
+
187
+ # 调用云端(自动注入 reply_to="edge/device_123")
188
+ config = await rpc.call("cloud/config-service", "get_device_config")
189
+ print(config)
190
+
191
+ await asyncio.sleep(60)
192
+
193
+ # 云端服务
194
+ async def cloud_service():
195
+ client_id = "config-service"
196
+ config = MQTTConfig(
197
+ broker_host="localhost",
198
+ client_id=client_id,
199
+ )
200
+
201
+ async with MQTTClient(config) as client:
202
+ # 自动订阅 cloud/config-service
203
+ rpc = ConventionalRPCManager(client, my_topic=f"cloud/{client_id}")
204
+
205
+ @rpc.register("get_device_config")
206
+ async def get_device_config(params):
207
+ return {"update_interval": 60, "servers": ["s1", "s2"]}
208
+
209
+ # 调用边缘设备(自动注入 reply_to="cloud/config-service")
210
+ status = await rpc.call("edge/device_123", "execute_command", params={"cmd": "restart"})
211
+ print(status)
212
+
213
+ await asyncio.sleep(60)
214
+
215
+ # 运行边缘设备或云端
216
+ asyncio.run(edge_device()) # 或 asyncio.run(cloud_service())
217
+ ```
218
+
219
+ **对比传统 RPC:**
220
+
221
+ | 场景 | 传统 RPC | 约定式 RPC |
222
+ |-----|---------|-----------|
223
+ | 初始化 | `rpc = RPCManager(client)`<br>`client.subscribe("edge/123", rpc.handle_rpc_message)` | `rpc = ConventionalRPCManager(client, my_topic="edge/123")`<br>→ 自动订阅 |
224
+ | 调用 | `await rpc.call(topic="cloud/svc", method="get", reply_to="edge/123")` | `await rpc.call("cloud/svc", "get")` |
225
+ | 代码量 | 100% | **60%** ↓ |
226
+
227
+ ---
228
+
229
+ ### 3. Event Channel(事件广播)
230
+
231
+ **Event Channel 是高吞吐、低耦合、无返回值的事件广播通道**,适用于:
232
+ - 传感器数据流(温度、湿度、位置)
233
+ - 系统监控指标(CPU、内存、网络)
234
+ - 设备状态心跳
235
+ - 日志流
236
+
237
+ **关键特性**:
238
+ - ✅ **单向广播** - 发布即忘,无返回值(与 RPC 形成对比)
239
+ - ✅ **通配符订阅** - 支持 MQTT 通配符(`+` 单级,`#` 多级)
240
+ - ✅ **混合模式** - 支持结构化事件(EventMessage)和原始 dict
241
+ - ✅ **装饰器订阅** - 简洁的 API
242
+
243
+ ```python
244
+ from mqttxx import MQTTClient, MQTTConfig, EventChannelManager, EventMessage
245
+
246
+ async def main():
247
+ config = MQTTConfig(broker_host="localhost", client_id="device_001")
248
+
249
+ async with MQTTClient(config) as client:
250
+ events = EventChannelManager(client)
251
+
252
+ # 订阅事件(支持通配符)
253
+ @events.subscribe("sensors/+/temperature")
254
+ async def on_temperature(topic, message):
255
+ print(f"[温度] {topic}: {message}")
256
+
257
+ @events.subscribe("sensors/#")
258
+ async def on_all_sensors(topic, message):
259
+ print(f"[所有传感器] {topic}")
260
+
261
+ # 发布结构化事件
262
+ await events.publish(
263
+ "sensors/room1/temperature",
264
+ EventMessage(
265
+ event_type="temperature.changed",
266
+ data={"value": 25.5, "unit": "C"},
267
+ source="sensor_001"
268
+ )
269
+ )
270
+
271
+ # 发布原始消息(零开销)
272
+ await events.publish(
273
+ "sensors/room1/humidity",
274
+ {"value": 60.2, "unit": "%"}
275
+ )
276
+
277
+ await asyncio.sleep(60)
278
+
279
+ asyncio.run(main())
280
+ ```
281
+
282
+ **Event Channel vs RPC**:
283
+
284
+ | 特性 | Event Channel | RPC |
285
+ |------|--------------|-----|
286
+ | 模式 | 发布-订阅(Pub-Sub) | 请求-响应(Request-Response) |
287
+ | 通信 | 单向,一对多广播 | 双向,点对点 |
288
+ | 返回值 | ❌ 无返回值 | ✅ 等待返回结果 |
289
+ | 用途 | 高频事件流、监控数据 | 远程方法调用、配置查询 |
290
+ | 通配符 | ✅ 支持 `+/#` | ❌ 精确匹配 |
291
+
292
+ **混合使用 RPC + Event**:
293
+
294
+ ```python
295
+ # 同时使用 RPC 和 Event Channel
296
+ rpc = ConventionalRPCManager(client, my_topic="device/device_001")
297
+ events = EventChannelManager(client)
298
+
299
+ # RPC: 获取配置(需要返回值)
300
+ config = await rpc.call("server/config", "get_device_config")
301
+
302
+ # Event: 发布心跳(无返回值)
303
+ await events.publish(
304
+ "device/device_001/heartbeat",
305
+ EventMessage(
306
+ event_type="heartbeat",
307
+ data={"cpu": 45.2, "memory": 78.5}
308
+ )
309
+ )
310
+ ```
311
+
312
+ ---
313
+
314
+ ### 4. RPC 基础用法(传统模式)
315
+
316
+ 需要手动订阅和传递 `reply_to`,适用于需要精细控制的场景。
317
+
318
+ ```python
319
+ from mqttxx import MQTTClient, MQTTConfig, RPCManager
320
+
321
+ async def main():
322
+ config = MQTTConfig(broker_host="localhost", client_id="device_001")
323
+
324
+ async with MQTTClient(config) as client:
325
+ rpc = RPCManager(client)
326
+
327
+ # 注册本地方法
328
+ @rpc.register("get_status")
329
+ async def get_status(params):
330
+ return {"status": "online", "cpu": 45.2}
331
+
332
+ # 订阅 RPC 主题
333
+ client.subscribe(
334
+ "server/device_001",
335
+ rpc.handle_rpc_message
336
+ )
337
+
338
+ # 调用远程方法
339
+ result = await rpc.call(
340
+ topic="bots/device_002",
341
+ method="get_data",
342
+ reply_to="server/device_001",
343
+ timeout=5
344
+ )
345
+ print(result) # {"data": [1, 2, 3]}
346
+
347
+ await asyncio.sleep(60)
348
+
349
+ asyncio.run(main())
350
+ ```
351
+
352
+ ---
353
+
354
+ ### 5. RPC 权限控制
355
+
356
+ ```python
357
+ from mqttxx import RPCManager, RPCRequest
358
+
359
+ async def auth_check(caller_id: str, method: str, request: RPCRequest) -> bool:
360
+ # 敏感方法只允许管理员
361
+ if method in ["delete_user", "reset_system"]:
362
+ return caller_id in ["admin_001", "admin_002"]
363
+ return True
364
+
365
+ rpc = RPCManager(client, auth_callback=auth_check)
366
+
367
+ @rpc.register("delete_user")
368
+ async def delete_user(params):
369
+ return {"result": "user deleted"}
370
+
371
+ # 未授权调用会返回 "Permission denied"
372
+ ```
373
+
374
+ ---
375
+
376
+ ### 6. TLS/SSL 和认证
377
+
378
+ ```python
379
+ from mqttxx import MQTTConfig, TLSConfig, AuthConfig
380
+ from pathlib import Path
381
+
382
+ config = MQTTConfig(
383
+ broker_host="secure.mqtt.example.com",
384
+ broker_port=8883,
385
+ tls=TLSConfig(
386
+ enabled=True,
387
+ ca_certs=Path("ca.crt"),
388
+ certfile=Path("client.crt"),
389
+ keyfile=Path("client.key"),
390
+ ),
391
+ auth=AuthConfig(
392
+ username="mqtt_user",
393
+ password="mqtt_password",
394
+ ),
395
+ )
396
+
397
+ async with MQTTClient(config) as client:
398
+ await client.publish("secure/topic", "encrypted message")
399
+ ```
400
+
401
+ ---
402
+
403
+ ## API 速查
404
+
405
+ ### MQTTClient
406
+
407
+ ```python
408
+ class MQTTClient:
409
+ def __init__(self, config: MQTTConfig)
410
+ async def connect(self) -> None
411
+ async def disconnect(self) -> None
412
+ def subscribe(self, topic: str, handler: Callable) -> None
413
+ async def publish(self, topic: str, payload: str, qos: int = 0) -> None
414
+
415
+ @property
416
+ def is_connected(self) -> bool
417
+ ```
418
+
419
+ ---
420
+
421
+ ### RPCManager(传统 RPC)
422
+
423
+ ```python
424
+ class RPCManager:
425
+ def __init__(self, client: MQTTClient, config: RPCConfig = None, auth_callback: AuthCallback = None)
426
+
427
+ def register(self, method_name: str) # 装饰器
428
+ def unregister(self, method_name: str) -> None
429
+ def handle_rpc_message(self, topic: str, message: RPCRequest | RPCResponse) -> None
430
+
431
+ async def call(
432
+ self,
433
+ topic: str,
434
+ method: str,
435
+ params: Any = None,
436
+ reply_to: str = None, # 必填
437
+ timeout: float = None,
438
+ ) -> Any
439
+ ```
440
+
441
+ ---
442
+
443
+ ### ConventionalRPCManager(约定式 RPC)
444
+
445
+ ```python
446
+ class ConventionalRPCManager(RPCManager):
447
+ def __init__(
448
+ self,
449
+ client: MQTTClient,
450
+ my_topic: str, # 本节点 topic(自动订阅,自动注入到 reply_to)
451
+ config: RPCConfig = None,
452
+ auth_callback: AuthCallback = None,
453
+ )
454
+
455
+ async def call(
456
+ self,
457
+ topic: str, # 对方的 topic
458
+ method: str,
459
+ params: Any = None,
460
+ timeout: float = None,
461
+ reply_to: str = None, # 可选,默认使用 my_topic
462
+ ) -> Any
463
+
464
+ # 属性
465
+ my_topic: str # 当前 topic(只读)
466
+ ```
467
+
468
+ **使用示例:**
469
+
470
+ ```python
471
+ # 边缘设备
472
+ rpc = ConventionalRPCManager(client, my_topic="edge/device_123")
473
+ config = await rpc.call("cloud/config-service", "get_config")
474
+
475
+ # 云端服务
476
+ rpc = ConventionalRPCManager(client, my_topic="cloud/config-service")
477
+ status = await rpc.call("edge/device_123", "execute_command")
478
+
479
+ # 微服务
480
+ rpc = ConventionalRPCManager(client, my_topic="auth-service")
481
+ user = await rpc.call("user-service", "get_user", params={"id": 123})
482
+ ```
483
+
484
+ ---
485
+
486
+ ### EventChannelManager(Event Channel)
487
+
488
+ ```python
489
+ class EventChannelManager:
490
+ def __init__(self, client: MQTTClient)
491
+
492
+ def subscribe(
493
+ self,
494
+ pattern: str, # MQTT topic 模式(支持通配符 +/#)
495
+ handler: Optional[EventHandler] = None
496
+ ) -> Callable # 装饰器或直接注册
497
+
498
+ def unsubscribe(self, pattern: str, handler: EventHandler) -> None
499
+
500
+ async def publish(
501
+ self,
502
+ topic: str, # MQTT topic
503
+ message: EventMessage | dict | Any, # 消息(支持多种格式)
504
+ qos: int = 0
505
+ ) -> None # 无返回值(单向广播)
506
+ ```
507
+
508
+ **EventMessage(可选的结构化格式)**:
509
+
510
+ ```python
511
+ @dataclass
512
+ class EventMessage:
513
+ type: str = "event"
514
+ event_type: str # 事件类型(如 "temperature.changed")
515
+ data: Any # 事件数据
516
+ timestamp: float # 时间戳(自动生成)
517
+ source: str = "" # 事件源
518
+
519
+ def to_dict(self) -> dict
520
+ @staticmethod
521
+ def from_dict(data: dict) -> EventMessage
522
+ ```
523
+
524
+ **使用示例:**
525
+
526
+ ```python
527
+ events = EventChannelManager(client)
528
+
529
+ # 订阅(装饰器模式)
530
+ @events.subscribe("sensors/+/temperature")
531
+ async def on_temp(topic, message):
532
+ print(f"{topic}: {message}")
533
+
534
+ # 发布结构化事件
535
+ await events.publish(
536
+ "sensors/room1/temperature",
537
+ EventMessage(
538
+ event_type="temperature.changed",
539
+ data={"value": 25.5}
540
+ )
541
+ )
542
+
543
+ # 发布原始消息(零开销)
544
+ await events.publish("sensors/room1/humidity", {"value": 60.2})
545
+ ```
546
+
547
+ ---
548
+
549
+ ## 配置对象
550
+
551
+ ### MQTTConfig
552
+
553
+ ```python
554
+ @dataclass
555
+ class MQTTConfig:
556
+ broker_host: str
557
+ broker_port: int = 1883
558
+ client_id: str = "" # 空字符串 = 自动生成
559
+ keepalive: int = 60
560
+ clean_session: bool = False
561
+ tls: TLSConfig = field(default_factory=TLSConfig)
562
+ auth: AuthConfig = field(default_factory=AuthConfig)
563
+ reconnect: ReconnectConfig = field(default_factory=ReconnectConfig)
564
+ max_queued_messages: int = 0 # 0 = 无限
565
+ max_payload_size: int = 1024 * 1024 # 1MB
566
+
567
+ # 高性能消息处理(v3.0+)
568
+ message_queue_maxsize: int = 100_000 # 消息队列大小(保险丝,防止 OOM)
569
+ num_workers: Optional[int] = None # Worker 数量(None = CPU核数×2,适合 IO-bound)
570
+ ```
571
+
572
+ **高性能配置说明**:
573
+
574
+ - **message_queue_maxsize**:消息队列容量限制
575
+ - 默认 100,000("几乎无限",仅作保险丝)
576
+ - 队列满时阻塞等待(背压信号)
577
+ - 触发背压时:CPU/延迟升高 → 扩容信号
578
+ - Python 字面量分隔符:`100_000 = 100000`(下划线仅用于可读性)
579
+
580
+ - **num_workers**:消息处理 Worker 数量
581
+ - `None`(默认):CPU核数 × 2(适合 IO-bound 负载)
582
+ - 自定义值:根据 handler 类型调整
583
+ - CPU-bound handler:设为 CPU核数
584
+ - IO-bound handler:设为 CPU核数 × 2~4
585
+
586
+ ### TLSConfig
587
+
588
+ ```python
589
+ @dataclass
590
+ class TLSConfig:
591
+ enabled: bool = False
592
+ ca_certs: Optional[Path] = None
593
+ certfile: Optional[Path] = None
594
+ keyfile: Optional[Path] = None
595
+ verify_mode: str = "CERT_REQUIRED" # CERT_REQUIRED | CERT_OPTIONAL | CERT_NONE
596
+ check_hostname: bool = True
597
+ ```
598
+
599
+ ### AuthConfig
600
+
601
+ ```python
602
+ @dataclass
603
+ class AuthConfig:
604
+ username: Optional[str] = None
605
+ password: Optional[str] = None
606
+ ```
607
+
608
+ ### ReconnectConfig
609
+
610
+ ```python
611
+ @dataclass
612
+ class ReconnectConfig:
613
+ enabled: bool = True
614
+ interval: int = 5 # 初始重连间隔(秒)
615
+ max_attempts: int = 0 # 0 = 无限重试
616
+ backoff_multiplier: float = 1.5 # 指数退避倍数
617
+ max_interval: int = 60 # 最大重连间隔(秒)
618
+ ```
619
+
620
+ ### RPCConfig
621
+
622
+ ```python
623
+ @dataclass
624
+ class RPCConfig:
625
+ default_timeout: float = 30.0 # 默认超时时间(秒)
626
+ max_concurrent_calls: int = 100 # 最大并发调用数
627
+ ```
628
+
629
+ ---
630
+
631
+ ## 异常系统
632
+
633
+ ```python
634
+ # 基础异常
635
+ class MQTTXError(Exception)
636
+ class ConnectionError(MQTTXError)
637
+ class MessageError(MQTTXError)
638
+ class RPCError(MQTTXError)
639
+
640
+ # RPC 异常
641
+ class RPCTimeoutError(RPCError) # RPC 调用超时
642
+ class RPCRemoteError(RPCError) # 远程方法执行失败
643
+ class RPCMethodNotFoundError(RPCError) # 方法未找到
644
+ class PermissionDeniedError(RPCError) # 权限拒绝
645
+ class TooManyConcurrentCallsError(RPCError) # 并发调用超限
646
+
647
+ # 错误码
648
+ class ErrorCode(IntEnum):
649
+ NOT_CONNECTED = 1001
650
+ RPC_TIMEOUT = 3002
651
+ PERMISSION_DENIED = 4001
652
+ # ... 更多错误码见源码
653
+ ```
654
+
655
+ **使用示例:**
656
+
657
+ ```python
658
+ from mqttxx import RPCTimeoutError, RPCRemoteError
659
+
660
+ try:
661
+ result = await rpc.call_bot("456", "get_data", timeout=5)
662
+ except RPCTimeoutError:
663
+ print("调用超时")
664
+ except RPCRemoteError as e:
665
+ print(f"远程方法执行失败: {e}")
666
+ ```
667
+
668
+ ---
669
+
670
+ ## RPC 消息协议
671
+
672
+ ### 请求
673
+
674
+ ```json
675
+ {
676
+ "type": "rpc_request",
677
+ "request_id": "uuid-string",
678
+ "method": "get_status",
679
+ "params": {"id": 123},
680
+ "reply_to": "server/device_001",
681
+ "caller_id": "device_002"
682
+ }
683
+ ```
684
+
685
+ ### 响应(成功)
686
+
687
+ ```json
688
+ {
689
+ "type": "rpc_response",
690
+ "request_id": "uuid-string",
691
+ "result": {"status": "online"}
692
+ }
693
+ ```
694
+
695
+ ### 响应(错误)
696
+
697
+ ```json
698
+ {
699
+ "type": "rpc_response",
700
+ "request_id": "uuid-string",
701
+ "error": "Permission denied"
702
+ }
703
+ ```
704
+
705
+ ---
706
+
707
+ ## v2.0.0 重大变更
708
+
709
+ 从 v2.0.0 开始,完全重写为基于 aiomqtt(纯 async/await),**不兼容** v0.x.x(gmqtt)。
710
+
711
+ **主要变化:**
712
+ - ✅ aiomqtt 替代 gmqtt
713
+ - ✅ 原生 dataclass 替代 python-box(性能提升 6 倍)
714
+ - ✅ **修复所有 P0 缺陷**(详见下方)
715
+ - ✅ 新增约定式 RPC(`ConventionalRPCManager`)
716
+ - ✅ 新增权限控制(`auth_callback`)
717
+ - ✅ 新增 TLS/SSL 支持
718
+
719
+ **P0 缺陷修复(v3.0)**:
720
+ 1. **任务泄漏 + 消息处理模型** - Queue + Worker 模式,可控并发
721
+ 2. **RawAPI.unsubscribe 缺失** - 实现完整的取消订阅功能
722
+ 3. **重连竞态条件** - 快照模式避免并发修改
723
+ 4. **连接资源泄漏** - 显式关闭连接
724
+
725
+ **并发模型说明**:
726
+ - ✅ **单 Event Loop 设计**:所有方法(subscribe/unsubscribe/handler)必须在同一 asyncio loop 中调用
727
+ - ✅ **Handler 顺序执行**:同一消息的多个 handlers 按注册顺序顺序调用(非并发)
728
+ - ❌ **不支持多线程/多 loop 并发**:如需多线程,请为每个线程创建独立的 MQTTClient 实例
729
+
730
+ **迁移关键点:**
731
+ 1. 使用 `MQTTConfig` 配置对象
732
+ 2. 使用 `async with` 上下文管理器
733
+ 3. `publish_message()` → `publish()`
734
+ 4. 移除 `EventEmitter`(改用 dict)
735
+ 5. (可选)配置 `message_queue_maxsize` 和 `num_workers` 以优化性能
736
+
737
+ ---
738
+
739
+ ## 开发
740
+
741
+ ```bash
742
+ # 克隆项目
743
+ git clone <repository-url>
744
+ cd mqttx
745
+
746
+ # 安装开发依赖
747
+ pip install -e ".[dev]"
748
+
749
+ # 运行测试
750
+ pytest tests/ -v
751
+
752
+ # 代码检查和格式化
753
+ make lint # 代码检查
754
+ make format # 代码格式化
755
+
756
+ # 构建和发布
757
+ make build # 构建分发包
758
+ make version # 查看当前版本
759
+ ```
760
+
761
+ **测试覆盖**:
762
+ - 单元测试 (`test_*`)
763
+ - 集成测试 (`test_integration`)
764
+ - P0 缺陷修复验证 (`test_p0_fixes`)
765
+ - 性能测试 (`test_performance`)
766
+
767
+ ---
768
+
769
+ ## 示例项目
770
+
771
+ 查看 [examples/](examples/) 目录获取完整示例:
772
+
773
+ - **conventional_rpc_generic.py** - 约定式 RPC 完整示例
774
+ - 边缘设备与云端服务通信
775
+ - 微服务之间的 RPC 调用
776
+ - 展示自动订阅和自动注入 reply_to
777
+
778
+ - **event_channel_basic.py** - Event Channel 基础示例
779
+ - 订阅事件(支持通配符)
780
+ - 发布结构化事件和原始消息
781
+ - 一个 topic 多个订阅者
782
+
783
+ - **rpc_event_mixed.py** - RPC 和 Event Channel 混合使用
784
+ - 同一客户端同时使用 RPC 和 Event Channel
785
+ - RPC 调用(请求-响应)
786
+ - Event 广播(单向,无返回值)
787
+
788
+ 运行示例:
789
+ ```bash
790
+ # RPC 示例
791
+ # 终端 1: 运行边缘设备
792
+ python examples/conventional_rpc_generic.py edge
793
+
794
+ # 终端 2: 运行云端服务
795
+ python examples/conventional_rpc_generic.py cloud
796
+
797
+ # Event Channel 示例
798
+ python examples/event_channel_basic.py
799
+
800
+ # RPC + Event 混合使用
801
+ # 终端 1: 运行设备端
802
+ python examples/rpc_event_mixed.py device
803
+
804
+ # 终端 2: 运行服务器端
805
+ python examples/rpc_event_mixed.py server
806
+ ```
807
+
808
+ ---
809
+
810
+ ## 常见问题
811
+
812
+ ### Q: 如何处理断线重连?
813
+ A: MQTTClient 默认启用自动重连,配置参数在 `ReconnectConfig` 中:
814
+ ```python
815
+ config = MQTTConfig(
816
+ broker_host="localhost",
817
+ reconnect=ReconnectConfig(
818
+ enabled=True,
819
+ interval=5, # 初始重连间隔
820
+ max_attempts=0, # 0 = 无限重试
821
+ backoff_multiplier=1.5,
822
+ max_interval=60
823
+ )
824
+ )
825
+ ```
826
+
827
+ ### Q: RPC 调用超时了怎么办?
828
+ A: 捕获 `RPCTimeoutError` 异常并处理:
829
+ ```python
830
+ from mqttxx import RPCTimeoutError
831
+
832
+ try:
833
+ result = await rpc.call("topic", "method", timeout=5)
834
+ except RPCTimeoutError:
835
+ print("RPC 调用超时,请检查远程服务是否在线")
836
+ ```
837
+
838
+ ### Q: 如何实现双向认证(mTLS)?
839
+ A: 配置 TLS 证书和密钥:
840
+ ```python
841
+ config = MQTTConfig(
842
+ broker_host="secure.mqtt.example.com",
843
+ broker_port=8883,
844
+ tls=TLSConfig(
845
+ enabled=True,
846
+ ca_certs=Path("ca.crt"), # CA 证书
847
+ certfile=Path("client.crt"), # 客户端证书
848
+ keyfile=Path("client.key"), # 客户端私钥
849
+ )
850
+ )
851
+ ```
852
+
853
+ ### Q: 约定式 RPC 和传统 RPC 有什么区别?
854
+ A:
855
+ - **约定式 RPC**: 自动订阅 `my_topic`,自动注入 `reply_to`,代码量减少 40%
856
+ - **传统 RPC**: 需要手动订阅、手动传递 `reply_to`,适合需要精细控制的场景
857
+
858
+ 推荐大多数场景使用约定式 RPC。
859
+
860
+ ### Q: 如何优化高吞吐场景的性能?
861
+ A: 根据实际负载调整消息处理配置:
862
+ ```python
863
+ import os
864
+
865
+ config = MQTTConfig(
866
+ broker_host="localhost",
867
+ # 队列容量(默认 100k 适合大多数场景)
868
+ message_queue_maxsize=100_000,
869
+
870
+ # Worker 数量(根据 handler 类型调整)
871
+ num_workers=os.cpu_count() * 2, # IO-bound(默认)
872
+ # num_workers=os.cpu_count(), # CPU-bound
873
+ )
874
+ ```
875
+
876
+ **性能监控**:
877
+ - 监控队列深度:如果队列长期接近满载,考虑增加 workers
878
+ - 监控 CPU/延迟:队列满时会触发背压,CPU/延迟会升高
879
+ - Handler 并发:默认顺序执行,如需并发请在 handler 内使用 `asyncio.create_task()`
880
+
881
+ ### Q: Handlers 是并发执行的吗?
882
+ A: 不是。同一消息的多个 handlers 按注册顺序**顺序执行**(非并发):
883
+ ```python
884
+ # 顺序处理(默认)
885
+ async def handler1(topic, payload):
886
+ await process_data(payload) # 阻塞后续 handlers
887
+
888
+ # 并发处理(手动)
889
+ async def handler2(topic, payload):
890
+ asyncio.create_task(process_async(payload)) # 不阻塞
891
+ ```
892
+
893
+ 这是有意的设计,确保消息处理的可预测性。如需并发,请在 handler 内部创建 task。
894
+
895
+ ---
896
+
897
+ ## 贡献
898
+
899
+ 欢迎提交 Issue 和 Pull Request!
900
+
901
+ 在提交 PR 之前,请确保:
902
+ 1. 通过所有测试 (`pytest tests/ -v`)
903
+ 2. 代码通过检查 (`make lint`)
904
+ 3. 代码已格式化 (`make format`)
905
+
906
+ ---
907
+
908
+ ## License
909
+
910
+ MIT