WebSocket端点必须用async def声明,因运行在事件循环中,所有I/O操作(如receive_text、send_text)均需异步;漏写async或误用同步调用会阻塞整个连接。

WebSocket端点必须用 async def 声明
FastAPI 的 WebSocket 路由不是普通函数,它运行在事件循环中,所有 I/O 操作(如 receive_text()、send_text())都必须是异步的。写成同步函数会导致阻塞整个连接,甚至让其他 WebSocket 客户端卡住。
常见错误是漏掉 async 关键字,或在 try/except 外部调用 await —— 这会直接抛出 SyntaxError: await outside async function。
- 正确写法:
@app.websocket("/ws")后跟async def websocket_endpoint(websocket: WebSocket): - 必须先调用
await websocket.accept(),否则客户端会收到 403 或连接立即关闭 -
WebSocketDisconnect异常必须显式捕获,不能靠全局异常处理器兜底,否则连接不会被清理
receive_text() 和 receive_bytes() 别混用
前端发 JSON 字符串,后端就用 receive_text();发 ArrayBuffer 或 Blob(比如上传二进制帧),就用 receive_bytes()。混用会导致 WebSocketException: cannot decode bytes as utf-8 或静默丢包。
实际项目里,多数聊天场景走文本,但实时音视频信令、模型 token 流、设备原始传感器数据往往走二进制。别为了“统一”硬转 str(data) —— 那会破坏字节完整性。
立即学习“前端免费学习笔记(深入)”;
- 文本消息:前端
socket.send(JSON.stringify({msg: "hi"}))→ 后端data = await websocket.receive_text() - 二进制消息:前端
socket.send(new Uint8Array([1,2,3]))→ 后端data = await websocket.receive_bytes() - 若需兼容两种类型,得先
receive()获取原始WebSocketMessage,再判 type 字段
广播消息时别直接遍历 active_connections 列表
看似简单的 for conn in active_connections: await conn.send_text(...) 在高并发下容易出问题:某个连接已断开但还没来得及从列表移除,send_text() 就会抛 WebSocketDisconnect,进而中断整个 for 循环,导致后续连接收不到消息。
更稳妥的做法是用 asyncio.create_task() 并发发送,并单独处理每个连接的异常。
- 不要用裸列表遍历广播;改用
asyncio.gather(*[conn.send_text(msg) for conn in connections], return_exceptions=True) - 连接管理建议封装成类(如
ConnectionManager),在disconnect()中用remove()+try/except双保险 - 记得在
WebSocketDisconnect捕获块里调用manager.disconnect(websocket),否则内存泄漏会随连接数线性增长
生产环境必须设 ping_interval 和超时参数
本地调试时连接能一直挂着,但真实网络存在 NAT 超时、代理中断、移动网络切换等场景。不主动 ping,连接可能“假活”——服务端以为在线,其实客户端早已断开。
Uvicorn 默认不启用 ping,得靠 FastAPI 层或 WebSocket 库自己补。最简单的是在 websocket.accept() 后手动启一个心跳任务,或者用 websockets 库的 ping_interval 参数(需替换默认 transport)。
- 推荐做法:在
connect()后启动asyncio.create_task(ping_loop(websocket)),每 30 秒发一次ping - 设置
websocket.iter_text(timeout=60)或自定义receive()超时,避免单个连接长期阻塞 - 注意:
uvicorn的--timeout-keep-alive是 HTTP 连接参数,对 WebSocket 无效
真正麻烦的从来不是写通第一行 await websocket.receive_text(),而是连接状态怎么精准感知、异常怎么不扩散、广播怎么不丢人。这些细节没压到代码里,上线后就会变成凌晨三点的报警电话。


















