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