资讯详情

WebSocket断线重连实战:心跳机制与指数退避策略解析

📅 2026/10/20 22:15:15 | 华诺云谱 👁 阅读
WebSocket断线重连实战:心跳机制与指数退避策略解析
做过WebSocket相关项目的人大概都背过这样的锅——用户隔一段时间就来问“消息又没收到了是不是服务挂了”然后你一看日志发现连接早就断了客户端却还傻傻地以为通道在服务端也一直没察觉异常。我刚接手内部消息推送系统的那阵子几乎每个星期都要处理几次类似的反馈排查到最后几乎都指向同一个问题断线之后没人管或者管了但没管好。这篇文章就把我在Java服务端配合WebSocket做断线重连时踩过的坑、设计思路和最终落地代码整理出来。内容适合两类读者一是刚开始做推送、IM这类长连接需求的新手需要一套能直接跑到线上的方案二是已经被线上断线问题折磨了一阵子想系统梳理重连机制的老手。后文涉及的代码后端基于Java的javax.websocketJSR 356标准前端是原生JavaScript实现没有引入额外框架移植起来很方便。1. 为什么WebSocket连接会断先搞清楚断线的本质很多人一上来就直接写重连代码结果方案漏洞百出最基本的场景都覆盖不了。所以第一步先把断线这件事本身拆开看仔细。1.1 长连接为什么这么脆弱WebSocket本质上是一条常驻的TCP连接相比HTTP那种“请求一次就关闭”的模式WebSocket的优势是双向实时通信代价是这条连接必须一直活着。但现实网络环境对长连接相当不友好。我自己总结断线的高发场景主要有这么几类网络切换。手机从Wi-Fi切到4G/5G或者路由器重新拨号IP地址都会变化原来的TCP连接直接失效。这个在移动端项目里特别常见用户进了电梯没了信号出来以后连接已经回不来。中间设备的空闲回收。运营商NAT设备、公司防火墙、云上的负载均衡普遍会在一段时间没流量后回收连接。不同设备的回收时间差异很大有的两分钟有的五分钟有的甚至更短。服务端重启或发版。Java进程一重启所有会话都没了。这时候如果客户端不做自动重连用户就彻底掉线必须重新刷新页面或重启App才能恢复。客户端进程被系统挂起或回收。手机App切到后台系统可能会冻结网络连接浏览器标签页长时间后台运行也可能被浏览器主动节流或断开WebSocket。发送超时。如果一段时间内没有任何数据包某些网络节点就直接把连接干掉。所以一条长连接随时可能断这不是程序写得不好而是网络环境本身就不讲道理。你能做的是让系统在断线之后足够快地发现、足够快地恢复。1.2 断线之后客户端和服务端的“信息差”断线最麻烦的地方不是连接本身断了而是断了之后双方都不一定马上知道。服务端还好一点如果尝试向一个已经断开的客户端的Session推送消息会抛出异常或者发送的字节根本得不到确认这时候至少能发现问题。真正麻烦的是客户端那边手机信号掉了、Wi-Fi断了操作系统不会立刻告诉WebSocket层“你的连接挂了”在JS里你可能看到readyState还是1OPEN但网络实际已经断了好几分钟。反过来客户端关掉页面或AppJava服务端也不会立刻感知到TCP连接断开除非你主动往这个连接上写数据触发异常或者依赖TCP的保活机制。这个“信息差”拉得越久问题越大。比如用户以为消息通道是通的结果一条消息都没收到服务端看着连接池里还有这个用户但实际上根本推不过去重试几次再报错日志里全是异常堆栈。解决这个信息差的唯一办法就是引入心跳。客户端每隔一段时间发一个微小的报文服务端只要收到过心跳就认为这条连接还是正常的超过一定时间没收到就认定连接已经死亡主动清理。反过来客户端同样可以依赖心跳响应来判断服务端是否还活着——比如连续几次心跳都没有收到pong就认为通道异常主动发起重连。搞清楚了这个逻辑后面所有设计都有了依据。2. 重连方案怎么设计三件事缺一不可搞清楚了断线的本质就可以开始设计重连方案。当时我给系统定方案时把整个需求拆成了三块心跳检测、自动重连、重连后的状态恢复。这三块互为依赖只做任何一个都会留下漏洞。2.1 心跳机制让服务端判断连接是否存活先明确一个问题心跳到底由谁来发早期我见过有人让服务端定时往客户端发Ping客户端收到后回Pong。这样做的逻辑是服务端主动探测客户端。但在真实项目里我更推荐反过来——客户端定时往服务端发ping服务端收到后回pong或者干脆只记录收到心跳的时间不专门回应。原因有几个第一服务端要管理的连接数量多如果每条连接都由服务端定时发起Ping扫描任务和定时器的压力全在服务端。客户端少还好几百上千个连接时这个开销就不小了。让客户端主动发服务端只是一个被动的接收者逻辑简单得多。第二客户端自己发的ping如果发不出去通常会比较快地触发onerror或onclose事件这样前端能尽早感知断线而不是被动等服务端通知。第三从业务建模的角度客户端发ping本身也是一种“我在线”的声明服务端可以把最后一次心跳时间作为活跃状态判断依据非常自然。至于用协议自带的Ping/Pong控制帧还是用应用层JSON消息取决于你的技术栈。协议层的控制帧更轻量Java的javax.websocket里可以通过session.getBasicRemote().sendPing()发送。但有个现实问题浏览器端的WebSocket API不支持直接发送控制帧只能通过send()发送文本或二进制数据。所以为了前后端统一我在项目里用的就是应用层心跳客户端定时发送字符串ping服务端收到后回一个pong。心跳间隔怎么定这需要结合两个因素中间设备的空闲回收时间和业务场景的可接受断线感知延迟。如果间隔设得太长比如5分钟用户可能断线5分钟之后系统才发现对消息推送这种业务来说太慢了如果设得太短比如1秒连接多的时候心跳消息会产生大量额外流量和CPU消耗。我测试下来业务推送系统取30秒比较合适服务端超时阈值设为90秒扫描周期30秒。这个组合的含义是客户端每30秒发一次心跳服务端如果在90秒内没有收到任何数据包括心跳和业务消息就判定连接死亡并主动关闭。2.2 自动重连策略为什么指数退避是标配心跳让系统能“发现”断线接下来的问题是发现之后怎么恢复最简单的想法是断开后每隔固定时间比如1秒重连一次。这个方案实现简单但有一个致命的隐患。设想这个场景服务端凌晨发版Java进程重启。在重启的一分钟内所有在线的WebSocket连接全部断开。如果客户端用的是固定1秒重连那么服务端一恢复几千个客户端会在同一秒内同时发起握手请求。服务端刚启动CPU、数据库连接池、Spring容器都还在预热一下被几千个握手请求灌进来很容易直接被打挂。我亲身经历过一次服务端重启后大概过了30秒就出现连接拒绝那叫一个惨。所以业界标配是指数退避 随机抖动。指数退避的公式很简单delay min(baseDelay * 2^n, maxDelay)其中n是连续重连的失败次数baseDelay一般取1秒maxDelay取30秒。这样重连间隔就是1s、2s、4s、8s、16s、30s、30s……如果是网络偶发抖动第一次重连往往就能成功用户几乎感觉不到中断如果服务端长期不可用重连间隔会越来越大避免客户端和服务端互相折磨。但这还不够。指数退避只能让你的重连节奏逐渐放缓如果几百个客户端是同一个时间点开始断线的它们的退避曲线完全一样会在某一轮的同一个时间点同时重连。这时候需要在每次重连间隔上加上随机抖动业内常用的是把间隔随机乘以0.8~1.2这样每个客户端的重连时刻被摊开不会出现整齐划一的“重连峰”。还有个参数容易被忽略最大重连次数。有的客户端会设置无限重连这在某些场景下合理但如果服务端已经主动告知客户端“你的鉴权过期了”或者“服务端正在维护”还无限重连就没有意义了。我在前端代码里默认限制10次超过后彻底停掉并把错误抛给上层业务逻辑由用户手动刷新或重新登录。2.3 重连成功后的收尾鉴权、状态恢复与消息补偿很多人把重连想得太简单网络恢复连接自动建立消息就通了。实际上重连成功这个动作本身不等于业务恢复。WebSocket握手时通常需要带上鉴权信息比如token放在URL参数里或请求头里。重连时如果token已经过期服务端会拒绝握手或者建立连接后立刻关闭。这种情况不能无脑重试应该停下来提醒用户重新登录。连接重新建立之后客户端还需要恢复“订阅状态”。如果服务端的设计是每个连接默认接收自己userId的所有推送那不存在这个问题如果你的协议里客户端需要显式订阅某个频道或某个会话那么重连成功后要立刻重新发送订阅命令否则服务端不会往这条新连接上推消息。最后是断线期间的消息补偿。重连的瞬间服务端和客户端都不知道双方已经错过了哪些消息。一个实用的简化方案是每条消息附带一个递增的序号客户端在每次收到消息后记录最后一条的序号重连成功后把最后一个序号传给服务端服务端把该序号之后的消息重新推给客户端。当然如果业务允许丢弃断线期间的消息这步可以省略。但对IM、订单通知这类场景消息补偿是必须的。3. Java服务端落地会话管理、心跳扫描与推送处理到这一步设计层面的问题都清楚了接下来进入服务端实现。我后端用的是Java标准JSR 356也就是javax.websocket那套注解。核心就是ServerEndpoint加四个回调注解OnOpen、OnMessage、OnClose、OnError外加一个用于心跳扫描的定时任务。3.1 基于javax.websocket的核心会话管理先上一个最小可跑的骨架。注意一个关键点ServerEndpoint标注的类每个连接都会创建一个新的实例所以共享数据必须放在static字段里否则Session池和心跳记录在多个实例之间不互通。Component ServerEndpoint(/ws/{userId}) Slf4j public class WebSocketServer { private static final ConcurrentHashMapString, Session SESSION_POOL new ConcurrentHashMap(); private static final ConcurrentHashMapString, Long LAST_HEARTBEAT new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(userId) String userId) throws IOException { // 同一个userId重复登录先把旧连接踢掉 Session oldSession SESSION_POOL.get(userId); if (oldSession ! null oldSession.isOpen()) { oldSession.close(new CloseReason(CloseCodes.NORMAL_CLOSURE, duplicate login)); } SESSION_POOL.put(userId, session); LAST_HEARTBEAT.put(userId, System.currentTimeMillis()); log.info(连接建立 userId{}, 当前连接数{}, userId, SESSION_POOL.size()); } OnMessage public void onMessage(String message, Session session, PathParam(userId) String userId) { if (ping.equals(message)) { LAST_HEARTBEAT.put(userId, System.currentTimeMillis()); session.getAsyncRemote().sendText(pong); return; } // 业务消息处理走这里 handleBizMessage(userId, message); } OnClose public void onClose(PathParam(userId) String userId, Session session) { SESSION_POOL.remove(userId); LAST_HEARTBEAT.remove(userId); log.info(连接关闭 userId{}, 当前连接数{}, userId, SESSION_POOL.size()); } OnError public void onError(PathParam(userId) String userId, Session session, Throwable error) { log.error(连接异常 userId{}, userId, error); try { session.close(new CloseReason(CloseCodes.CLOSED_ABNORMALLY, error)); } catch (IOException e) { log.error(关闭异常连接失败, e); } } }几个容易踩的点说一下。第一踢旧连接这个逻辑必须有否则用户反复切换网络时客户端重连会出现同一个userId对应多个Session推送时可能会发到旧连接上旧连接其实已经收不到包了。第二SESSION_POOL和LAST_HEARTBEAT这两个Map必须同步更新否则就会出现“连接还在池子里但心跳时间不存在”的数据不一致。第三OnError里别光记日志要把Session主动关掉否则连接可能保持在半开状态不释放。3.2 定时扫描心跳线程的参数与实现有了心跳记录还得有个后台任务来扫描。我用一个单线程的ScheduledExecutorService就够了因为扫描操作本身不重。Component public class HeartbeatScanTask { private static final long HEARTBEAT_TIMEOUT 90 * 1000L; private final ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); PostConstruct public void start() { scheduler.scheduleAtFixedRate(this::scan, 30, 30, TimeUnit.SECONDS); } private void scan() { try { long now System.currentTimeMillis(); WebSocketServer.getSessionPool().forEach((userId, session) - { Long last WebSocketServer.getLastHeartbeat(userId); if (last ! null now - last HEARTBEAT_TIMEOUT) { log.warn(心跳超时关闭连接 userId{}, userId); try { session.close(new CloseReason(CloseCodes.CLOSED_ABNORMALLY, heartbeat timeout)); } catch (IOException e) { log.error(心跳超时关闭连接失败 userId{}, userId, e); } WebSocketServer.removeSession(userId); } }); } catch (Exception e) { log.error(心跳扫描任务异常, e); } } }这里有一个必须解释清楚的细节为什么心跳间隔30秒超时阈值却是90秒因为客户端可能因为事件循环卡顿暂时没发出心跳网络也可能有轻微波动导致某个心跳包丢了如果把阈值压得跟间隔一样紧很容易把正常连接误杀。留出3倍余量既不会让僵尸连接存活太久也不会误伤正常连接。如果业务对实时性更敏感可以收紧为间隔30秒、阈值60秒扫描周期调整到20秒左右。使用Executors.newSingleThreadScheduledExecutor时要注意线程池最好是服务级别的单例不要让每条WebSocket连接都new一个。心跳扫描任务里必须catch住所有异常否则定时任务一旦抛出未捕获异常scheduleAtFixedRate会终止后续所有执行那心跳清理就彻底失效了。3.3 主动消息推送时如何识别并清理失效连接心跳扫描能兜底但业务代码里主动推送也应该做检查。我给推送封装了一个静态方法public static boolean sendToUser(String userId, String payload) { Session session SESSION_POOL.get(userId); if (session null || !session.isOpen()) { return false; } try { session.getAsyncRemote().sendText(payload); return true; } catch (Exception e) { log.error(推送失败 userId{}, userId, e); SESSION_POOL.remove(userId); LAST_HEARTBEAT.remove(userId); return false; } }注意这里有个异步和同步的选择问题。session.getBasicRemote().sendText()是同步发送如果客户端接收慢服务端的发送线程容易被阻塞影响其他连接的推送getAsyncRemote()把发送交给IO线程去处理不会阻塞当前业务线程。在推送频率不太高、单条消息较大的场景异步发送更合适。另一个细节是异步sendText返回成功后并不代表对方一定收到了它只代表数据已经交给了底层Socket。TCP不保证对端应用一定能立即读到但对这个业务场景来说只要能成功交给底层协议栈就已经尽到了推送义务。最后提一句如果你用的是Spring WebSocket的TextWebSocketHandler思路完全一样只是回调方法从注解换成了重写接口方法会话管理的逻辑可以直接复用。4. 前端重连模块JS端完整实现与边界场景重连这事光靠后端可不行前端不配合服务端做得再好也白搭。我见过不少项目后端把Session管理、心跳扫描都做得漂漂亮亮结果前端WebSocket对象是裸连的断线后没有任何处理逻辑用户只能靠刷新页面。所以前端这块我按生产要求写了一个可直接使用的封装。4.1 一个可直接使用的WebSocket客户端封装下面是原生JavaScript的WebSocket客户端类包含心跳、指数退避重连、连接状态管理。没有引入任何框架放到任意页面都能直接用。class ReconnectWebSocket { constructor(url, options {}) { this.url url; this.baseDelay options.baseDelay || 1000; this.maxDelay options.maxDelay || 30000; this.maxReconnectAttempts options.maxReconnectAttempts || 10; this.heartbeatInterval options.heartbeatInterval || 30000; this.onMessage options.onMessage || function () {}; this.onReconnect options.onReconnect || function () {}; this.ws null; this.reconnectAttempts 0; this.manualClosed false; this.heartbeatTimer null; this.reconnectTimer null; this.connect(); } connect() { this.ws new WebSocket(this.url); this.ws.onopen () { this.reconnectAttempts 0; this.startHeartbeat(); this.onReconnect(); }; this.ws.onmessage (event) { // 约定心跳响应是纯文本 pong业务消息走JSON if (event.data pong) return; this.onMessage(event.data); }; this.ws.onclose () { this.stopHeartbeat(); if (!this.manualClosed) { this.scheduleReconnect(); } }; this.ws.onerror (error) { // onerror 之后通常会紧接着触发 onclose重连逻辑放 onclose 里 console.error(WebSocket连接异常, error); }; } scheduleReconnect() { if (this.reconnectAttempts this.maxReconnectAttempts) { console.error(重连次数达到上限停止重连); return; } let delay this.baseDelay * Math.pow(2, this.reconnectAttempts); delay Math.min(delay, this.maxDelay); // 随机抖动防止所有客户端在同一时刻重连 delay * 0.8 Math.random() * 0.4; console.log(第 ${this.reconnectAttempts 1} 次重连将在 ${Math.round(delay / 1000)} 秒后发起); this.reconnectTimer setTimeout(() { this.reconnectAttempts; this.connect(); }, delay); } startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer setInterval(() { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(ping); } }, this.heartbeatInterval); } stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer null; } } send(data) { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(data); } else { console.warn(连接未就绪消息发送失败, data); } } close() { this.manualClosed true; this.stopHeartbeat(); if (this.reconnectTimer) clearTimeout(this.reconnectTimer); if (this.ws) this.ws.close(); } }这个封装的核心逻辑onopen时重置重连次数并启动心跳onclose时如果非手动关闭就安排重连重连间隔按指数退避加抖动计算手动关闭时通过manualClosed标志阻止自动重连。用起来就是let client new ReconnectWebSocket(wss://your-domain/ws/user123, { onMessage: function (msg) { console.log(收到消息, msg); }, onReconnect: function () { console.log(重连成功新连接已建立); // 在这里重新鉴权或重新订阅 } });有个细节要提醒onerror里面不要写重连逻辑。标准WebSocket的onerror触发后浏览器会自动走到onclose你在onclose里做重连就行。如果两边都写重连逻辑会出现重连请求发两次的问题。4.2 手动关闭、网络切换、Token失效这几个坑怎么避上一节的封装考虑到了日常断线但还有几个边界场景如果不处理照样出问题。手动关闭。用户退出登录、或者页面主动销毁时一定要调用client.close()把manualClosed置为true否则会出现用户明明已经退出但客户端还在后台拼命重连的情况。这个其实很常见尤其是单页应用里切页面时没有正确销毁WebSocket实例结果同一个浏览器环境里堆积了多个重连循环。移动端网络切换。手机从Wi-Fi切换到移动网络时WebSocket连接会断开但onclose可能延迟几秒才会触发。为了更快感知网络变化可以监听window的online事件在变成online时主动检查连接状态如果没有处于OPEN就立即尝试重连。需要注意online事件在部分iOS版本上触发时机不稳定所以只能作为辅助手段不能完全替代心跳。Token失效。WebSocket握手失败或连接建立后被服务端关闭前端需要区分关闭原因再做重连决策。比如服务端鉴权失败时返回一个自定义关闭码4001前端收到后就知道不是网络问题而是需要重新登录。这时候客户端要停止自动重连弹出登录提示。如果不管这个场景会陷入“重连-鉴权失败-再重连”的死循环。页面可见性。浏览器标签页切到后台后JS定时器会被大幅节流心跳可能不准时发送。好在真正切回前台时连接大概率已经断掉onclose会触发重连。经验做法是在visibilitychange事件里如果页面变为可见且连接不在OPEN状态立即手动触发一次重连而不是干等退避计时器走完。4.3 断线期间的离线消息补偿一个简化的序号方案前面提到消息补偿这里给出一个具体的简化实现。约定服务端给每个用户维护一个lastSeq每条推送消息都带一个seq。客户端在内存里记录lastReceivedSeq每次收到消息后更新。重连成功后客户端把lastReceivedSeq通过新建的连接发给服务端服务端取出这个用户的消息队列里大于lastReceivedSeq的记录重新推给客户端。client.send(JSON.stringify({ type: reconnect, lastSeq: this.lastReceivedSeq }));服务端收到后if (reconnect.equals(type)) { int lastSeq json.getIntValue(lastSeq); ListPushMessage missed messageStore.fetchAfter(userId, lastSeq); missed.forEach(msg - sendToUser(userId, serialize(msg))); }这个方案虽然简单但足以覆盖大多数业务场景。需要注意两点一是服务端要把用户的最新序号持久化不能只放在内存里否则连接断开期间服务端进程重启序号就丢了二是消息队列里要设置一个容量上限比如最多保留最近1000条超出的直接丢弃避免无限占用内存。5. 上线后的真实踩坑记录排查思路与避坑清单方案设计完了代码也上线了但真正的问题总是在上线之后才浮出水面。我把实际项目里碰到过的几个问题记录在这里这些问题在文档和教程里基本看不到但遇到过一次就一辈子忘不掉。5.1 重连风暴把服务打挂的那个晚上第一次上线自动重连后的第二天凌晨服务端例行发版重启完成后的几分钟内CPU和内存同时飙升监控告警响个不停。排查下来就是前文说的那个问题所有客户端在断线后按同样的退避曲线同时发起重连几千个握手请求挤在同一秒到达。解决办法有三个层次。第一层客户端加随机抖动这个上文已经说了。第二层服务端发版时尽量滚动重启一个节点一个节点来避免所有连接同时断。第三层接入层加一个简单的限流——同一userId的握手请求在短时间内只允许一次其他直接拒绝让客户端走退避逻辑。这三层都做了之后重连风暴就再也没出现过。顺便说一句排查这类问题有个习惯很管用连接数要按时间维度画成监控曲线进程重启后曲线是什么形状一眼就能看出来。如果没有监控全靠出问题时人工排查效率太低了。5.2 服务端“僵尸连接”占满了线程池还有一次线上事故让我印象很深。某天下午突然收到大量推送延迟的告警Java进程里线程数异常偏高。当时第一反应是消息量增加了但看了半天流量并没有明显上涨。后来用线程转储抓线程栈发现大量线程阻塞在Socket的输出流写入上再一查这些连接对应的客户端早就不在了。原因很简单客户端App进程被系统杀掉但TCP连接没有发送结束报文服务端的Socket一直处于半开状态。业务推送调用同步sendText时数据发到内核缓冲区收不到确认就反复重发发送线程被长时间阻塞。僵尸连接越多线程池被占满得越快。这个问题的根治方案就是前面说的心跳扫描。心跳扫描配合发送超时控制把长时间无心跳的半开连接及时清理掉。从那以后我把异步发送和心跳扫描真正重视起来每天上线前也会检查连接池里的连接数量和系统实际TCP连接数是否一致不一致就说明有连接泄漏的隐患。5.3 心跳和业务消息的竞争问题还有一个容易忽视的坑如果连接长时间只有心跳消息业务消息的“最后到达时间”会被心跳的到达时间掩盖。比如你以收到任何消息来判断连接活跃那么一条连接即使业务完全没消息也会因为心跳而一直保持“活跃”。这本不是问题但如果你的离线判断逻辑写错了地方就会出现误判。我遇到的具体情况是服务端给用户A推送业务消息时A已经断线好几分钟了但系统因为“最后活跃时间”被心跳刷新过误以为他还在线于是消息进了在线推送通道而不是离线存储通道导致用户重连后拿不到这条消息。正确的做法是把两种时间分开LastHeartbeatTime用来判断连接是否存活LastBizMessageTime用来判断用户是否在线、决定消息走在线推送还是离线存储。别图省事用一个字段覆盖两个语义我那次就是踩了这个坑排查了整整一天。5.4 常见问题速查表最后把上面这些经验和一些零散的注意点整理成一张速查表方便你现在就对照核对自己的项目。症状可能原因解决方案客户端频繁重连但总是失败服务端拒绝握手或鉴权不通过检查握手参数、token有效期失败时停止自动重连提示重新登录服务端CPU在重启后瞬间飙高大量客户端按同一时间点同时重连客户端增加随机抖动服务端滚动重启接入层限流服务端连接数缓慢上涨删不掉客户端进程被杀TCP半开连接未被清理心跳扫描定期关闭超时连接推送失败时立即清理Session用户重连后收不到断线期间的消息断线期间消息走了在线通道引入消息序号按lastSeq补偿离线消息重连成功后业务状态丢失订阅或鉴权状态未恢复重连成功回调里重新执行订阅、鉴权逻辑页面切换后出现多个重连循环WebSocket实例未正确销毁页面销毁时调用close()并清理定时器这张表里的每一条都是我在真实项目里碰到过或者帮同事排查过的。其中“连接数缓慢上涨”那条特别容易被人忽略因为不等到线程池被打满你根本意识不到问题有多严重。写到这里断线重连这套东西的基本盘算是讲完了。如果只挑一句最重要的说我想说重连不是前端加一个定时器就完事也不是后端加一个心跳扫描就万事大吉它是一个需要前后端一起设计、一起对参数的完整闭环。我最初那版方案就是只做了前端重连结果服务端堆了一堆僵尸连接后来才补上心跳和清理机制。如果你现在正要给项目加断线重连建议按这个顺序动手先把服务端的心跳扫描和会话清理做扎实再写前端重连模块最后补消息补偿。每加一块都跑一遍断网-恢复的模拟测试你会发现这东西比想象中磨人但也真的值得做好。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑