直接用 asyncio.streamreader/streamwriter 搭配 json 行或 protobuf 长度头协议可实现 5k+ qps 轻量 rpc;关键在精准控制 request_id 生命周期、防粘包、超时与连接复用,避免使用默认带流控的 asyncio.protocol。

直接用 asyncio.StreamReader + asyncio.StreamWriter 搭配 JSON 行协议或 protobuf 长度头协议,就能跑出 5k+ QPS 的轻量 RPC——关键不是堆库,而是控制好 request_id 生命周期、防粘包、超时和连接复用。
为什么别碰 asyncio.Protocol 做 RPC?
它默认启用 flow control 和 buffer 管理,对「一问一答」型 RPC 反而引入额外延迟;StreamReader/StreamWriter 更直白:你能精确 await reader.readuntil(b'\n') 或 await reader.readexactly(4),边界清晰,调试不抓瞎。
常见错误是以为 Protocol 更“专业”,结果连粘包都处理不好,还误以为是 asyncio bug。
- server 端用
async def handle_client(reader, writer),每个连接一个协程,干净利落 - client 端用
await asyncio.open_connection(host, port),拿到 reader/writer 后直接读写 - 若硬要用 Protocol,必须重写
data_received并手动维护缓冲区,得不偿失
request_id 怎么设才不会错配?
只用 uuid.uuid4().hex 是危险的:server 重启后旧 pending future 还在内存里,新请求带同 id 的响应进来,futures[req_id].set_result() 就会塞错对象。
安全做法是组合连接上下文:
- client 每次新连接从 1 开始计数,前缀加上
int(time.time())或连接建立时生成的 session_id - 例如:
f"{session_id}_{seq_num}",既保证单连接内递增,又跨连接隔离 - server 不存 request_id 全局表,只按当前连接上下文查——避免跨连接污染
JSON 行协议 vs protobuf 长度头,怎么选?
开发阶段优先 JSON 行协议(json.dumps(obj) + '\n'),调试时用 telnet 或 nc 直连就能发消息;上线后压测发现序列化瓶颈,再切 protobuf。
但切 protobuf 时必须加长度头,否则必然粘包:
- 客户端发送前:
payload = proto_msg.SerializeToString()→header = struct.pack("!I", len(payload))→writer.write(header + payload) - 服务端接收:
header = await reader.readexactly(4)→size = struct.unpack("!I", header)[0]→payload = await reader.readexactly(size) - 漏掉
readexactly改用read,遇到网络抖动就卡死或解析失败
client 等响应时最容易卡死的三个地方
不是 server 慢,而是 client 协程自己把自己挂住了。
- 没包
asyncio.wait_for(future, timeout=5.0),future 永远不完成,协程永远 await -
writer.write()后没等await writer.drain()就 close(),最后几字节丢了,server 根本没收到请求 - 接收协程(负责监听 socket 并 set_result)意外退出或被 cancel,future 再也没人唤醒
最后一句提醒:RPC 的复杂性不在并发模型,而在请求-响应生命周期的完整闭环——从 request_id 生成、发送、等待、超时、响应解析、到 future 完成,任一环断开,整个调用就不可观测。别省那几行错误处理代码。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











