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.
- mqttxx/__init__.py +13 -5
- mqttxx/client.py +295 -150
- mqttxx/config.py +14 -0
- mqttxx/conventions.py +8 -5
- mqttxx/events.py +340 -0
- mqttxx/protocol.py +189 -14
- mqttxx/rpc.py +46 -6
- mqttxx-3.1.2.dist-info/METADATA +910 -0
- mqttxx-3.1.2.dist-info/RECORD +13 -0
- mqttxx-2.0.3.dist-info/METADATA +0 -490
- mqttxx-2.0.3.dist-info/RECORD +0 -12
- {mqttxx-2.0.3.dist-info → mqttxx-3.1.2.dist-info}/LICENSE +0 -0
- {mqttxx-2.0.3.dist-info → mqttxx-3.1.2.dist-info}/WHEEL +0 -0
- {mqttxx-2.0.3.dist-info → mqttxx-3.1.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
[](https://pypi.org/project/mqttxx/)
|
|
22
|
+
[](https://www.python.org/downloads/)
|
|
23
|
+
[](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
|