跳转到主内容
极星编程网:以代码为星,赴技术山海!

fastapi 如何实现 WebSocket 心跳与断线检测

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 超时回调”,必须手动维护状态 + 超时检查。 Comet AI 浏览器 下载 Comet AI 浏览器,体验由 Perplexity AI 驱动的革命性上网方式。内置 AI 助手可实时总结网页、跨标签页对比信息、自动执行任务。告别繁琐操作,让 AI 成为你的浏览副驾,大幅提升研究与工作效率。支持 Windows、macOS、Android 和 iOS。 下载 实操建议: 用字典或
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
判断是否仍可写。

相关文章