WebSocket心跳不必由服务端主动发ping,但强烈建议如此;应使用await websocket.ping()发送原生ping帧,设间隔25–30秒、超时40–45秒,并在finally中统一清理资源。

WebSocket 心跳必须由服务端主动发 ping 吗?
不是必须,但强烈建议由服务端发起 ping。FastAPI 的 WebSocket 底层基于 Starlette,它默认不自动处理 WebSocket ping/pong;客户端是否响应 ping、是否自动发 pong,取决于其实现(如浏览器 WebSocket API 会自动应答,但某些嵌入式客户端不会)。如果只靠客户端定时发 ping,服务端无法感知其是否已静默断连(比如网络中断后客户端未触发 onclose)。
实操建议:
- 在
websocket_endpoint中启动一个后台任务(用asyncio.create_task),每 15–30 秒调用websocket.send_text('{"type":"ping"}')或直接发 WebSocket ping 帧(见下一点) - 避免用
time.sleep(),必须用await asyncio.sleep() - 心跳消息体建议用轻量协议(如纯字符串
"ping"或最小 JSON),不要带时间戳或随机数——除非你真需要做 RTT 测量
如何发送原生 WebSocket ping 帧(非文本消息)?
HTTP 协议中 WebSocket ping/pong 是控制帧(opcode 0x9 / 0xA),和文本/二进制数据帧不同。用 send_text() 发的只是普通消息,不能触发底层连接保活机制。Starlette 的 WebSocket 对象提供了 ping() 方法,它会真正发送 opcode=0x9 的帧。
实操建议:
- 直接调用
await websocket.ping()—— 这是最干净的心跳方式,不依赖消息解析逻辑 - 注意:
ping()不抛异常不代表客户端在线;它只表示“发出去了”,收不到 pong 不会报错 - 若需兼容老版本 Starlette(ping() 可能不存在,此时降级为发送文本
send_text("ping")并在客户端约定响应"pong" - 不要在
ping()后立刻await websocket.receive()等 pong —— 这会阻塞,且 pong 帧不会出现在 receive 队列中(Starlette 自动处理)
如何检测客户端已断线并清理资源?
仅发 ping 不足以判断断连。关键是在接收端设置超时,并捕获 WebSocketDisconnect 或底层异常。FastAPI 不提供“ping 超时回调”,必须手动维护状态 + 超时检查。
实操建议:
- 用字典或
WeakKeyDictionary记录每个websocket实例最后收到消息的时间(含 pong 响应或任意receive_*成功) - 心跳任务中检查该时间:若距今 > 45 秒,调用
await websocket.close(code=4001, reason="timeout")并从记录中移除 - 在主接收循环中(
while True: await websocket.receive_text())用try/except捕获WebSocketDisconnect和RuntimeError(常见于客户端强制关闭 socket) - 务必在
finally块中清理关联资源(如取消心跳 task、从连接池移除、释放数据库连接等)
为什么心跳间隔设为 30 秒却要容忍 45 秒断连?
这是为应对网络抖动与调度延迟留的余量。asyncio 任务调度不是实时的;WebSocket 底层 TCP 层可能重传;客户端处理 pong 也可能延迟。硬设“30 秒没 pong 就断”会导致误杀。
实操建议:
- 心跳间隔(ping 频率)设为 25–30 秒,超时阈值设为 1.5× 间隔(即 40–45 秒)
- 避免把超时阈值设得过长(如 120 秒)——这会让失效连接滞留太久,拖慢服务端连接数统计和内存释放
- 如果业务要求极低延迟断连(如实时协作编辑),可缩短到 10 秒心跳 + 20 秒超时,但需压测确认服务端并发能力
- 别忘了:Nginx / Cloudflare 等反向代理默认 60 秒 idle timeout,需同步调整其
proxy_read_timeout或timeout_idle
真正难的是状态一致性——心跳任务、接收循环、连接清理三者间没有原子协调机制,容易出现“task 还在跑但 websocket 已 close”的竞态。最稳妥的方式是用 asyncio.shield() 包裹清理逻辑,并在所有路径上统一用 websocket.client_state 判断是否仍可写。










