websocket连接需客户端主动心跳探测与指数退避重连,服务端须用redis维护用户fd映射并双重校验,且心跳、nginx超时、服务端空闲时间参数必须对齐。

当用户在聊天页面突然看到“连接已断开”,而服务端日志却一片平静,说明WebSocket连接已在无声中失效——这不是服务崩溃,而是长连接被NAT、LB或浏览器后台策略悄悄掐断。你必须让客户端主动探测、判断、重连,而不是等待用户手动刷新。
客户端检测连接是否真正存活
仅监听 onclose 不够:网络抖动可能触发短暂断开又自动恢复,但 onclose 已被触发;也有些断开根本不会触发该事件(如中间设备静默丢包)。必须叠加心跳验证。
第一步:在 WebSocket 实例上挂载心跳定时器,每 30 秒发送一次 {"type":"ping"} 消息。
第二步:服务端收到后必须立即回传 {"type":"pong"},不能走业务逻辑队列,否则延迟会误判为断连。
第三步:客户端维护一个 【lastPongTime】 时间戳,每次收到 pong 就更新;若距离上次 pong 超过 45 秒,且 socket.readyState === WebSocket.OPEN,则强制调用 socket.close(1006, "heartbeat timeout") —— 这会真实触发 onclose,避免假在线。
前端实现指数退避重连
方法一:基础重连循环(适用于低并发内部系统)
监听 onclose 后,用 setTimeout 延迟重连,每次失败后延迟翻倍,上限设为 30 秒。
方法二:带互斥锁的防雪崩重连(生产环境必选)
声明一个全局变量 let isReconnecting = false;进入重连前先检查该值,为 【true】 则直接 return;成功连接后置为 false;失败时仍保持 true 直到本次定时器结束。
方法三:随机抖动 + 最大重试次数限制
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
每次重连延迟 = Math.min(30000, Math.pow(2, retryCount) * 1000 + Math.random() * 1000);同时设置 maxRetry = 5,达到后弹出提示并停止自动重连。
服务端配合:去重、映射、校验
用户快速刷新页面时,旧连接尚未触发 onClose,新连接已建立,服务端会收到两个相同 user_id 的 onOpen。若不做处理,后续消息可能发错 fd,甚至导致账号被踢下线。
第一步:客户端首次连接成功后,必须携带有效 token 发送登录请求,服务端解析出 user_id。
第二步:服务端在 onOpen 中执行 redis->hSet('user_fd_map', $user_id, $fd),并记录当前时间戳到 user_last_active。
第三步:在 onClose 中立刻执行 redis->hDel('user_fd_map', $user_id);【这一步不可省略,否则重连后推送会发到已失效的 fd】。
第四步:每次向指定用户推送前,先 redis->hGet('user_fd_map', $user_id) 取 fd,再调用 $server->exist($fd) 确认连接有效,双重校验缺一不可。
心跳与空闲超时参数对齐
Nginx 默认 proxy_read_timeout 60s,而你的 WebSocket 心跳间隔设为 45s,服务端 heartbeat_idle_time 设为 90s——这组数值存在断裂风险:Nginx 在第 60 秒切断连接,但服务端要等到第 90 秒才判定空闲,中间 30 秒连接已死却未通知客户端。
必须统一为:客户端心跳间隔 ≤ Nginx proxy_read_timeout × 0.6,且 heartbeat_idle_time = proxy_read_timeout + 10。
例如 Nginx 设置 proxy_read_timeout 300,则客户端心跳发包间隔设为 180s,Swoole 配置 'heartbeat_idle_time' => 310。
PHP-FPM 模式下还需禁用脚本超时:ini_set('max_execution_time', '0');,否则 worker 进程可能被回收,导致连接意外中断。










