绝大多数websocket频繁断开是某层主动切断,需通过onclose.code定位:1006为tcp层异常(如代理rst、nat超时),1001或“going away”表明服务端主动关闭,4001多因鉴权失败;抓包确认fin/rst来源,重点检查nginx代理配置与心跳保活契约。

WebSocket连接频繁断开,**绝大多数情况不是代码写错了,而是连接在某一层被主动切断了**——你要做的不是重写逻辑,而是定位“谁下的手”、以及“为什么下手”。
看客户端 onclose 的 code 和 reason
这是最直接的线索。别只盯着“断开了”,重点看关闭时带的元信息:
-
code === 1006:几乎可以锁定是 TCP 层异常中断(比如代理发了 RST、NAT 超时、防火墙静默丢包),服务端和客户端都可能没收到通知 -
code === 1001或 reason 含"going away"、"shutdown":服务端主动关闭,查服务日志里是否有ws disconnect reason: heartbeat timeout或read timeout -
code === 4001或 reason 含"auth failed"、"duplicate connection":协议层校验失败,常见于 token 过期、重复登录踢人逻辑触发 - 控制台没打印
onerror,但高频触发onclose:大概率是中间件(Nginx/ALB/SLB)在空闲后强制断连,而不是你的代码抛异常
抓包确认 FIN/RST 是谁发的
用 wireshark 或 tshark 抓客户端和服务端之间的流量,过滤 tcp.port == your_ws_port,关注最后几个包:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 如果 FIN 或 RST 包源 IP 是你服务器的公网 IP → 服务端或内核主动断开(检查
net.ipv4.tcp_keepalive_time、Worker 进程是否崩溃) - 如果 FIN/RST 源 IP 是 Nginx 所在机器 → 立即检查 Nginx 配置:
proxy_read_timeout是否小于心跳间隔 ×2,proxy_set_header Upgrade是否透传 - 如果根本抓不到 FIN,只有突然中断的 TCP 重传 → 典型 NAT 超时或移动网络切换(如 iOS Safari 切后台、Wi-Fi 切 4G)
检查 Nginx 是否在“假装代理”,实则默默断连
Nginx 默认不认 WebSocket,必须显式配置才能透传升级请求和维持长连接。漏掉任意一项,它就会把 WebSocket 当普通 HTTP 处理,60 秒无数据就 kill:
- 必须有
proxy_http_version 1.1 - 必须透传两个 header:
proxy_set_header Upgrade $http_upgrade和proxy_set_header Connection "upgrade" -
proxy_read_timeout必须 ≥ 心跳间隔 ×2 + 网络抖动缓冲(例如心跳 30s,这里至少设90) - 别信“我配了 upgrade 就够了”——少一个分号、多一个空格、变量名写成
$upgrade(错)而非$http_upgrade(对),都会让透传失效
心跳不是可选功能,是保活契约
WebSocket 协议本身不保活,ping/pong 帧是唯一被标准支持的保活机制。但要注意:
- 客户端发
ping,服务端必须回pong;反过来也一样。单向心跳无效 - Spring Boot 2.6+ 中
WebSocketSession.setHeartbeat()已废弃,必须手动用ScheduledExecutorService发送 - 浏览器原生
WebSocket对象不暴露 ping/pong 控制权,只能发自定义消息(如{"type":"ping"}),服务端需识别并响应,否则中间件仍会超时 - 移动端切后台时,浏览器可能冻结定时器——要监听
visibilitychange,页面可见时立刻补发心跳或重连
真正难排查的,永远是那个“没报错、没日志、没 FIN,但就是连不上”的瞬间——它往往藏在 NAT 设备的超时表里,或者 iOS 系统的后台节电策略中。先盯死 onclose.code 和抓包方向,比反复改重连逻辑管用得多。










