WebSocket 为什么老是断开?
更新时间:2026-09-18。问题驱动短答页。WebSocket「连上后过一会儿就断」绝大多数不是协议问题,而是链路中间的空闲超时和没有心跳/重连。
一句话回答
TCP 长连接在长时间没有数据时,会被 Nginx/网关/负载均衡/运营商 NAT 当作空闲连接回收;移动端还会因切网断开。解决办法是应用层定时心跳(ping/pong)把间隔设到代理超时的三分之一以内,并实现指数退避自动重连 + 重新认证/重订阅。
常见断因
| 断因 | 表现 | 对策 |
|---|---|---|
| 网关/代理空闲超时 | 空闲 60s 后规律断开 | 心跳间隔 < proxy_read_timeout 的 1/3 |
| NAT/防火墙回收 | 移动网络、公司网络几分钟无流量被断 | 心跳保活,间隔通常 20~30s |
| 服务端重启/发版/LB 摘节点 | 部署时集中掉线 | 优雅关闭通知 + 客户端重连 |
| 移动网络切换/休眠 | WiFi↔4G、锁屏后断 | 监听 online 事件,唤醒后重连 |
| 半开连接 | 对端已挂,本端却以为在线,发消息才报错 | 心跳探活,pong 超时即判死重连 |
| 鉴权过期 | 连接被服务端按 token 失效关闭(如 1008) | 重连前刷新 token |
关键点:TCP 不会主动告诉你「对端没了」,不发心跳就可能持有一个半开连接,直到写入失败才暴露。
心跳怎么设
bash
代理/网关 idle timeout = 60s(常见默认)
→ 客户端每 20s 发一次 ping(取超时的 1/3 或更短)
→ 服务端回 pong;连续 N 次(如 2~3 次)收不到 pong
→ 判定连接已死,主动关闭并重连js
// 客户端思路(以浏览器 WebSocket 为例)
let timer;
ws.onopen = () => {
timer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({type:'ping'}));
}, 20000); // 小于网关 idle timeout
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'pong') markAlive();
};服务端(Nginx)要相应调大/对齐超时:
nginx
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s; # 必须大于心跳间隔
}重连要点
- 指数退避:1s→2s→4s…加上随机抖动并封顶(如 30s),避免服务刚恢复时大量客户端同时重连形成「重连风暴」。
- 重连后恢复状态:重新带 token 握手、重新订阅频道、用序号/游标补拉断线期间错过的消息。
- 服务端发版时先发「即将关闭」帧(close code 1001),让客户端平滑切到其他节点。
坑
- 只在客户端发心跳、服务端不做 pong 超时检测:对端网络已断而服务端不知道,仍往死连接写。
- 心跳间隔设成等于代理超时:临界抖动必然踩线,务必留足余量(1/3)。
- 重连不退避:服务重启后 N 万连接同时重连,可能二次击垮服务。
- 重连不补消息:断线期间的消息丢失,需要序号机制或走 SSE/拉取兜底。
完整心跳状态机、退避重连与消息补偿代码见 WebSocket 心跳保活与断线重连设计;只需服务端单向推送时更省的方案见 SSE 与 WebSocket 怎么选。
总结
WebSocket 老断开的根因基本是长连接空闲后被 Nginx、负载均衡或运营商 NAT 按 idle timeout 回收,叠加移动切网、服务发版和无心跳导致的半开连接;解决靠应用层 ping/pong 心跳(间隔设为代理 proxy_read_timeout 的三分之一以内,连续收不到 pong 即判死)、Nginx 对齐超时,再加指数退避+随机抖动的自动重连,重连后重新认证、重订阅并用消息序号补拉断线期间数据,同时让服务端发版前发关闭帧引导平滑切换。