- 将 7 个文档中的 mac 绝对路径链接(file:///Users/qingsong/...)转换为仓库相对路径, 并修正错误目标:aibox 入口 main.py → aibox.py、pymsg C++ 实现 → libmsg/inc/us/Msg.hpp、 bot4cj cpp/cnn → cuda - 主 README 补 liblogc 基础库条目与 confs2 归档说明,安装顺序补 ca - 新增 liblogc / libtry / confs2 / 3d / memdb README - 同步 db2 脚本迁移(db/create_sqlite3.sql → db2/scripts/)导致的失效引用
30 KiB
30 KiB
pymsg 模块技术手册
文档版本:2026-07-30 代码版本:0.3.0 语言:Python 3.9+(核心)、C++(gateway 集成)、Go(waf 集成) 定位:跨语言 TCP 消息帧协议库
一、模块概述
1.1 定位
pymsg 是一个极简跨语言 TCP 消息帧协议库,基于长度前缀(4 字节大端 uint32)+ UTF-8 载荷。为微服务体系提供轻量级 RPC 通信层。
1.2 设计哲学
| 原则 | 说明 |
|---|---|
| 零依赖 | 仅使用 Python stdlib,无需任何第三方包 |
| 极简协议 | 4 字节头 + 载荷,实现只需 20 行代码 |
| 跨语言 | Python/C++/Go 三端实现完全兼容 |
| 二进制安全 | 长度前缀帧,JSON 中的换行符无需转义 |
| fail-safe | 日志推送在 db 不可用时静默丢弃,不影响业务 |
1.3 与微服务体系的关系
pymsg 是微服务的核心通信层:
┌──────────────┐ pymsg ┌──────────────┐
│ dnfs:8006 │ ───────→│ db:8005 │
│ (Python) │ 短连接 │ (Python) │
└──────────────┘ RPC └──────────────┘
┌──────────────┐ pymsg ┌──────────────┐
│ gateway:8443 │ ───────→│ db:8005 │
│ (C++20) │ 短连接 │ │
└──────────────┘ RPC └──────────────┘
┌──────────────┐ pymsg ┌──────────────┐
│ waf:443 │ ───────→│ db:8005 │
│ (Go) │ 短连接 │ │
└──────────────┘ RPC └──────────────┘
二、技术架构
2.1 协议格式
┌────────────────────────┬──────────────────────────┐
│ 4 字节(大端序) │ N 字节(UTF-8) │
│ uint32 载荷字节长度 │ JSON / 文本载荷 │
└────────────────────────┴──────────────────────────┘
2.2 架构图
┌─────────────────────────────────────────────────────────────┐
│ 微服务调用方 │
│ (dnfs / gateway / waf / static / smtp / captcha) │
└─────────────────────────┬───────────────────────────────────┘
│ MsgClient.call() / send()
┌─────────────────────────▼───────────────────────────────────┐
│ pymsg 协议层 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 底层协议函数 │ │
│ │ send(sock, data) → [4字节长度头][UTF-8载荷] │ │
│ │ recv(sock) → 读4字节→解析长度→读对应字节→返回 │ │
│ └──────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼──────────────────────────────┐ │
│ │ MsgClient 客户端 │ │
│ │ send(msg) — 短连接,发送即忘(带 name 前缀) │ │
│ │ call(msg) — 短连接,RPC 请求-应答 │ │
│ │ listen(cb) — 长连接,回调模式 │ │
│ └──────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼──────────────────────────────┐ │
│ │ MsgServer 服务端 │ │
│ │ start(on_message) — 启动监听 │ │
│ │ reply(sock, msg) — 回复客户端 │ │
│ │ broadcast(msg) — 广播到所有客户端 │ │
│ │ stop() — 停止服务 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ TCP
┌─────────────────────────▼───────────────────────────────────┐
│ db:8005 服务端 │
│ MsgServer + 路由分发 │
└─────────────────────────────────────────────────────────────┘
2.3 消息流
RPC 请求-应答
调用方 服务端 (db)
────── ──────────
MsgClient.call(json_str)
│
├── socket.connect(127.0.0.1:8005)
├── send(sock, json_str)
│ ├── payload = json_str.encode("utf-8")
│ ├── header = struct.pack("!I", len(payload))
│ └── sock.sendall(header + payload)
│
├── recv(sock) ←── on_message(sock, addr, text)
│ ├── sock.recv(4) ├── json.loads(text)
│ ├── length = struct.unpack("!I", header)[0]
│ ├── sock.recv(length) ├── handler(req)
│ └── payload.decode("utf-8") └── server.reply(sock, response)
│ ├── send(sock, response)
├── close(sock) │ └── sock.sendall(header + payload)
└── return response ←── recv(sock) 返回 response
日志推送
各服务 (gateway/static/smtp) db:8005
───────────────────────────── ──────────
log_client.push(entry)
│
├── 入内存队列 (ch, 容量 256)
│
├── 后台 goroutine 批量 flush
│ ├── 满 50 条 或 2 秒超时
│ └── 逐条 MsgClient.call(json)
│
└── (同 RPC 流程,fail-safe:失败静默丢弃)
三、代码结构
3.1 文件结构
pymsg/
├── pymsg/
│ ├── __init__.py # 核心协议与 MsgClient/MsgServer(~470 行)
│ └── rpc.py # JSON RPC 约定层 RpcClient/RpcServer(~170 行,v0.3.0)
├── tests/
│ ├── test_threadpool.py # 线程池/同步模式单元测试(纯 stdlib)
│ └── test_rpc.py # RPC 约定层单元测试(纯 stdlib)
├── README.md # 用户文档
├── pyproject.toml # 项目配置
└── .gitignore
3.2 跨语言实现位置
| 语言 | 文件 | 说明 |
|---|---|---|
| Python | pymsg/__init__.py | 主实现 |
| C++ | gateway/inc/Msg.hpp | gateway 使用 |
| Go | waf/internal/logclient/logclient.go | waf 使用 |
| Python (黑盒) | tests/blackbox/helpers.py | 纯 stdlib 测试实现 |
3.3 代码索引
Python 实现(__init__.py)
| 函数/类 | 行号 | 功能 |
|---|---|---|
send(sock, data, *, timeout=None) |
L51 | 底层发送 |
recv(sock, *, timeout=None) |
L75 | 底层接收 |
_recv_exact(sock, n) |
L106 | 精确读 n 字节 |
_timeout(sock, seconds) |
L38 | 超时 context manager |
TimeoutError |
L33 | 超时异常 |
MsgClient |
L123 | TCP 客户端 |
MsgClient.__init__ |
L139 | 构造器 |
MsgClient.connect() |
L147 | 建立长连接 |
MsgClient.send(msg) |
L153 | 短连接发送(带 name 前缀) |
MsgClient.call(msg, *, timeout=None) |
L166 | 短连接 RPC |
MsgClient.listen(on_message) |
L196 | 长连接监听 |
MsgClient.stop() |
L223 | 停止监听 |
MsgServer |
L237 | TCP 服务端 |
MsgServer.__init__(addr, port, *, workers=0) |
L262 | 构造器(workers>0 启用线程池) |
MsgServer.start(on_message) |
L276 | 启动服务 |
MsgServer.broadcast(msg, except_sock) |
L329 | 广播(每连接锁串行化) |
MsgServer.reply(sock, msg) |
L355 | 回复(每连接锁串行化) |
MsgServer._locked_send(sock, msg) |
L363 | 带每连接锁的发送 |
MsgServer.stop() |
L373 | 停止服务 |
MsgServer._run_task(...) |
L413 | 工作线程:执行回调、失败打标记 |
MsgServer._reap_failed(socketlist) |
L434 | 主循环统一移除失败连接 |
RPC 约定层(rpc.py,v0.3.0)
| 函数/类 | 行号 | 功能 |
|---|---|---|
RpcError |
L28 | 业务错误异常(success=false / 非法响应) |
RpcClient |
L32 | JSON RPC 客户端 |
RpcClient.call(route, payload, *, timeout) |
L46 | 注入 _route、编解码、检查 success,返回 data |
RpcServer |
L80 | JSON RPC 服务端(基于 MsgServer) |
RpcServer.route(name) / add_route |
L119/L126 | 注册路由 handler(fn(req) -> data) |
RpcServer.start() / stop() |
L131/L135 | 启动(阻塞)/ 停止 |
RpcServer._dispatch |
L140 | 路由分发与异常包装 |
RpcServer._reply |
L164 | 响应包装(支持 json_default) |
C++ 实现(Msg.hpp)
| 函数/类 | 行号 | 功能 |
|---|---|---|
MsgException |
L47 | 基础异常 |
MsgTimeoutException |
L52 | 超时异常 |
MsgConnectionException |
L57 | 连接异常 |
writeUint32BE(buf, val) |
L72 | 4 字节大端编码 |
readUint32BE(buf) |
L79 | 4 字节大端解码 |
setSockTimeout(sockfd, seconds) |
L88 | 设置 socket 超时 |
recvExact(sockfd, buf, n) |
L99 | 精确读 n 字节 |
sendAll(sockfd, data, len) |
L121 | 完整发送 |
openConnection(addr, port, timeout) |
L139 | 打开短连接 |
pmsg_send(sockfd, data) |
L174 | 协议发送 |
pmsg_recv(sockfd) |
L188 | 协议接收 |
MsgClient |
L210 | C++ 客户端 |
MsgClient.connect() |
L220 | 长连接 |
MsgClient.send(msg) |
L228 | 短连接发送 |
MsgClient.call(msg, timeout) |
L248 | 短连接 RPC |
MsgClient.listen(on_message) |
L269 | 长连接监听 |
MsgClient.stop() |
L311 | 停止 |
Go 实现(logclient.go)
| 函数 | 行号 | 功能 |
|---|---|---|
pymsgCall(payload) |
L226 | 发送并接收一条 pymsg 消息 |
Client.sendOne(entry, ts) |
L179 | 发送单条日志 |
Client.send(batch) |
L171 | 批量发送 |
Client.flushLoop() |
L125 | 后台 flush goroutine |
Push(service, level, message, extra) |
L82 | 全局推送 |
四、API 参考
4.1 底层函数
send(sock, data, *, timeout=None)
def send(sock: socket.socket, data: str, *, timeout: float | None = None) -> None
功能:在已连接的 socket 上发送一条长度前缀帧消息
- 将字符串编码为 UTF-8
- 前面加上 4 字节大端长度头
- 调用
sock.sendall()发送
参数:
sock:已连接的 socket 对象data:要发送的字符串timeout:可选超时(秒),超时抛出TimeoutError
recv(sock, *, timeout=None)
def recv(sock: socket.socket, *, timeout: float | None = None) -> str
功能:从已连接的 socket 接收一条长度前缀帧消息
- 读取 4 字节头解出长度
- 读取指定字节数
- 返回解码后的 UTF-8 字符串
参数:
sock:已连接的 socket 对象timeout:可选超时(秒)
返回值:解码后的字符串
异常:
ConnectionError:连接在完整消息接收前断开TimeoutError:操作超时
TimeoutError
class TimeoutError(Exception)
send 或 recv 超过指定超时时抛出的异常。
4.2 MsgClient(客户端)
构造
client = MsgClient(addr: str, port: int, name: str)
参数:
addr:服务端地址或 IPport:服务端端口name:客户端标识(send()模式下以"name: message"格式发送)
方法
| 方法 | 模式 | 说明 |
|---|---|---|
connect() |
长连接 | 创建持久 TCP 连接(供 listen() 使用) |
send(msg) |
短连接 | 发送即忘,消息带 name: msg 前缀 |
call(msg, timeout=10.0) |
短连接 | RPC 模式,阻塞等待回复 |
listen(on_message) |
长连接 | 阻塞当前线程,持续接收消息 |
stop() |
— | 中断 listen(),可从另一线程调用 |
使用示例
from pymsg import MsgClient
# RPC 调用(短连接)
client = MsgClient("127.0.0.1", 8005, "my-service")
resp = client.call('{"_route":"execute","action":"select","table":"Users"}', timeout=10.0)
print(resp) # {"success": true, "data": [...]}
# 发送即忘
client.send("hello")
# 长连接监听
def on_message(text):
print(f"收到: {text}")
client.connect()
client.listen(on_message) # 阻塞,直到 stop()
4.3 MsgServer(服务端)
构造
server = MsgServer(addr: str, port: int, *, workers: int = 0)
参数:
addr:监听地址(0.0.0.0表示所有网卡)port:监听端口workers:工作线程数(v0.2.0+)。默认0为同步模式(回调在 select 循环线程内串行执行);>0时回调提交到ThreadPoolExecutor并发执行
并发模型(v0.2.0)
客户端 A ──┐
客户端 B ──┼──► select 主循环(单线程)
客户端 C ──┘ │
├── accept / recv(主线程,快速 I/O)
│
├── workers=0:on_message 就地同步执行(串行)
│
└── workers>0:标记 busy → submit 到 ThreadPoolExecutor
│
▼
worker 线程执行 on_message
├── reply() 经该连接发送锁串行化
├── 异常 → 标记 _failed(主循环下轮统一移除)
└── 完成 → 清除 busy,连接恢复 select 监听
保证:
- 同连接串行:busy 标记使回调完成前该连接暂停 select 监听,同一连接的消息不会并发执行(恢复粒度 ≤100ms,短连接 RPC 无感)
- 发送原子性:
reply()/broadcast()经每连接Lock串行化,多 worker 向同一 socket 写入不会帧交错 - 故障隔离:回调抛非连接异常时打印 traceback 并移除该连接,服务整体存活(同步模式下异常会使
start()崩溃,两模式语义不同) - fd 安全:worker 线程不直接 close socket,连接移除统一由主循环执行,避免与 accept/select 产生 fd 复用竞态
方法
| 方法 | 说明 |
|---|---|
start(on_message) |
启动服务器(阻塞),回调签名:on_message(sock, addr, text) |
reply(sock, msg) |
向指定客户端回复 |
broadcast(msg, except_sock=None) |
广播到所有客户端(可排除某个 socket) |
stop() |
停止服务(可从另一线程调用) |
回调签名
def on_message(sock: socket.socket, addr: tuple, text: str):
"""
Args:
sock: 发送消息的客户端套接字(用于 reply)
addr: (ip, port) 元组
text: 解码后的字符串消息
"""
server.reply(sock, "ack")
使用示例
from pymsg import MsgServer
def on_message(sock, addr, text):
print(f"[{addr}] {text}")
server.reply(sock, "ack")
server = MsgServer("0.0.0.0", 8005)
server.start(on_message) # 阻塞
4.4 异常速查
| 异常 | 触发条件 |
|---|---|
pymsg.TimeoutError |
send() 或 recv() 超过 timeout |
ConnectionError(内置) |
完整消息接收前 socket 断开 |
ConnectionRefusedError(内置) |
MsgClient.call() 或 send() 无法连接目标 |
RuntimeError |
MsgClient.listen() 在 connect() 之前调用 |
五、使用场景
5.1 场景一:数据库服务(db:8005)
db 服务通过 pymsg 暴露通用 CRUD 接口:
import json
from pymsg import MsgServer
class DBServer:
def __init__(self):
self._server = MsgServer("127.0.0.1", 8005)
self._routes = {"execute": self._handle_execute}
def start(self):
self._server.start(self._dispatch)
def _dispatch(self, sock, addr, text):
req = json.loads(text)
route = req.pop("_route", "")
handler = self._routes.get(route)
if handler is None:
self._server.reply(sock,
json.dumps({"success": False, "error": "未知路由"}))
return
try:
result = handler(req)
self._server.reply(sock,
json.dumps({"success": True, "data": result}))
except Exception as e:
self._server.reply(sock,
json.dumps({"success": False, "error": str(e)}))
def _handle_execute(self, data):
# 通过 SQLAlchemy 执行 SQL,返回 dict 列表
pass
5.2 场景二:RPC 调用(dnfs → db)
import json
from pymsg import MsgClient
def _call_dbv2(payload):
client = MsgClient("127.0.0.1", 8005, "dnfs")
resp = client.call(json.dumps(payload), timeout=10.0)
return json.loads(resp)
# 创建笔记
result = _call_dbv2({
"_route": "execute",
"action": "insert",
"table": "Notes",
"values": {"title": "会议记录", "content": "..."}
})
5.3 场景三:异步日志推送(log_client)
import json
from pymsg import MsgClient
def _send(batch):
for entry in batch:
client = MsgClient("127.0.0.1", 8005, "gateway-log")
client.call(json.dumps({
"_route": "execute",
"action": "insert",
"table": "Logs",
"values": {
"ts": now,
"service": entry["service"],
"level": entry["level"],
"message": entry["message"],
}
}), timeout=5)
5.4 场景四:日志推送(Go 实现)
// waf/internal/logclient/logclient.go
func (c *Client) sendOne(entry Entry, ts string) {
req := dbV2Request{
Route: "execute",
Action: "insert",
Table: "Logs",
Values: values,
}
data, _ := json.Marshal(req)
resp, err := pymsgCall(data) // 短连接 RPC
if err != nil {
return // fail-safe
}
}
func pymsgCall(payload []byte) ([]byte, error) {
conn, err := net.DialTimeout("tcp", "127.0.0.1:8005", 5*time.Second)
// ... 写 4 字节头 + 载荷,读回复
}
六、跨语言实现对照
6.1 协议层
| 功能 | Python | C++ | Go |
|---|---|---|---|
| 4 字节编码 | struct.Struct("!I") |
writeUint32BE (手动位移) |
binary.BigEndian.PutUint32 |
| 4 字节解码 | struct.unpack("!I", header) |
readUint32BE (手动位移) |
binary.BigEndian.Uint32 |
| 超时设置 | _timeout context manager |
setSockTimeout (setsockopt) |
net.DialTimeout + SetDeadline |
| 安全上限 | 无 | 64 MiB | 无 |
6.2 客户端对照
| 操作 | Python | C++ | Go |
|---|---|---|---|
| 构造 | MsgClient(addr, port, name) |
MsgClient(addr, port, name) |
— |
| 长连接 | connect() |
connect() |
— |
| 发送即忘 | send(msg) 短连接 + name: 前缀 |
send(msg) 短连接 + name: 前缀 |
— |
| RPC 调用 | call(msg, timeout) 短连接 |
call(msg, timeout) 短连接 |
pymsgCall(payload) 短连接 |
| 长监听 | listen(on_message) |
listen(on_message) |
— |
| 停止 | stop() |
stop() |
— |
6.3 服务端对照
| 操作 | Python | C++ | Go |
|---|---|---|---|
| 启动 | start(on_message) |
— | — |
| 回复 | reply(sock, msg) |
pmsg_send(fd, msg) |
— |
| 广播 | broadcast(msg, except_sock) |
— | — |
| 停止 | stop() |
— | — |
6.4 从头实现协议
仅需四步:
1. 将字符串编码为 UTF-8 字节
2. 将字节长度打包为 4 字节大端无符号整数
3. 写入 头 + 载荷 到 socket
4. 接收端先读 4 字节解出长度,再读对应字节数,解码为 UTF-8
七、线程安全
| 场景 | 安全性 | 说明 |
|---|---|---|
MsgServer.stop() 跨线程调用 |
✅ 安全 | 设置原子标志并关闭监听 socket |
MsgServer.reply() 在回调内调用 |
✅ 安全 | 经每连接发送锁串行化,worker 线程内同样安全 |
MsgServer.broadcast() 在回调内调用 |
✅ 安全 | 发送经每连接锁;但会遍历客户端列表,仅限回调线程,勿从无关第三方线程并发调用 |
MsgClient.stop() 跨线程调用 |
✅ 安全 | 可中断 listen() |
send()/recv() 多线程共享 socket |
⚠️ 不安全 | 底层函数无锁,调用方需自行串行化(MsgServer 内部已用每连接锁保护) |
| 同一连接消息并发执行回调 | ✅ 不会 | busy 标记暂停 select 监听,保证同连接串行 |
八、性能特性
8.1 性能特点
| 特性 | 说明 |
|---|---|
| 零拷贝 | 长度前缀帧避免数据复制 |
| 短连接复用 | call() 每次新建连接,但调用完成立即释放 |
| fail-safe | 日志推送失败静默丢弃,不阻塞业务 |
| 批量缓冲 | 日志推送:50 条或 2 秒自动 flush |
8.2 连接模型
| 模式 | 连接数 | 延迟 | 适用场景 |
|---|---|---|---|
send() |
每次 1 个 | 中 | 低频通知 |
call() |
每次 1 个 | 中 | RPC 调用 |
listen() |
持久 1 个 | 低 | 高频消息流 |
九、已知限制
| ID | 描述 | 严重等级 | 影响范围 |
|---|---|---|---|
| L1 | 无消息校验:协议无 CRC/哈希,传输损坏无法检测 | 🟡 Medium | 所有消息 |
| L2 | 无认证:任何人可连接 8005 端口,无鉴权机制 | 🔴 High | db 服务 |
| L3 | 无加密:明文传输,无 TLS/加密保护 | 🔴 High | 所有服务 |
| L4 | Python 版无安全上限:C++ 版有 64 MiB 上限,Python 版未检查 | 🟡 Medium | Python 客户端 |
| L5 | 无重试机制:连接失败直接抛异常,无自动重试 | 🟢 Low | 所有调用 |
| L6 | 无版本号:协议升级无兼容路径 | 🟢 Low | 长期维护 |
| L7 | 无心跳:长连接无 keep-alive,可能被防火墙切断 | 🟡 Medium | listen 模式 |
| L8 | 全局阻塞:MsgServer.start() 阻塞主线程,需另开线程 |
🟢 Low | 服务端 |
建议改进
- L2/L3:在防火墙层面限制 8005 端口仅允许本机连接(当前部署已这样做)
- L4:在 Python
recv()中添加消息大小上限检查 - L7:在
listen()循环中添加 ping/pong 机制 - L5:在
call()中添加简单重试(1-2 次)
十、设计考量
10.1 为什么用长度前缀帧?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 长度前缀帧 ✅ | 二进制安全、实现简单、无需流式解析 | 无 |
| 换行分隔 | 简单 | JSON 中换行需转义、缓冲区管理复杂 |
| HTTP | 成熟、有工具 | 头部开销大、需额外依赖 |
| Protobuf | 高效、类型安全 | 需额外依赖、跨语言实现复杂 |
10.2 为什么用短连接?
| 考虑 | 说明 |
|---|---|
| 简单性 | 短连接实现最简洁,无连接管理开销 |
| 可靠性 | 每次调用都是独立连接,单个请求失败不影响其他 |
| 性能 | 本地回环(127.0.0.1)下 TCP 连接建立开销很小 |
| 可测试性 | 易于模拟各种网络故障 |
10.3 协议设计取舍
| 取舍 | 选择 | 原因 |
|---|---|---|
| 头大小 | 4 字节 | 支持最大 4 GiB 消息,足够使用 |
| 字节序 | 大端 | 网络标准序,跨语言一致 |
| 编码 | UTF-8 | 支持中文/多语言,与 JSON 兼容 |
| 版本号 | 无 | 保持最简,升级时可扩展 |
十一、开发指南
11.1 安装
# 开发模式
cd pymsg
pip install -e .
# 作为本地依赖
# 在其他项目的 requirements.txt 中:
# ../pymsg
11.2 添加新功能
添加消息校验(CRC)
import zlib
def send_with_crc(sock, data):
payload = data.encode("utf-8")
crc = zlib.crc32(payload) & 0xFFFFFFFF
# [4字节长度][4字节CRC][UTF-8载荷]
header = _HEADER.pack(len(payload) + 4)
crc_bytes = struct.pack("!I", crc)
sock.sendall(header + crc_bytes + payload)
添加心跳机制
import time
def listen_with_heartbeat(self, on_message, interval=30):
last_recv = time.time()
while self._running:
# 发送心跳
if time.time() - last_recv > interval:
send(self._sock, "PING")
# 接收消息
text = recv(self._sock, timeout=1.0)
if text == "PONG":
continue
last_recv = time.time()
on_message(text)
11.3 扩展协议版本
# 在头中添加版本号
_VERSION = 1 # 1 字节版本号
def send_v2(sock, data):
payload = data.encode("utf-8")
header = struct.pack("!BI", _VERSION, len(payload))
sock.sendall(header + payload)
def recv_v2(sock):
header = _recv_exact(sock, 5)
version, length = struct.unpack("!BI", header)
if version != _VERSION:
raise ValueError(f"Unsupported protocol version: {version}")
payload = _recv_exact(sock, length)
return payload.decode("utf-8")
11.4 构建与测试
# Python 测试
cd pymsg
python -c "
from pymsg import MsgClient, MsgServer, send, recv
# ... 单元测试
"
# 黑盒测试
cd tests/blackbox
python test_runner.py
十二、版本历史
| 版本 | 日期 | 主要变更 |
|---|---|---|
| 0.1.0 | — | 初始版本:核心协议、MsgClient、MsgServer、跨语言实现 |
| 0.2.0 | 2026-07-30 | MsgServer 新增 workers 参数:回调可提交线程池并发执行;同连接串行(busy 标记)、每连接发送锁防帧交错、回调异常故障隔离;tests/test_threadpool.py 单元测试 |
| 0.3.0 | 2026-07-30 | 新增 rpc.py JSON RPC 约定层:RpcClient(_route 注入 + 编解码 + success 检查 + RpcError)、RpcServer(路由分发 + 响应包装 + json_default/on_error 钩子);db/smtp/captcha 服务端与 dnfs/log_client 客户端完成迁移;tests/test_rpc.py 单元测试 |
附录
A. 协议规范(RFC 风格)
Protocol: pymsg v0
Frame Format:
┌──────────┬──────────┬──────────────────┐
│ 4 bytes │ N bytes │ │
├──────────┼──────────┼──────────────────┤
│ uint32 │ UTF-8 │ │
│ big-end │ string │ │
│ payload │ payload │ │
│ length │ │ │
└──────────┴──────────┴──────────────────┘
Encoding: UTF-8
Byte Order: Big-Endian (network byte order)
Max Message Size: 2^32 - 1 bytes (theoretical)
Timeout: Optional, specified per operation
Modes:
- Short Connection (send/call): connect → send → recv → close
- Long Connection (listen): connect → loop(recv → callback)
B. 错误码映射
| 异常 | 来源 | 场景 |
|---|---|---|
TimeoutError |
pymsg | 操作超过指定超时 |
ConnectionError |
Python | 连接在接收前断开 |
ConnectionRefusedError |
Python | 目标服务不可达 |
RuntimeError |
Python | 在 connect() 前调用 listen() |
MsgTimeoutException |
C++ | C++ 版超时 |
MsgConnectionException |
C++ | C++ 版连接异常 |
C. 参考实现清单
| 语言 | 文件 | 类型 |
|---|---|---|
| Python | pymsg/__init__.py | 主实现 |
| Python | tests/blackbox/helpers.py | 测试客户端 |
| C++ | gateway/inc/Msg.hpp | gateway 客户端 |
| Go | waf/internal/logclient/logclient.go | waf 客户端 |
D. 参考资源
- Beej's Guide to Network Programming — Socket 编程经典
- The SO_REUSEADDR socket option
- Python socket module
本文档基于 pymsg 源码分析生成,供开发团队参考。