恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
IM聊天不掉线?生产级WebSocket断线重连、心跳保活实战
首页
资讯中心
/
IM聊天不掉线?生产级WebSocket断线重连、心跳保活实战
IM聊天不掉线?生产级WebSocket断线重连、心跳保活实战
发布时间:2026/10/15 19:21:39
本文作者老前端的功夫有修订和改动。1、引言聊天聊的正嗨突然IM消息发不出去一看控制台——WebSocket disconnected。网络抖动、切换WiFi、电脑休眠都可能触发断线。如果不做掉线重连和心跳保活用户只能手动刷新页面体验极差。本文将给出一套生产级Web端即时通信方案包括心跳检测保活 指数退避重连代码直接能跑。2、技术思路主要思路如下核心要解决三个问题1怎么知道连接断了心跳超时 浏览器onclose事件2断了怎么重连指数退避避免疯狂重试压垮服务器3重连后状态怎么恢复重新鉴权、重新加入房间3、实现代码class ReconnectingWebSocket { constructor(url, options {}) { this.url url this.options { heartbeatInterval: 30000, // 心跳间隔 30秒 heartbeatTimeout: 10000, // 心跳超时 10秒 reconnectDelay: 1000, // 初始重连延迟 1秒 maxReconnectDelay: 30000, // 最大重连延迟 30秒 reconnectDecay: 1.5, // 退避指数 maxReconnectAttempts: Infinity, // 最大重试次数 ...options } this.ws null this.reconnectAttempts 0 this.heartbeatTimer null this.heartbeatTimeoutTimer null this.manualClose false // 是否手动关闭手动关不重连 this.messageQueue [] // 重连期间的离线消息队列 this.listeners new Map() // 事件监听器 this.connect() } // 建立连接 connect() { this.ws new WebSocket(this.url) this.ws.onopen (event) { console.log(WebSocket 连接成功) this.reconnectAttempts 0 this.startHeartbeat() this.flushMessageQueue() // 重连成功后发送队列中的消息 this.emit(open, event) } this.ws.onmessage (event) { // 收到任何消息都重置心跳超时计时器 this.resetHeartbeatTimeout() this.emit(message, event.data) } this.ws.onclose (event) { console.warn(WebSocket 连接关闭, event.code, event.reason) this.stopHeartbeat() if (!this.manualClose) { this.scheduleReconnect() } this.emit(close, event) } this.ws.onerror (error) { console.error(WebSocket 错误, error) this.emit(error, error) // onerror之后通常会触发onclose所以这里不额外处理重连 } } // 启动心跳 startHeartbeat() { this.stopHeartbeat() // 清理旧定时器 this.heartbeatTimer setInterval(() { if (this.ws.readyState WebSocket.OPEN) { // 发送心跳包格式和后端约定好 this.ws.send(JSON.stringify({ type: PING })) // 设置心跳超时定时器 this.heartbeatTimeoutTimer setTimeout(() { console.warn(心跳超时主动断开连接) this.ws.close() // 触发onclose进而触发重连 }, this.options.heartbeatTimeout) } }, this.options.heartbeatInterval) } // 重置心跳超时收到消息时调用 resetHeartbeatTimeout() { if (this.heartbeatTimeoutTimer) { clearTimeout(this.heartbeatTimeoutTimer) this.heartbeatTimeoutTimer null } } // 停止心跳 stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer) this.heartbeatTimer null } this.resetHeartbeatTimeout() } // 指数退避重连调度 scheduleReconnect() { if (this.reconnectAttempts this.options.maxReconnectAttempts) { console.error(已达最大重连次数停止重连) this.emit(reconnectFailed) return } const delay Math.min( this.options.reconnectDelay * Math.pow(this.options.reconnectDecay, this.reconnectAttempts), this.options.maxReconnectDelay ) console.log(${delay}ms 后尝试第 ${this.reconnectAttempts 1} 次重连) setTimeout(() { if (!this.manualClose) { this.reconnectAttempts this.emit(reconnecting, this.reconnectAttempts) this.connect() } }, delay) } // 发送消息支持离线队列 send(data) { const message typeof data string ? data : JSON.stringify(data) if (this.ws.readyState WebSocket.OPEN) { this.ws.send(message) } else { console.warn(连接未就绪消息进入队列) this.messageQueue.push(message) } } // 连接恢复后发送队列中的消息 flushMessageQueue() { while (this.messageQueue.length 0) { const msg this.messageQueue.shift() if (this.ws.readyState WebSocket.OPEN) { this.ws.send(msg) } } } // 手动关闭不再重连 close() { this.manualClose true this.stopHeartbeat() this.ws.close() } // 事件监听系统 on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()) } this.listeners.get(event).add(callback) } off(event, callback) { this.listeners.get(event)?.delete(callback) } emit(event, ...args) { this.listeners.get(event)?.forEach(cb cb(...args)) } } // 使用示例 const ws new ReconnectingWebSocket(wss://chat.example.com/ws, { heartbeatInterval: 20000, maxReconnectAttempts: 10 }) ws.on(open, () { console.log(连接建立发送鉴权信息) ws.send({ type: AUTH, token: localStorage.getItem(token) }) }) ws.on(message, (data) { const msg JSON.parse(data) if (msg.type PONG) { console.log(收到心跳响应) } else { // 处理业务消息 handleChatMessage(msg) } }) ws.on(reconnecting, (attempt) { showToast(网络不稳定正在重连... (${attempt}/10)) }) ws.on(reconnectFailed, () { showToast(连接失败请检查网络后刷新页面) })4、为什么要双向心跳很多文章只讲前端定时发PING忽略了后端也要发PONG。如果后端不响应心跳前端无法区分“连接正常”和“连接已断但TCP还未感知”。代码中设置了heartbeatTimeout超时未收到任何消息包括PONG就主动断开触发重连。关于TCP长连接有效性的文章《为何基于TCP协议的移动端IM仍然需要心跳保活机制》《彻底搞懂TCP协议层的KeepAlive保活机制》关于移动网络复杂性问题的资料《为什么WiFi信号差一文即懂》《上网卡顿网络掉线一文即懂》《为什么手机信号差一文即懂》《高铁上无线上网有多难一文即懂》关于移动弱网问题、优化方案等《现代移动端网络短连接的优化手段总结请求速度、弱网适应、安全保障》《移动端IM开发者必读(一)通俗易懂理解移动网络的“弱”和“慢”》《移动端IM开发者必读(二)史上最全移动弱网络优化方法总结》《百度APP移动端网络深度优化实践分享(三)移动端弱网优化篇》5、为什么要指数退避网络抖动时如果立即重连很可能再次失败。指数退避1s → 1.5s → 2.25s...既能快速恢复又能避免重连风暴打挂服务器。公式delay * decay^attempts上限 maxReconnectDelay。6、离线消息队列的作用重连期间用户可能还在发消息放入队列等onopen触发后批量发送。注意要做去重或幂等处理避免消息重复。7、手动关闭与自动重连的区分用户主动退出聊天室时不应该再重连。用manualClose标志区分调用close()方法关闭连接且不再重连。8、与后端配合的约定前后端需要约定好心跳消息格式例如//前端发{ type: PING}//后端回{ type: PONG}后端也应有超时踢人机制例如60秒未收到PING则主动断开该连接。9、生产环境补充优化主要有以下几点1页面可见性API页面切后台时降低心跳频率或暂停心跳切回来立即检查连接状态2网络状态监听结合navigator.onLine断网时暂停重连网络恢复时立即重连3token过期处理重连后鉴权可能失败需在onmessage中处理401并引导重新登录。// 页面可见性优化示例 document.addEventListener(visibilitychange, () { if(document.hidden) { ws.options.heartbeatInterval 60000 // 后台降频 } else{ ws.options.heartbeatInterval 20000 if(ws.ws.readyState ! WebSocket.OPEN) { ws.connect() // 立即重连 } } })这套方案已在多个IM项目中稳定运行断线重连成功率99%以上。复制代码到项目里根据业务微调参数即可。10、参考资料[1]零基础IM开发入门(二)什么是IM聊天系统的实时性[2]冰山之下一次网络请求背后的技术秘密[3]为何基于TCP协议的移动端IM仍然需要心跳保活机制[4]跟着源码学IM(五)正确理解IM长连接、心跳及重连机制并动手实现[5]彻底搞懂TCP协议层的KeepAlive保活机制[6]能Ping通TCP就一定能连接和通信吗[7]网络ping不通到底有多少原因一文搞明白[8]关闭TCP连接时为什么会TIME_WAIT、CLOSE_WAIT[9]深入研究分析TCP的异常关闭[10]通俗易懂理解移动网络的“弱”和“慢”[11]为什么WiFi信号差一文即懂[12]上网卡顿网络掉线一文即懂[13]为什么手机信号差一文即懂[14]高铁上无线上网有多难一文即懂[15]Web端即时通讯实践干货如何让你的WebSocket断网重连更快速[16]WebSocket从入门到精通半小时就够[17]都HTML5了Web端即时通讯技术到底该用什么一文即懂即时通讯技术学习- 移动端IM开发入门文章《新手入门一篇就够从零开发移动端IM》- 开源IM框架源码https://github.com/JackJiang2011/MobileIMSDK备用地址点此本文同步发布于http://www.52im.net/thread-4924-1-1.html