websockets库是python 3.10+下最轻量可靠的websocket方案,需用asyncio.task管理连接、显式配置心跳参数、避免gather广播、同步业务状态。

asyncio + websockets 库是当前最轻量可靠的组合
Python 3.10 的 asyncio 已完全稳定,但直接用它裸写 WebSocket 握手、帧解析、心跳和连接管理会踩大量协议细节坑。实际项目中,websockets 库(v10+)是唯一推荐的底层依赖——它原生支持 Python 3.10+、深度集成 asyncio、自动处理分帧/掩码/关闭码,且不引入 trio 或 curio 等额外运行时。
别碰 aiohttp 的 WebSocket 支持:它在高并发下有已知的连接泄漏问题(ClientDisconnected 不总触发 cleanup),且 API 设计偏重 HTTP 生命周期,不适合长连接服务。
连接管理必须用 asyncio.Task 显式跟踪,不能只靠 async for
常见错误是写成这样:
async def handler(websocket):
async for message in websocket:
await process(message)
这会导致:连接断开时任务不退出、异常无法捕获、无法统一做超时或限速。正确做法是把每个连接生命周期封装为一个独立 asyncio.Task,并监听 websocket.closed 和异常:
- 用
asyncio.create_task()启动连接处理协程,并存入全局set或weakref.WeakSet中便于广播时遍历 - 在协程开头加
try/except websockets.exceptions.ConnectionClosed,确保断连后清理资源 - 用
asyncio.wait_for(websocket.recv(), timeout=30)替代无限制async for,防止客户端假死拖垮服务 - 发送前检查
if not websocket.closed,避免RuntimeError: unable to perform operation on <ssl.sslsocket...></ssl.sslsocket...>
websockets.serve() 的 ping_interval 和 ping_timeout 必须显式设值
默认值(ping_interval=None)意味着不发心跳,生产环境极易因 NAT 超时或中间代理静默断连。不设 ping_timeout 则客户端失联后服务端仍维持 socket 连接数,内存缓慢上涨。
建议配置:
server = await websockets.serve(
handler,
"0.0.0.0",
8080,
ping_interval=20, # 每20秒发一次 ping
ping_timeout=10, # 等 pong 超过10秒则关连接
close_timeout=5, # 关闭握手最多等5秒
max_size=2**20, # 单帧最大 1MB,防恶意大包
)
注意:ping_interval 和 ping_timeout 是 websockets v10.0+ 才支持的参数,旧版本需手动实现心跳逻辑。
广播场景下别用 asyncio.gather() 直接发消息
当需要向数百个连接广播同一条消息时,写 await asyncio.gather(*[ws.send(msg) for ws in active_connections]) 会瞬间创建大量并发 send 任务,触发内核 socket 缓冲区溢出,部分连接收到 ConnectionResetError。
更稳妥的做法:
- 对每个
websocket使用asyncio.create_task(ws.send(msg))并忽略返回值,让事件循环自行调度 - 若需控制并发量,用
asyncio.Semaphore(50)限制同时发送的连接数 - 对高频广播(如行情推送),先用
websocket.transfer_data()检查ws.state == websockets.protocol.State.OPEN,跳过正在关闭的连接 - 不要在广播循环里调用
len(active_connections)——集合大小变化快,应改用list(active_connections)快照
真正难的是连接状态与业务状态的同步:比如用户登录后要绑定 session ID,断连时要触发登出事件。这些逻辑不能只靠 websockets 自带的钩子,得自己维护映射表并加锁,否则并发修改会丢状态。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











