ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WebSocket实战指南:从浏览器API到心跳重连与避坑手册

WebSocket实战指南:从浏览器API到心跳重连与避坑手册 WebSocket 这个技术我前后用了一年多从最初在网页上做实时聊天到后台上万台设备的监控大屏再到物联网传感器的数据推送可以说把能踩的坑基本踩了一遍。今天这篇就把我对 WebSocket 的理解、浏览器端的完整用法、以及封装过程中的细节一次性说透内容偏实战有需要可以直接收藏。1. WebSocket 到底是什么为什么它敢叫“实时黑科技”1.1 从 HTTP 的“一问一答”说起要理解 WebSocket 的价值先得回头看 HTTP 协议的设计模型。传统 HTTP 是严格的请求-响应模式浏览器发一个请求服务器回一个响应然后这个连接基本就结束了。哪怕是在 HTTP/1.1 里开启了 keep-alive连接能做到复用本质上也还是“浏览器问一句、服务器答一句”的半双工通信。这种模式下想做实时功能最常见的做法是轮询。前端每隔几秒发一个 AJAX 请求问服务器“有没有新消息”。如果消息量小、频率低轮询还能凑合一旦消息频率上来比如一个股票行情页面每秒要刷一次行情每个用户每秒都要发一个完整 HTTP 请求服务器光处理请求头、Cookie、路由匹配就已经不堪重负了。更关键的问题是轮询的实时性有天花板间隔设短了服务器扛不住间隔设长了用户觉得卡。还有一种叫“长轮询”的方案客户端发请求后服务器先hold住连接等有数据了再返回前端收到响应后再立刻发下一个请求。这个方案比普通轮询实时性好一些但连接一直在“请求-等待-请求”的循环里切换服务器端需要维护大量挂起的请求资源消耗依然很大。我印象很深曾在一个项目里用长轮询给前端推送通知并发一上来Nginx worker 数量直接被打满后端日志里全是连接超时。1.2 一次握手打通全双工通道WebSocket 的设计思路完全不同。它在 HTTP 的基础上做了一次升级握手握手成功后浏览器和服务器之间就建立起一条全双工的 TCP 通道双方随时可以互发数据不再需要一问一答。握手过程很有意思。浏览器发起一个带着特殊头部的 HTTP 请求GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13服务器收到后如果支持 WebSocket就返回HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo状态码 101 表示协议切换完成之后这条 TCP 连接上跑的就不再是 HTTP 语义而是 WebSocket 的数据帧协议。这里有个关键点Sec-WebSocket-Accept 的值不是随便写的它是把 Sec-WebSocket-Key 加上一段固定 GUID 做 SHA-1 哈希后再 Base64 编码的结果。这个机制主要是为了防止普通 HTTP 请求误打误撞地升级成 WebSocket也算是一道简单的防呆校验。对前端开发者来说你不需要自己处理这些握手细节浏览器已经把 WebSocket 的协议封装成了简单的 API。但理解握手过程很重要后面排查 101 状态码异常、反向代理配置问题全都得回到这一节知识。1.3 在哪些场景必须上 WebSocket下面几个是我实际接触过的场景用 WebSocket 和不用 WebSocket体验完全是两个量级第一类是实时协作工具比如在线文档、白板、多人画布。一个人编辑内容其他端的界面要毫秒级同步这只有全双工推送才能做到。第二类是监控大屏和运维控制台。我在一个服务器监控系统里用 WebSocket 持续推送 CPU、内存、网络流量数据前端拿到数据后直接更新图表整个过程没有一次轮询请求。第三类是即时通讯和客服系统。聊天消息天然是双向的、频繁的、低延迟的WebSocket 可以说是为此量身定做。第四类是物联网设备的实时状态上报。智能设备通过网关建立 WebSocket 长连接定时上报状态服务器也能主动下发指令。这类场景里WebSocket 比 MQTT 更容易让前端直接消费数据减少一层协议转换。简单说只要客户端和服务端的交互频率高、且服务端需要主动发起推送就必须考虑 WebSocket。如果只是周期性地拉一些低频数据用普通轮询反而更省事不必为了用而用。2. 浏览器端的 WebSocket API实战绕不开的核心2.1 创建一个 WebSocket 连接url、协议与状态机浏览器里创建连接非常简单就是一个构造函数const ws new WebSocket(ws://localhost:8080/ws);注意 url 的开头本地开发常用ws://线上环境如果用 HTTPS就必须用wss://。wss是 WebSocket over TLS数据加密传输和 HTTPS 同理。如果线上用 HTTP 域名却去连接wss://浏览器会报混合内容错误这点后面专门说。构造函数还支持第二个参数传入子协议const ws new WebSocket(ws://localhost:8080/ws, chat-protocol);或者多个协议const ws new WebSocket(ws://localhost:8080/ws, [chat-protocol, super-chat]);这个子协议是给应用层用的因为 WebSocket 本身只负责传输通道不关心消息格式。前端声明自己支持哪些协议服务器在握手响应里选择一个。如果服务器都不支持握手会失败。创建之后实例上有个readyState属性用来表示当前连接状态常量值含义CONNECTING0正在连接中OPEN1连接已建立可以通信CLOSING2正在关闭中CLOSED3连接已关闭或建立失败这个状态在很多场景下有用比如发送消息前判断一下readyState避免连接没建立就调用 send 导致报错。我的习惯是封装一个sendMessage方法内部先检查ws.readyState WebSocket.OPEN再发送。2.2 四大事件open、message、error、closeWebSocket 实例上有四个核心事件理解好这四兄弟前端逻辑就成功了一半。onopen在连接建立后触发。一般在这里做初始化操作比如发送一个认证消息或者启动心跳定时器。我见过一些人喜欢在new WebSocket之后立刻调 send这其实是无效的因为握手还没完成。正确做法是在onopen里发。onmessage是接收数据的入口。回调参数是MessageEvent核心数据在event.data里。数据类型取决于你服务器发的是什么如果服务器发的是字符串那这里就是字符串如果发的是二进制那这里可能是Blob或ArrayBuffer具体由binaryType决定。后续展开说。onerror在发生错误时触发。注意它不会直接告诉你错误原因通常紧接着会有onclose。排查错误得结合event对象、服务端日志、网络面板综合判断。我在调试时经常只看 error 事件就抓瞎后来养成一个习惯在onerror里把连接状态和当前环境打点记录到日志平台方便事后复盘。onclose在连接关闭时触发。回调参数是CloseEvent上面带三个重要字段code关闭码、reason原因描述、wasClean是否为正常关闭。正常关闭时wasClean为 truecode一般是 1000异常断开时wasClean为 falsecode可能是 1006。重连、提示信息、统计埋点全都要在onclose里处理。ws.onclose (event) { console.log(连接关闭 code${event.code} reason${event.reason} clean${event.wasClean}); };这里我要特别提一下 1006。1006 不是任何一方主动发送的关闭帧而是浏览器检测到底层 TCP 连接意外断开时报告的状态码。代理超时、服务器进程崩溃、网络切换都会导致 1006。前端看到 1006 基本可以断定是底层链路问题优先查服务器与代理。2.3 send() 不是只能发字符串send()方法可以发送的数据类型比大部分人以为的多。除了常见字符串还支持ArrayBuffer、Blob、TypedArray比如Uint8Array。// 发送文本 ws.send(hello); // 发送二进制 const buffer new Uint8Array([1, 2, 3, 4]); ws.send(buffer); // 发送 Blob const blob new Blob([hello world], { type: text/plain }); ws.send(blob);这个特性在做文件传输、音频采集、游戏道具同步时非常有用。比如在前端做一个实时音频聊天你从麦克风拿到的 PCM 数据本质上是二进制流直接用ArrayBuffer发送性能和语义都比转成 Base64 字符串强得多。接收二进制数据时可以通过binaryType属性控制数据类型。默认是blob如果你希望收ArrayBuffer要提前设置ws.binaryType arraybuffer; ws.onmessage (event) { if (event.data instanceof ArrayBuffer) { // 处理二进制数据 } };我自己踩过一个坑服务端用 Protobuf 压缩消息浏览器端默认 binaryType 是 Blob收到后还要转成 ArrayBuffer 才能解码。转来转去不仅代码啰嗦在大消息场景下还占用额外内存。后来统一在连接建立时就把binaryType arraybuffer写好省事不少。至于消息分帧很多新手会担心“粘包”。HTTP 时代确实有粘包问题需要应用层处理但 WebSocket 协议本身就有消息边界每个帧都携带长度信息浏览器解析出来后逐个触发onmessage所以不用自己切割数据。唯一要注意的是消息大小如果服务器发一个几十 MB 的二进制消息浏览器会一次性把数据塞进内存页面直接卡顿。这种场景应该走分片传输。3. 实操手写一个带心跳与自动重连的 WebSocket 封装3.1 为什么需要心跳很多人写过最简单的 WebSocket 客户端跑通后很开心但一上线就翻车。最常见的现象是页面开着数据流突然停了页面看起来一切正常但服务器那边已经完全没有这个连接了。问题在于 TCP 连接是“假活”的。如果链路中间没有数据传输运营商 NAT 设备、防火墙、负载均衡器会在空闲一段时间后悄悄断开连接。断开时两端不一定能立刻感知到尤其是手机网络切换 Wi-Fi 和 4G 的时候旧的连接直接失效前端却还以为自己连着。这就需要心跳机制前端每隔一段时间主动发一个很小的数据包服务器收到后回一个响应。只要能持续收到响应就证明链路是通的如果连续若干个心跳都没收到响应就主动关闭连接并重连。我见过有人用浏览器内置的WebSocket.prototype.ping但很遗憾浏览器端的 WebSocket API 并没有暴露协议层的 Ping/Pong 帧接口标准 WebSocket 里确实有 Ping/Pong 帧但那是服务器对服务器或服务器主动用的浏览器不能主动发协议级 Ping。所以前端只能借助应用层消息模拟心跳比如约定发送{type:ping}服务器收到后回{type:pong}。3.2 用 JS 实现心跳机制的详细过程心跳的核心思路是定时发送 超时判定。直接上代码class HeartbeatManager { constructor(ws, interval 15000, timeout 30000) { this.ws ws; this.interval interval; // 发心跳的间隔 this.timeout timeout; // 多久收不到pong判定超时 this.timer null; this.lastPongTime Date.now(); } start() { this.lastPongTime Date.now(); this.stop(); this.timer setInterval(() { // 连接不在了就直接跳过 if (this.ws.readyState ! WebSocket.OPEN) { return; } // 超过 timeout 没收到 pong强制断开 if (Date.now() - this.lastPongTime this.timeout) { this.ws.close(4000, heartbeat timeout); return; } this.ws.send(JSON.stringify({ type: ping, ts: Date.now() })); }, this.interval); } markPong() { this.lastPongTime Date.now(); } stop() { clearInterval(this.timer); this.timer null; } }用法是在onopen里启动心跳在onmessage里判断消息类型ws.onopen () { heartbeat new HeartbeatManager(ws); heartbeat.start(); }; ws.onmessage (event) { try { const data JSON.parse(event.data); if (data.type pong) { heartbeat.markPong(); return; } // 业务消息处理 } catch (e) { console.error(消息解析失败, event.data, e); } };间隔参数怎么定我一般取 10~15 秒发一次心跳超时时间设为心跳间隔的两倍左右。也就是说如果 30 秒没收到 pong就认为链路有问题主动断开重连。这个参数不要设太激进否则在网络抖动场景下容易误杀正常连接也不要设太保守否则用户流失了一分钟你才反应过来。有一点必须提醒心跳消息也要走消息类型约定。不要在业务消息里追加 ping/pong 字段而是用独立的 type 字段区分方便前后端处理。规范的协议设计会在心跳包里带上时间戳方便排查链路延迟。3.3 断线重连指数退避而不是死循环断线后直接重连是最直觉的做法但也是最容易击穿服务器的做法。假设线上有 1 万个客户端服务器重启一次1 万个客户端在同一秒发起重连服务器刚起来就被打得措手不及这就是重连风暴。我的处理方式是“指数退避”也就是失败后延迟时间按倍数递增const INITIAL_DELAY 1000; // 第一次重连延迟1秒 const MAX_DELAY 30000; // 最大延迟30秒 let retryCount 0; function scheduleReconnect() { const delay Math.min(INITIAL_DELAY * Math.pow(2, retryCount), MAX_DELAY); retryCount; console.log(将在 ${delay}ms 后重连); setTimeout(connect, delay); } ws.onclose () { scheduleReconnect(); }; ws.onopen () { retryCount 0; // 连接成功就重置 };延迟序列就是 1 秒、2 秒、4 秒、8 秒、16 秒、30 秒、30 秒……这样既不会让服务器被冲垮也能在网络恢复后尽快重连。如果想让效果更细还可以加上随机抖动比如在延迟上乘以一个 0.8~1.2 的随机因子防止大量客户端在完全相同的时刻重连。收到onclose事件后先判断一下是否需要重连也很关键。有些关闭是主动发起的比如用户退出登录这种就不要重连。正常逻辑是只有code 1000或业务层主动调用close()关闭的才不重连其他一律重连。3.4 一个可以直接抄走的完整封装类把上面两小节整合起来就是一个可用的前端 WebSocket 封装。我习惯用 class 组织class ReconnectWebSocket { constructor(url, options {}) { this.url url; this.heartbeatInterval options.heartbeatInterval || 15000; this.heartbeatTimeout options.heartbeatTimeout || 30000; this.maxDelay options.maxDelay || 30000; this.retryCount 0; this.ws null; this.heartbeatTimer null; this.lastPongTime Date.now(); this.manualClose false; this.connect(); } connect() { this.ws new WebSocket(this.url); this.ws.onopen () { this.retryCount 0; this.lastPongTime Date.now(); this.startHeartbeat(); this.onOpen?.(this); }; this.ws.onmessage (event) { const data event.data; if (typeof data string) { try { const obj JSON.parse(data); if (obj.type ping) { // 收到服务器ping时回pong this.send(JSON.stringify({ type: pong })); return; } if (obj.type pong) { this.lastPongTime Date.now(); return; } } catch (_) { // 非JSON消息直接交给业务层 } } this.onMessage?.(event); }; this.ws.onerror (error) { this.onError?.(error); }; this.ws.onclose (event) { clearInterval(this.heartbeatTimer); if (!this.manualClose) { const delay Math.min(1000 * Math.pow(2, this.retryCount), this.maxDelay); this.retryCount; setTimeout(() this.connect(), delay); } this.onClose?.(event); }; } startHeartbeat() { clearInterval(this.heartbeatTimer); this.heartbeatTimer setInterval(() { if (this.ws.readyState ! WebSocket.OPEN) return; if (Date.now() - this.lastPongTime this.heartbeatTimeout) { this.ws.close(4000, heartbeat timeout); return; } this.send(JSON.stringify({ type: ping, ts: Date.now() })); }, this.heartbeatInterval); } send(data) { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(data); return true; } return false; } close() { this.manualClose true; this.ws?.close(1000, manual close); } }使用方式很灵活const client new ReconnectWebSocket(wss://example.com/ws, { heartbeatInterval: 10000, heartbeatTimeout: 20000, }); client.onOpen (ws) { console.log(连接建立); ws.send(JSON.stringify({ type: login, token: getToken() })); }; client.onMessage (event) { console.log(收到消息, event.data); };要说封装里最容易忽略的地方就是手动关闭。如果不维护manualClose标志用户在页面跳转时手动 close 触发onclose封装类还会傻傻地重连一次造成“关闭后又复活”的诡异现象。用 flag 控制后手动关闭和异常断开就被明确区分了。另外页面跳转时最好在 beforeunload 里调用client.close()避免连接还在时页面突然销毁。4. 浏览器调试与兼容性从 DevTools 到多浏览器适配4.1 用 Chrome DevTools 抓 WS 帧排查 WebSocket 问题第一步永远是打开浏览器开发者工具。在 Chrome 或 Edge 里按 F12切到 Network 面板筛选条件选 WS就能看到所有 WebSocket 连接。点击一个连接可以看到它的握手请求信息请求头、响应头、是否有 101 状态码还能看到具体的 Sec-WebSocket-Key 和 Sec-WebSocket-Accept。连接建立后点击“Messages”或“Frames”子标签页能看到所有经过该连接的数据帧。Chrome 里可以直观看到三类帧Text 帧也就是字符串消息右边会直接显示内容。Binary 帧显示为长度信息可以右键转成 ArrayBuffer 保存查看。Ping/Pong 帧浏览器实现的底层心跳帧一般不会显示太多内容。排查断连问题有一个很实用的习惯先看会话里最后一个帧是什么。如果最后一条是服务器发的 text 帧然后立刻出现“Connection Closed”那基本是服务器主动关闭如果最后一条是浏览器发的 text 帧之后就没有任何帧直接进入 closed那多半是链路被中间设备切断或者服务器侧没有响应。光靠这个前后顺序就能筛掉一半的问题。另外DevTools 的 Console 面板会输出 WebSocket 的报错信息比如 “WebSocket connection to wss://... failed”。点击这个报错控制台会带出 Network 面板定位到对应请求非常方便。Firefox 的 DevTools 也有类似功能Safari 的 Web Inspector 相对简陋一些但基本抓帧能力是有的。4.2 跨浏览器兼容情况和降级方案现代主流浏览器都支持 WebSocketPC 端 Chrome、Edge、Firefox、Safari 自不必说移动端 iOS Safari、Android Chrome 也都内置。用一张表看兼容度浏览器最低支持版本支持 wss备注Chrome16是包括 Android 版Edge12是新版基于 Chromium 内核Firefox11是Safari/ios Safari7是部分旧版有自动挂起问题IE10是是 IE 里少数值得夸的功能如果项目还需要兼容 IE10 以下那就比较难受了只能降级方案。降级策略我一般分两档第一档是 Server-Sent EventsSSE也叫 EventSource。它支持服务器单向推送浏览器端只要new EventSource(url)就能持续收到推送且自带断线重连机制非常轻量。缺点是只能服务端往客户端推客户端要临时发消息还得额外走一个普通 HTTP 请求。如果业务是通知类、行情类这种服务器单向推送场景SSE 是极好的降级选择。第二档是长轮询。在真正老旧的浏览器上SSE 也可能不可用那就只能用 setTimeout XMLHttpRequest 模拟长轮询。这种方案实时性一般服务器压力也大只能算兜底。选型标准我建议按业务来消息是双向的还是单向的双向必须 WebSocket单向优先 SSEWebSocket 和 SSE 都不支持的极端老旧环境才考虑长轮询。4.3 混合内容、代理和其他浏览器相关的坑“混合内容”是浏览器端非常典型的坑。如果你的页面是通过 HTTPS 访问的那浏览器默认不允许页面里的 WebSocket 去连ws://协议因为ws://是明文传输。Chrome 会在控制台报 “Mixed Content” 错误连接直接失败。解决办法就是把 WebSocket 地址改成wss://。同理如果页面是 HTTP那既可以用ws://也可以用wss://但从安全角度我永远推荐wss://。与之相关的还有代理配置。很多局域网环境或企业网关会把非标准端口过滤掉WebSocket 普遍用的端口是 80、443、8080 这些常用端口问题不大。但如果你自定义了一个冷门端口比如 30001一旦中间的代理不认这个端口101 响应会直接被截断。前端表现通常就是握手失败DevTools 里看到 101 之后立即断开或在 onerror 里看到Unexpected response code: 200。此时后端排查时重点检查反向代理的 Upgrade 头配置Nginx 需要显式设置location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }这一节是经验盲区很多前端同学把问题定位到前端代码反复重连最后发现是 Nginx 少配了一行。所以一旦遇到握手 200 而非 101第一反应应该是检查反向代理的配置。手机浏览器还有一个独特问题页面切到后台WebSocket 连接可能被系统挂起。iOS Safari 尤其明显锁屏后 TCP 连接很容易被系统回收。Web 端页面重新可见时应该主动检测连接状态必要时重新连接。可以监听visibilitychange事件页面一可见就检查readyState如果已经 close 就立即重连。5. 我踩过的坑和问题排查实录5.1 高频问题排查表把这一年多遇到的典型问题整理成了速查表建议截图保存症状可能原因排查方向握手失败状态码 200反向代理未开启 Upgrade检查 Nginx/Apache 代理配置握手失败状态码 404服务端路由未处理 WebSocket 升级检查后端路由配置连接频繁断开code1006代理超时/网络切换/服务端宕机看服务端日志、调整代理超时时间消息发不出去readyState 不是 OPEN发送前检查状态等 onopen页面在后台一段时间回数据不更新连接被系统挂起或关闭监听 visibilitychange 重连onmessage 收不到但网络上看有数据帧消息被业务层提前消费检查是否有其他 onmessage 监听抢占连接稳定但页面 CPU 高收到大量高频消息导致渲染频繁前端做节流合并渲染close code 重新解释锁定为 1000但业务断开服务端主动关闭看业务代码在哪里调 close排查的基本原则是分层定位先看浏览器 DevTools 里握手是否成功再看帧数据是否在传输然后看后端日志最后检查代理与安全设备。不要在未观察帧传输前就急着改代码。5.2 一个真实的心跳误判案例我这个项目里心跳参数设置得比较激进3 秒发一次心跳5 秒没反馈就断线重连。上线后客服端长时间挂在后台时不时自动重连一次用户虽然没感知但账号在线状态频繁闪烁。后来查下来是服务器在高负载时处理质量有所波动偶尔一条 pong 消息处理超过了 5 秒。前端认为心跳超时主动断开并重连。其实网络和应用都是正常的只是服务器偶发慢了一点我的判死标准太严了。调整方案是把心跳间隔改成 15 秒判死标准改为 45 秒。还有一个更稳健的办法不要单纯依赖最后一次 pong 时间而是连续失败 N 次才判死。这相当于给心跳加了去抖逻辑避免单次超时直接误杀。很多生产级方案里都会做“连续 2~3 次未收到 pong 才重连”我认为这比单独一个超时更科学。5.3 一个跨浏览器的兼容笑料有段时间用户反馈在 Windows 某老浏览器上打不开充值页面查了一圈后端日志完全没有异常请求。后来用开发者工具一看是业务代码里直接用了new WebSocket(...)但这个老浏览器版本里的 WebSocket 对象只实现了握手和文本发送没有处理二进制帧所以收发消息全部异常。处理方法是加一个环境检测工具函数function isWebSocketSupported() { return WebSocket in window URL in window; }不支持的浏览器降级到 SSE 或者长轮询。这是一个很典型的“现代浏览器都支持不代表所有浏览器都支持”的教训。兼容性判断不要靠版本号字符串匹配用特性检测最可靠。另外提一个容易被击穿的点WebSocket 地址里的路径参数也会触发浏览器缓存吗一般不会但有些浏览器插件会拦截ws://或wss://请求导致用户侧连接异常。排查这类问题可以让用户开一个无痕窗口试试关闭所有扩展后如果恢复正常十有八九是插件的问题。5.4 给新手的几条实操建议第一所有 WebSocket 消息一定要走统一的协议格式。我推荐{ type: xxx, data: {...} }格式type 区分业务类型data 携带业务数据。不要每个接口自己定义一种消息结构否则前后端联调和维护会非常痛苦。第二不要在 onmessage 里直接做复杂 DOM 操作。高频消息一次性来了几十条每条都直接操作 DOM页面帧率必崩。正确做法是做队列合并比如把消息攒到一批再一次性渲染或者用 requestAnimationFrame 控制渲染节奏。第三留意消息大小。文本消息建议精简格式能只传必要字段就只传必要字段。我在监控项目里把一个 JSON 消息从 2KB 压缩到 200 字节带宽压力直接降了一个量级。还能用二进制协议进一步压缩但复杂度会上升优先做 JSON 瘦身。第四日志必须有。生产环境里 WebSocket 问题最难定位的原因就是没有日志。在前端至少要把连接建立、主动/被动断开、重连次数、心跳超时这几个事件打到日志平台出了事才能找到证据链。第五连接生命周期要和页面生命周期绑定。SPA 路由切换时该关闭的连接要关闭页面可见性变化时该检查的要检查。很多页面卡死或后台耗电量高都是因为一个已经没用的 WebSocket 还一直在跑心跳。WebSocket 的难点从来不在“能跑起来”而在于长期稳定地跑。我把这些坑写下来是希望你看完这篇文章后能少踩一半我踩过的雷。如果你的项目也打算上 WebSocket不妨先从封装一个带心跳和重连的客户端开始不要裸用原生 API。实测下来这套封装在多个项目里稳定运行了大半年线上基本没有再因为连接稳定性出过大的事故。
返回列表