mqttxx 2.0.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 +81 -0
- mqttxx/client.py +459 -0
- mqttxx/config.py +190 -0
- mqttxx/conventions.py +145 -0
- mqttxx/exceptions.py +160 -0
- mqttxx/protocol.py +201 -0
- mqttxx/rpc.py +417 -0
- mqttxx-2.0.2.dist-info/LICENSE +21 -0
- mqttxx-2.0.2.dist-info/METADATA +490 -0
- mqttxx-2.0.2.dist-info/RECORD +12 -0
- mqttxx-2.0.2.dist-info/WHEEL +5 -0
- mqttxx-2.0.2.dist-info/top_level.txt +1 -0
mqttxx/config.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# MQTT 客户端配置对象
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class TLSConfig:
|
|
10
|
+
"""TLS/SSL 配置
|
|
11
|
+
|
|
12
|
+
用于配置 MQTT 连接的加密传输层
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
enabled: 是否启用 TLS/SSL
|
|
16
|
+
ca_certs: CA 证书路径(用于验证服务器证书)
|
|
17
|
+
certfile: 客户端证书路径(双向认证)
|
|
18
|
+
keyfile: 客户端私钥路径(双向认证)
|
|
19
|
+
verify_mode: 证书验证模式("CERT_REQUIRED" | "CERT_OPTIONAL" | "CERT_NONE")
|
|
20
|
+
check_hostname: 是否验证服务器主机名
|
|
21
|
+
|
|
22
|
+
示例:
|
|
23
|
+
# 单向 TLS(仅验证服务器)
|
|
24
|
+
tls = TLSConfig(
|
|
25
|
+
enabled=True,
|
|
26
|
+
ca_certs=Path("/path/to/ca.crt")
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# 双向 TLS(客户端证书认证)
|
|
30
|
+
tls = TLSConfig(
|
|
31
|
+
enabled=True,
|
|
32
|
+
ca_certs=Path("/path/to/ca.crt"),
|
|
33
|
+
certfile=Path("/path/to/client.crt"),
|
|
34
|
+
keyfile=Path("/path/to/client.key")
|
|
35
|
+
)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
enabled: bool = False
|
|
39
|
+
ca_certs: Optional[Path] = None
|
|
40
|
+
certfile: Optional[Path] = None
|
|
41
|
+
keyfile: Optional[Path] = None
|
|
42
|
+
verify_mode: str = "CERT_REQUIRED"
|
|
43
|
+
check_hostname: bool = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class AuthConfig:
|
|
48
|
+
"""MQTT 认证配置
|
|
49
|
+
|
|
50
|
+
用于配置 MQTT Broker 的用户名密码认证
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
username: MQTT 用户名
|
|
54
|
+
password: MQTT 密码
|
|
55
|
+
|
|
56
|
+
示例:
|
|
57
|
+
auth = AuthConfig(
|
|
58
|
+
username="device_123",
|
|
59
|
+
password="secret_password"
|
|
60
|
+
)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
username: Optional[str] = None
|
|
64
|
+
password: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ReconnectConfig:
|
|
69
|
+
"""重连配置
|
|
70
|
+
|
|
71
|
+
配置 MQTT 连接断开后的自动重连行为
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
enabled: 是否启用自动重连
|
|
75
|
+
interval: 初始重连间隔(秒)
|
|
76
|
+
max_attempts: 最大重连次数(0 = 无限重试)
|
|
77
|
+
backoff_multiplier: 指数退避倍数(每次重连失败后,间隔乘以此倍数)
|
|
78
|
+
max_interval: 最大重连间隔(秒)
|
|
79
|
+
|
|
80
|
+
示例:
|
|
81
|
+
# 无限重试,指数退避
|
|
82
|
+
reconnect = ReconnectConfig(
|
|
83
|
+
enabled=True,
|
|
84
|
+
interval=5,
|
|
85
|
+
max_attempts=0,
|
|
86
|
+
backoff_multiplier=1.5,
|
|
87
|
+
max_interval=60
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# 固定间隔,最多重试 10 次
|
|
91
|
+
reconnect = ReconnectConfig(
|
|
92
|
+
interval=5,
|
|
93
|
+
max_attempts=10,
|
|
94
|
+
backoff_multiplier=1.0 # 不使用指数退避
|
|
95
|
+
)
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
enabled: bool = True
|
|
99
|
+
interval: int = 5
|
|
100
|
+
max_attempts: int = 0
|
|
101
|
+
backoff_multiplier: float = 1.5
|
|
102
|
+
max_interval: int = 60
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class MQTTConfig:
|
|
107
|
+
"""MQTT 客户端完整配置
|
|
108
|
+
|
|
109
|
+
所有 MQTT 连接相关的配置参数
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
broker_host: MQTT Broker 地址
|
|
113
|
+
broker_port: MQTT Broker 端口(1883=明文, 8883=TLS)
|
|
114
|
+
client_id: 客户端标识符(空字符串=自动生成)
|
|
115
|
+
keepalive: 心跳保活时间(秒)
|
|
116
|
+
clean_session: 是否清除会话(False=服务器保持订阅)
|
|
117
|
+
tls: TLS/SSL 配置
|
|
118
|
+
auth: 认证配置
|
|
119
|
+
reconnect: 重连配置
|
|
120
|
+
max_queued_messages: 最大排队消息数(0=无限)
|
|
121
|
+
max_payload_size: 最大消息载荷大小(字节,防止 DoS 攻击)
|
|
122
|
+
log_level: 日志级别(DEBUG|INFO|WARNING|ERROR)
|
|
123
|
+
|
|
124
|
+
示例:
|
|
125
|
+
# 基础配置(明文连接)
|
|
126
|
+
config = MQTTConfig(
|
|
127
|
+
broker_host="localhost",
|
|
128
|
+
client_id="device_123"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# 生产配置(TLS + 认证 + 自动重连)
|
|
132
|
+
config = MQTTConfig(
|
|
133
|
+
broker_host="mqtt.example.com",
|
|
134
|
+
broker_port=8883,
|
|
135
|
+
client_id="device_123",
|
|
136
|
+
clean_session=False, # 保持会话
|
|
137
|
+
tls=TLSConfig(
|
|
138
|
+
enabled=True,
|
|
139
|
+
ca_certs=Path("/etc/ssl/ca.crt")
|
|
140
|
+
),
|
|
141
|
+
auth=AuthConfig(
|
|
142
|
+
username="device_123",
|
|
143
|
+
password="secret"
|
|
144
|
+
),
|
|
145
|
+
reconnect=ReconnectConfig(
|
|
146
|
+
interval=5,
|
|
147
|
+
max_attempts=0 # 无限重试
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
# 连接参数
|
|
153
|
+
broker_host: str
|
|
154
|
+
broker_port: int = 1883
|
|
155
|
+
client_id: str = ""
|
|
156
|
+
keepalive: int = 60
|
|
157
|
+
clean_session: bool = False
|
|
158
|
+
|
|
159
|
+
# 子配置对象
|
|
160
|
+
tls: TLSConfig = field(default_factory=TLSConfig)
|
|
161
|
+
auth: AuthConfig = field(default_factory=AuthConfig)
|
|
162
|
+
reconnect: ReconnectConfig = field(default_factory=ReconnectConfig)
|
|
163
|
+
|
|
164
|
+
# 消息限制
|
|
165
|
+
max_queued_messages: int = 0 # 0 = 无限
|
|
166
|
+
max_payload_size: int = 1024 * 1024 # 1MB
|
|
167
|
+
|
|
168
|
+
# 日志级别
|
|
169
|
+
log_level: str = "INFO"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class RPCConfig:
|
|
174
|
+
"""RPC 配置
|
|
175
|
+
|
|
176
|
+
RPC 调用相关的配置参数
|
|
177
|
+
|
|
178
|
+
Attributes:
|
|
179
|
+
default_timeout: 默认超时时间(秒)
|
|
180
|
+
max_concurrent_calls: 最大并发 RPC 调用数
|
|
181
|
+
|
|
182
|
+
示例:
|
|
183
|
+
rpc_config = RPCConfig(
|
|
184
|
+
default_timeout=30.0,
|
|
185
|
+
max_concurrent_calls=100
|
|
186
|
+
)
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
default_timeout: float = 30.0
|
|
190
|
+
max_concurrent_calls: int = 100
|
mqttxx/conventions.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# 约定式 RPC 管理器 - 去角色设计
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from .client import MQTTClient
|
|
7
|
+
from .config import RPCConfig
|
|
8
|
+
from .rpc import RPCManager, AuthCallback
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConventionalRPCManager(RPCManager):
|
|
12
|
+
"""约定式 RPC 管理器
|
|
13
|
+
|
|
14
|
+
设计原则:
|
|
15
|
+
- 约定优于配置
|
|
16
|
+
- 自动订阅本地 topic
|
|
17
|
+
- 自动注入 reply_to
|
|
18
|
+
|
|
19
|
+
核心功能:
|
|
20
|
+
1. 初始化时自动订阅 `my_topic`
|
|
21
|
+
2. 调用时自动将 `my_topic` 注入到 `reply_to`
|
|
22
|
+
|
|
23
|
+
适用场景:
|
|
24
|
+
- 边缘设备 ↔ 云端(edge/xxx ↔ cloud/xxx)
|
|
25
|
+
- 微服务之间(auth-service ↔ user-service)
|
|
26
|
+
- IoT 网关 ↔ 设备(gateway/001 ↔ device/123)
|
|
27
|
+
- 任何需要简化 RPC 调用的场景
|
|
28
|
+
|
|
29
|
+
示例:
|
|
30
|
+
# 边缘设备
|
|
31
|
+
rpc = ConventionalRPCManager(client, my_topic="edge/device_123")
|
|
32
|
+
|
|
33
|
+
@rpc.register("get_status")
|
|
34
|
+
async def get_status(params):
|
|
35
|
+
return {"status": "online"}
|
|
36
|
+
|
|
37
|
+
# 调用云端(自动注入 reply_to="edge/device_123")
|
|
38
|
+
config = await rpc.call("cloud/config-service", "get_config")
|
|
39
|
+
|
|
40
|
+
# 云端服务
|
|
41
|
+
rpc = ConventionalRPCManager(client, my_topic="cloud/config-service")
|
|
42
|
+
|
|
43
|
+
# 调用边缘设备(自动注入 reply_to="cloud/config-service")
|
|
44
|
+
status = await rpc.call("edge/device_123", "execute_command")
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
client: MQTTClient,
|
|
50
|
+
my_topic: str,
|
|
51
|
+
config: Optional[RPCConfig] = None,
|
|
52
|
+
auth_callback: Optional[AuthCallback] = None,
|
|
53
|
+
):
|
|
54
|
+
"""初始化约定式 RPC 管理器
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
client: MQTTClient 实例
|
|
58
|
+
my_topic: 本节点的 topic(自动订阅,自动注入到 reply_to)
|
|
59
|
+
config: RPC 配置(可选)
|
|
60
|
+
auth_callback: 权限检查回调(可选)
|
|
61
|
+
|
|
62
|
+
自动行为:
|
|
63
|
+
- 自动订阅 my_topic
|
|
64
|
+
- 自动绑定消息处理器
|
|
65
|
+
|
|
66
|
+
示例:
|
|
67
|
+
# 边缘设备
|
|
68
|
+
rpc = ConventionalRPCManager(client, my_topic="edge/device_123")
|
|
69
|
+
|
|
70
|
+
# 云端服务
|
|
71
|
+
rpc = ConventionalRPCManager(client, my_topic="cloud/server_001")
|
|
72
|
+
|
|
73
|
+
# 微服务
|
|
74
|
+
rpc = ConventionalRPCManager(client, my_topic="auth-service")
|
|
75
|
+
|
|
76
|
+
# 多层级
|
|
77
|
+
rpc = ConventionalRPCManager(client, my_topic="region/zone/device")
|
|
78
|
+
"""
|
|
79
|
+
super().__init__(client, config, auth_callback)
|
|
80
|
+
|
|
81
|
+
self._my_topic = my_topic
|
|
82
|
+
|
|
83
|
+
# 自动订阅
|
|
84
|
+
client.subscribe(my_topic, self.handle_rpc_message)
|
|
85
|
+
|
|
86
|
+
logger.info(f"ConventionalRPCManager 已初始化 - my_topic: {my_topic}")
|
|
87
|
+
|
|
88
|
+
async def call(
|
|
89
|
+
self,
|
|
90
|
+
topic: str,
|
|
91
|
+
method: str,
|
|
92
|
+
params: Any = None,
|
|
93
|
+
timeout: Optional[float] = None,
|
|
94
|
+
reply_to: Optional[str] = None,
|
|
95
|
+
) -> Any:
|
|
96
|
+
"""调用远程方法(自动注入 reply_to)
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
topic: 对方的 topic
|
|
100
|
+
method: 方法名
|
|
101
|
+
params: 参数(可选)
|
|
102
|
+
timeout: 超时时间(可选)
|
|
103
|
+
reply_to: 响应 topic(可选,默认使用 my_topic)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
方法返回值
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
MQTTXError: 客户端未连接
|
|
110
|
+
RPCTimeoutError: 调用超时
|
|
111
|
+
RPCRemoteError: 远程执行失败
|
|
112
|
+
|
|
113
|
+
示例:
|
|
114
|
+
# 边缘设备调用云端
|
|
115
|
+
rpc = ConventionalRPCManager(client, my_topic="edge/device_123")
|
|
116
|
+
result = await rpc.call("cloud/server_001", "get_config")
|
|
117
|
+
# 等价于:await super().call("cloud/server_001", "get_config", reply_to="edge/device_123")
|
|
118
|
+
|
|
119
|
+
# 云端调用边缘设备
|
|
120
|
+
rpc = ConventionalRPCManager(client, my_topic="cloud/server_001")
|
|
121
|
+
result = await rpc.call("edge/device_123", "execute_command", params={"cmd": "restart"})
|
|
122
|
+
|
|
123
|
+
# 微服务调用
|
|
124
|
+
rpc = ConventionalRPCManager(client, my_topic="auth-service")
|
|
125
|
+
user = await rpc.call("user-service", "get_user", params={"id": 123})
|
|
126
|
+
"""
|
|
127
|
+
# 自动注入 reply_to
|
|
128
|
+
reply_to = reply_to or self._my_topic
|
|
129
|
+
|
|
130
|
+
return await super().call(
|
|
131
|
+
topic=topic,
|
|
132
|
+
method=method,
|
|
133
|
+
params=params,
|
|
134
|
+
timeout=timeout,
|
|
135
|
+
reply_to=reply_to,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def my_topic(self) -> str:
|
|
140
|
+
"""获取当前节点的 topic
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
当前节点订阅的 topic
|
|
144
|
+
"""
|
|
145
|
+
return self._my_topic
|
mqttxx/exceptions.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# MQTTX 异常定义和错误码
|
|
2
|
+
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ErrorCode(IntEnum):
|
|
7
|
+
"""错误码定义(遵循 HTTP 风格分类)
|
|
8
|
+
|
|
9
|
+
错误码范围:
|
|
10
|
+
- 1xxx: 连接错误
|
|
11
|
+
- 2xxx: 消息错误
|
|
12
|
+
- 3xxx: RPC 错误
|
|
13
|
+
- 4xxx: 权限错误
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# 连接错误 (1xxx)
|
|
17
|
+
NOT_CONNECTED = 1001
|
|
18
|
+
CONNECTION_FAILED = 1002
|
|
19
|
+
CONNECTION_LOST = 1003
|
|
20
|
+
SUBSCRIBE_FAILED = 1004
|
|
21
|
+
PUBLISH_FAILED = 1005
|
|
22
|
+
|
|
23
|
+
# 消息错误 (2xxx)
|
|
24
|
+
INVALID_JSON = 2001
|
|
25
|
+
INVALID_UTF8 = 2002
|
|
26
|
+
PAYLOAD_TOO_LARGE = 2003
|
|
27
|
+
INVALID_MESSAGE_TYPE = 2004
|
|
28
|
+
MISSING_REQUIRED_FIELD = 2005
|
|
29
|
+
|
|
30
|
+
# RPC 错误 (3xxx)
|
|
31
|
+
METHOD_NOT_FOUND = 3001
|
|
32
|
+
RPC_TIMEOUT = 3002
|
|
33
|
+
RPC_EXECUTION_ERROR = 3003
|
|
34
|
+
TOO_MANY_PENDING_CALLS = 3004
|
|
35
|
+
INVALID_RPC_REQUEST = 3005
|
|
36
|
+
INVALID_RPC_RESPONSE = 3006
|
|
37
|
+
|
|
38
|
+
# 权限错误 (4xxx)
|
|
39
|
+
PERMISSION_DENIED = 4001
|
|
40
|
+
AUTHENTICATION_FAILED = 4002
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MQTTXError(Exception):
|
|
44
|
+
"""MQTTX 基础异常
|
|
45
|
+
|
|
46
|
+
所有 MQTTX 异常的基类,包含错误码和消息
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
message: 错误消息
|
|
50
|
+
code: 错误码(ErrorCode 枚举)
|
|
51
|
+
|
|
52
|
+
示例:
|
|
53
|
+
raise MQTTXError("连接失败", ErrorCode.CONNECTION_FAILED)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, message: str, code: ErrorCode):
|
|
57
|
+
super().__init__(message)
|
|
58
|
+
self.code = code
|
|
59
|
+
self.message = message
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
return f"[{self.code.name}] {self.message}"
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return f"{self.__class__.__name__}(code={self.code}, message={self.message!r})"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ConnectionError(MQTTXError):
|
|
69
|
+
"""连接相关异常
|
|
70
|
+
|
|
71
|
+
用于表示 MQTT 连接、订阅、发布等操作失败
|
|
72
|
+
|
|
73
|
+
示例:
|
|
74
|
+
raise ConnectionError("客户端未连接", ErrorCode.NOT_CONNECTED)
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MessageError(MQTTXError):
|
|
80
|
+
"""消息处理异常
|
|
81
|
+
|
|
82
|
+
用于表示消息解析、验证失败
|
|
83
|
+
|
|
84
|
+
示例:
|
|
85
|
+
raise MessageError("JSON 解析失败", ErrorCode.INVALID_JSON)
|
|
86
|
+
"""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class RPCError(MQTTXError):
|
|
91
|
+
"""RPC 基础异常
|
|
92
|
+
|
|
93
|
+
所有 RPC 相关异常的基类
|
|
94
|
+
"""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RPCTimeoutError(RPCError):
|
|
99
|
+
"""RPC 超时异常
|
|
100
|
+
|
|
101
|
+
当 RPC 调用超过指定时间未收到响应时抛出
|
|
102
|
+
|
|
103
|
+
示例:
|
|
104
|
+
raise RPCTimeoutError("RPC 调用超时: get_status")
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, message: str):
|
|
108
|
+
super().__init__(message, ErrorCode.RPC_TIMEOUT)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RPCRemoteError(RPCError):
|
|
112
|
+
"""远程执行失败异常
|
|
113
|
+
|
|
114
|
+
当远程方法执行过程中抛出异常时,封装该异常并返回
|
|
115
|
+
|
|
116
|
+
示例:
|
|
117
|
+
raise RPCRemoteError("远程方法执行失败: division by zero")
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, message: str):
|
|
121
|
+
super().__init__(message, ErrorCode.RPC_EXECUTION_ERROR)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class RPCMethodNotFoundError(RPCError):
|
|
125
|
+
"""方法未找到异常
|
|
126
|
+
|
|
127
|
+
当调用的 RPC 方法未在远程节点注册时抛出
|
|
128
|
+
|
|
129
|
+
示例:
|
|
130
|
+
raise RPCMethodNotFoundError("方法未找到: unknown_method")
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, message: str):
|
|
134
|
+
super().__init__(message, ErrorCode.METHOD_NOT_FOUND)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PermissionDeniedError(RPCError):
|
|
138
|
+
"""权限拒绝异常
|
|
139
|
+
|
|
140
|
+
当 RPC 调用未通过权限检查时抛出
|
|
141
|
+
|
|
142
|
+
示例:
|
|
143
|
+
raise PermissionDeniedError("权限拒绝: delete_user")
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, message: str):
|
|
147
|
+
super().__init__(message, ErrorCode.PERMISSION_DENIED)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TooManyConcurrentCallsError(RPCError):
|
|
151
|
+
"""并发调用过多异常
|
|
152
|
+
|
|
153
|
+
当并发 RPC 调用数量超过限制时抛出
|
|
154
|
+
|
|
155
|
+
示例:
|
|
156
|
+
raise TooManyConcurrentCallsError("并发调用超限: 100/100")
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, message: str):
|
|
160
|
+
super().__init__(message, ErrorCode.TOO_MANY_PENDING_CALLS)
|
mqttxx/protocol.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# MQTT RPC 消息协议定义
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Optional, Literal
|
|
5
|
+
|
|
6
|
+
from .exceptions import MessageError, ErrorCode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class RPCRequest:
|
|
11
|
+
"""RPC 请求消息
|
|
12
|
+
|
|
13
|
+
客户端发起 RPC 调用时构造的消息格式
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
request_id: 请求唯一标识符(UUID)
|
|
17
|
+
method: 远程方法名
|
|
18
|
+
type: 消息类型(固定为 "rpc_request")
|
|
19
|
+
params: 方法参数(任意类型)
|
|
20
|
+
reply_to: 响应主题(用于接收响应)
|
|
21
|
+
caller_id: 调用者标识符(用于权限检查)
|
|
22
|
+
|
|
23
|
+
示例:
|
|
24
|
+
request = RPCRequest(
|
|
25
|
+
request_id="123e4567-e89b-12d3-a456-426614174000",
|
|
26
|
+
method="get_status",
|
|
27
|
+
params={"device_id": "dev_001"},
|
|
28
|
+
reply_to="client/response",
|
|
29
|
+
caller_id="client_123"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# 序列化为字典(用于 JSON 发送)
|
|
33
|
+
data = request.to_dict()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# 必填字段(无默认值)
|
|
37
|
+
request_id: str
|
|
38
|
+
method: str
|
|
39
|
+
# 可选字段(有默认值)
|
|
40
|
+
type: Literal["rpc_request"] = "rpc_request"
|
|
41
|
+
params: Any = None
|
|
42
|
+
reply_to: str = ""
|
|
43
|
+
caller_id: str = ""
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
"""转为字典(用于 JSON 序列化)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
包含所有字段的字典
|
|
50
|
+
"""
|
|
51
|
+
return {
|
|
52
|
+
"type": self.type,
|
|
53
|
+
"request_id": self.request_id,
|
|
54
|
+
"method": self.method,
|
|
55
|
+
"params": self.params,
|
|
56
|
+
"reply_to": self.reply_to,
|
|
57
|
+
"caller_id": self.caller_id,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, data: dict) -> "RPCRequest":
|
|
62
|
+
"""从字典构造(用于 JSON 反序列化)
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
data: 包含消息字段的字典
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
RPCRequest 实例
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
MessageError: 缺少必需字段时抛出
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
return cls(
|
|
75
|
+
request_id=data["request_id"],
|
|
76
|
+
method=data["method"],
|
|
77
|
+
params=data.get("params"),
|
|
78
|
+
reply_to=data.get("reply_to", ""),
|
|
79
|
+
caller_id=data.get("caller_id", ""),
|
|
80
|
+
)
|
|
81
|
+
except KeyError as e:
|
|
82
|
+
raise MessageError(
|
|
83
|
+
f"RPC 请求缺少必需字段: {e}",
|
|
84
|
+
ErrorCode.MISSING_REQUIRED_FIELD
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class RPCResponse:
|
|
90
|
+
"""RPC 响应消息
|
|
91
|
+
|
|
92
|
+
服务端处理 RPC 请求后返回的消息格式
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
type: 消息类型(固定为 "rpc_response")
|
|
96
|
+
request_id: 对应请求的唯一标识符
|
|
97
|
+
result: 方法返回值(成功时)
|
|
98
|
+
error: 错误消息(失败时)
|
|
99
|
+
|
|
100
|
+
注意:
|
|
101
|
+
result 和 error 只能有一个非空
|
|
102
|
+
|
|
103
|
+
示例:
|
|
104
|
+
# 成功响应
|
|
105
|
+
response = RPCResponse(
|
|
106
|
+
request_id="123e4567-e89b-12d3-a456-426614174000",
|
|
107
|
+
result={"status": "online", "temperature": 25.5}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 错误响应
|
|
111
|
+
response = RPCResponse(
|
|
112
|
+
request_id="123e4567-e89b-12d3-a456-426614174000",
|
|
113
|
+
error="方法未找到: unknown_method"
|
|
114
|
+
)
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
type: Literal["rpc_response"] = "rpc_response"
|
|
118
|
+
request_id: str = ""
|
|
119
|
+
result: Any = None
|
|
120
|
+
error: Optional[str] = None
|
|
121
|
+
|
|
122
|
+
def to_dict(self) -> dict:
|
|
123
|
+
"""转为字典(用于 JSON 序列化)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
包含所有字段的字典
|
|
127
|
+
"""
|
|
128
|
+
data = {
|
|
129
|
+
"type": self.type,
|
|
130
|
+
"request_id": self.request_id,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if self.error is not None:
|
|
134
|
+
data["error"] = self.error
|
|
135
|
+
else:
|
|
136
|
+
data["result"] = self.result
|
|
137
|
+
|
|
138
|
+
return data
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_dict(cls, data: dict) -> "RPCResponse":
|
|
142
|
+
"""从字典构造(用于 JSON 反序列化)
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
data: 包含消息字段的字典
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
RPCResponse 实例
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
MessageError: 缺少必需字段时抛出
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
return cls(
|
|
155
|
+
request_id=data["request_id"],
|
|
156
|
+
result=data.get("result"),
|
|
157
|
+
error=data.get("error"),
|
|
158
|
+
)
|
|
159
|
+
except KeyError as e:
|
|
160
|
+
raise MessageError(
|
|
161
|
+
f"RPC 响应缺少必需字段: {e}",
|
|
162
|
+
ErrorCode.MISSING_REQUIRED_FIELD
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def parse_message(data: dict) -> RPCRequest | RPCResponse:
|
|
167
|
+
"""解析 RPC 消息(带类型验证)
|
|
168
|
+
|
|
169
|
+
根据消息的 type 字段自动判断消息类型并解析
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
data: JSON 解析后的字典
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
RPCRequest 或 RPCResponse 实例
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
MessageError: 未知消息类型或解析失败
|
|
179
|
+
|
|
180
|
+
示例:
|
|
181
|
+
data = json.loads(payload)
|
|
182
|
+
message = parse_message(data)
|
|
183
|
+
|
|
184
|
+
if isinstance(message, RPCRequest):
|
|
185
|
+
# 处理请求
|
|
186
|
+
pass
|
|
187
|
+
elif isinstance(message, RPCResponse):
|
|
188
|
+
# 处理响应
|
|
189
|
+
pass
|
|
190
|
+
"""
|
|
191
|
+
msg_type = data.get("type")
|
|
192
|
+
|
|
193
|
+
if msg_type == "rpc_request":
|
|
194
|
+
return RPCRequest.from_dict(data)
|
|
195
|
+
elif msg_type == "rpc_response":
|
|
196
|
+
return RPCResponse.from_dict(data)
|
|
197
|
+
else:
|
|
198
|
+
raise MessageError(
|
|
199
|
+
f"未知消息类型: {msg_type}",
|
|
200
|
+
ErrorCode.INVALID_MESSAGE_TYPE
|
|
201
|
+
)
|