IM聊天不掉线?生产级WebSocket断线重连、心跳保活实战

简介: 本文将给出一套生产级Web端即时通信方案,包括心跳检测保活 + 指数退避重连,代码直接能跑。

本文作者老前端的功夫,有修订和改动。

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 = newMap()        // 事件监听器

   this.connect()

 }

 

 // 建立连接

 connect() {

   this.ws = newWebSocket(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 = typeofdata === '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, newSet())

   }

   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 = newReconnectingWebSocket('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长连接有效性的文章:

关于移动网络复杂性问题的资料:

关于移动弱网问题、优化方案等:

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,断网时暂停重连,网络恢复时立即重连;
  • 3)token过期处理:重连后鉴权可能失败,需在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 )

相关文章
|
23天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
9365 30
|
9天前
|
存储 人工智能 并行计算
大模型本地部署终端选型方法论:以 Qwen3.8-27B 为例的四档分层完整流程
本文提出一套大模型本地部署终端选型方法论:定约束、定档位、定框架、定参数四步决策法,配合入门、主力、质量、无损四档分层模型。以 Qwen3.8-27B 实测数据为例,逐环节解读显存、带宽、存储、散热、系统、预算等要素,给出面向不同预算的优选方案、决策自查清单与市场观察框架。文末前瞻 AI 笔记本的 CPU+GPU 与统一内存两条路线,论证四步决策法在新品类上的延续性。
|
8天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
638 1
|
21天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2478 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
4天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型全解析:核心原理拆解,性能实战测评与落地部署教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为System One Model(系统一模型),对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
413 0
|
3天前
|
存储 缓存 测试技术
QWT 3D 模块重构记:旧渲染管线改进介绍及实时瀑布图示例
QWT 3D模块完成重大重构:解耦窗口与图形(Plot+Item),摒弃OpenGL 1.x固定管线,迁至Core Profile着色器渲染;支持多图元同场景叠加,并以实时3D瀑布图验证——滑动窗口、固定色域、64帧×256点达61fps。兼容Qt 5.12+/6,LGPL协议。
157 120
|
15天前
|
Windows
【保姆级】.NET Framework 3.5离线安装教程(2026最新实测)
.NET Framework 3.5是微软经典运行库,专为兼容老软件、老游戏设计。Win7自带,Win10/11需手动启用(在线或离线安装)。它与4.x系列并存不冲突,缺它会导致“找不到组件”报错。本文详解两种安装方式及常见问题解决,小白也能轻松上手。(239字)
1874 0
|
10天前
|
人工智能 Linux Windows
千问办公(QwenWork)官网入口:其实有2个,一个是网页端千问办公,一个是介绍指南页面
千问办公(QwenWork)是阿里云推出的AI智能办公平台,支持网页端直接使用及Windows/Mac/Linux客户端下载。提供PPT生成、财报分析、网页搭建等AI功能,个人版免费,企业版198元/席/月。详情见官网qwenwork.cn或阿里云产品页。
1194 0
千问办公(QwenWork)官网入口:其实有2个,一个是网页端千问办公,一个是介绍指南页面