ARTICLE DETAIL

资讯详情

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

WebSocket从握手到心跳:长连接机制与工程实践全解析

WebSocket从握手到心跳:长连接机制与工程实践全解析 WebSocket一从握手到心跳把长连接那些事儿彻底讲明白在开始写这篇之前我在热搜里看到“websocket使用”“websocket心跳机制实现”这些词频繁出现有理由相信很多同学正卡在实时推送这条路上。WebSocket这东西说简单是简单前端一个new WebSocket()就能连上说复杂也复杂一旦涉及心跳、重连、鉴权、集群推送细节多到能淹没一整周的工作时间。这个系列我会把WebSocket从零开始拆开讲到生产环境能用的程度。今天这第一篇先解决三个问题WebSocket到底是什么、为什么要用它、以及上手第一步最容易踩的坑在哪。适合刚接触实时通信的前端同学、后端接口人以及那些正被HTTP轮询折磨得头皮发麻的开发者。看完这篇你至少能自己搭一个带心跳和重连的WebSocket服务并且在遇到连接掉线时知道先查哪里。内容整体设计与思路拆解1.1 为什么HTTP轮询不行WebSocket能行聊WebSocket之前得先把“为什么需要它”这件事说透。传统HTTP协议是严格的一问一答客户端发请求服务端给响应请求没了连接也就“散”了。服务端想主动给客户端推点消息做不到只能等客户端下次发请求时顺带把新数据带回去。于是就有了轮询客户端每隔几秒发一次“有新消息吗”有就返没有也返个空。轮询的问题在于效率太低。消息实时性取决于轮询间隔间隔设短了大量请求打上来毫无意义地消耗带宽和服务器资源间隔设长了消息延迟又肉眼可见。我自己见过一个极端例子某内部系统为了“实时”刷新工单状态把轮询间隔压到了1秒一天下来光是无效请求就占了几十万次数据库查询压力全耗在“根本没变化”的数据上。WebSocket的核心价值在于一次握手建立长连接之后客户端和服务端随时可以互发消息。它复用了HTTP的握手过程但完成升级后就不再受“请求-响应”模型的约束变成了真正的全双工通道。用大白话说HTTP像寄信来回一趟就是一次完整周期想多聊几句就得反复寄WebSocket像通了电话接通之后两边随时能说话不用每次重新拨号。这个差异带来的直接影响有两层。第一层是资源效率长连接建立后没有HTTP头和Cookie的重复开销一条普通文本消息帧头只有几个字节对比HTTP请求动不动几百字节的头部省下的带宽非常可观。第二层是开发模式服务端可以主动“找”客户端了服务端有状态变化就能立刻推下去不再需要客户端“隔一会儿来看一眼”。1.2 为什么你现在就该学习WebSocket如果你做的是聊天室、弹幕、协同编辑、实时白板、在线客服、行情推送、服务端进度通知这类功能那WebSocket不是可选项是必需品。2024年了用户对“实时”的容忍度越来越低一个页面里手动刷新才能看到新数据放在几年前还能忍放在现在就是产品缺陷。就我观察到的趋势传统行业也在这两年密集接入实时通信。我做过的项目里一个物流系统需要实时更新司机位置到调度大屏一个工厂需要把产线设备状态实时推到看板上一个教育平台要把老师批改作业的结果实时返回到学生端。这些场景无一例外都选择了WebSocket原因不是跟风而是它们全都具备一个共同特征数据变化频繁、服务端需要主动通知客户端、对延迟敏感。当然WebSocket也不是银弹。如果你的业务只是偶尔几条通知一天推不了几次那用SSEServer-Sent Events甚至短轮询可能更省事。WebSocket的维护成本在于连接管理连接多了以后心跳、重连、鉴权、集群转发都要自己去搞。所以我建议每个开发者都学WebSocket但要不要用在自己的项目里还是得先做需求评估。1.3 第一篇要解决的核心问题这个系列我自己划分了一下第一篇集中在“基础建连与连接保活”核心目标有三个第一把WebSocket的通信模型说明白包括握手过程、帧格式、连接关闭方式把这些基础认知打牢后面遇到问题才不会是“玄学”。第二把心跳机制的几种实现方式讲透回答“为什么明明连着线还是会断”这个高频问题并给出可以直接抄的配置参数。第三把前端接入的完整代码骨架给出来包括服务端代码、前端封装、重连策略让读这篇文章的人能直接搭出一个能用的项目雏形。有了这个基础后续再讲集群部署、消息可靠性、鉴权模型才有往上盖楼的地基。核心细节解析与实操要点2.1 通信过程拆解从握手到收发WebSocket的通信过程可以拆成三个阶段。第一阶段是握手。这一步复用的是HTTP协议客户端发一个普通的GET请求带上特定的头信息表示“我想升级协议”GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13服务端收到后会校验这些头如果同意升级就返回101 Switching Protocols同时带上Sec-WebSocket-Accept响应头。这个值是服务端对客户端发来的Sec-WebSocket-Key做了一次SHA-1加Base64编码计算得到的用来证明双方确实完成了协议升级。校验通过之后TCP连接就正式变成WebSocket连接了后续数据不再走HTTP的请求响应模型。这里有个细节很多人第一次写会忽略Sec-WebSocket-Key并不是用来做安全认证的它只是用来确认服务端确实支持WebSocket协议。真正的安全控制比如鉴权、权限校验要在业务层自己做比如在握手URL里带token或者握手完成后的第一条消息里做身份确认。第二阶段是数据帧传输。握手完成后客户端和服务端之间收发的是“帧”。每一帧的二进制结构包含FIN位、操作码文本帧是0x1二进制帧是0x2关闭帧是0x8Ping帧是0x9Pong帧是0xA、掩码位、长度字段和载荷数据。客户端发送给服务端的帧必须做掩码处理服务端发送给客户端的帧则不需要。这是协议强制要求目的是防止早期某些代理服务器把WebSocket数据误当成HTTP请求来缓存或解析。如果你自己写客户端底层实现这个掩码逻辑漏掉就会导致服务端直接断开连接。第三阶段是关闭连接。关闭流程是双方各发一个Close帧然后各自回复Close帧确认再关闭底层TCP连接。所以严格来说WebSocket的关闭是一个协商关闭的过程不是某一方直接掐断。当然如果某一端异常崩溃另一方会通过TCP层感知到连接断开。2.2 心跳机制为什么必须有怎么实现你看到热搜里有“websocket心跳机制实现”这几乎是生产环境绕不开的必修课。先说为什么需要心跳。WebSocket建立之后连接长时间没有数据传输中间可能经过的各种网络设备路由器、交换机、负载均衡器会认为这个连接空闲了出于节省资源的考虑把它断开。比如很多云厂商的负载均衡服务默认空闲超时时间是60秒左右超过这个时间没有任何数据流动连接就被悄悄掐掉了。而客户端如果不主动发消息是感知不到连接已经被断掉的只有在下次真正发消息时才会发现发送失败然后触发重连逻辑但这时候消息已经丢了。心跳机制的实现思路很简单定时发送Ping帧收到Pong帧就认为连接存活。具体有两种做法。一种做法是协议自带的方式。客户端定时发Ping控制帧服务端收到后必须自动回复Pong帧这个由协议保证不需要业务层动手。服务端也可以主动发Ping探测客户端是否还活着客户端同样会自动回Pong。另一种做法是业务层自定义的方式。客户端定时发一个自定义文本消息比如{type:heartbeat,timestamp:...}服务端收到后回一个{type:heartbeat_ack,...}。双方约定好超时时间超过N秒没收到Ack就判定连接异常。实际项目中我建议以业务层心跳为主、Ping/Pong为辅。原因有两个一是业务层心跳可以顺便携带一些状态信息比如客户端的在线状态、当前正在浏览的页面二是Ping/Pong的控制帧在一些老旧的代理或网关设备上可能被过滤导致收不到Pong但连接其实还活着造成误判。心跳的时间间隔设置也有讲究。假设你的负载均衡空闲超时是60秒那么心跳间隔建议设置在20到30秒之间保证在设备判定空闲之前有数据流动。判断超时可以按连续3到5次没有收到心跳响应来算避免单次网络抖动就触发重连。2.3 重连机制指数退避是首选没有重连机制的WebSocket应用在弱网环境下基本没法用。手机切Wi-Fi、地铁过隧道、笔记本合盖再打开任何一个场景都可能导致连接断开。最简单的重连是固定间隔重连比如断开后每5秒重试一次。但这样有两个问题服务器如果是宕机重启短时间内大量客户端同时重连会造成惊群效应把服务端打挂网络持续不可用的时候高频重试只会浪费客户端电量和服务端资源。更稳妥的做法是指数退避重连第一次重试等1秒第二次等2秒第三次等4秒第四次等8秒封顶到30秒。在此基础上再加随机抖动比如在计算出的等待时间上增加0到500毫秒的随机值避免多个客户端在同一时刻同时发起重连。这个思路和网络协议里的退避算法一致也是解决重连风暴最实用的方案。重连时还要注意状态恢复。断开期间用户的操作比如发了几条消息应该缓存到本地重连成功后补发会话标识如果过期了要在重连前重新获取。这个流程虽然琐碎但直接决定了用户对弱网体验的评价。实操过程与核心环节实现3.1 服务端与客户端的典型实现我用一个简易聊天室作为例子把前后端跑通的核心代码写出来。环境假设是后端用Node.js前端用原生JavaScript不依赖任何框架。后端实现使用Node.js加ws库const WebSocket require(ws); const http require(http); const server http.createServer(); const wss new WebSocket.Server({ server }); // 保存所有连接的客户端 const clients new Set(); wss.on(connection, (ws, req) { console.log(新客户端接入来源: ${req.socket.remoteAddress}); clients.add(ws); // 接收消息并广播给所有人 ws.on(message, (data) { const msg data.toString(); console.log(收到消息: ${msg}); // 构造要广播的消息 const broadcastData JSON.stringify({ type: chat, content: msg, time: Date.now() }); clients.forEach((client) { if (client.readyState WebSocket.OPEN) { client.send(broadcastData); } }); }); // 处理心跳收到ping自动回pongws库内置支持 ws.on(pong, () { console.log(收到pong连接正常); }); ws.on(close, () { console.log(客户端断开); clients.delete(ws); }); ws.on(error, (err) { console.error(连接异常: ${err.message}); clients.delete(ws); }); }); // 服务端主动发送Ping检测客户端存活 const heartbeatTimer setInterval(() { wss.clients.forEach((ws) { if (ws.isAlive false) { ws.terminate(); return; } ws.isAlive false; ws.ping(); }); }, 30000); server.listen(8080, () { console.log(WebSocket服务已启动: ws://localhost:8080); });这里有一个我在实际项目中踩过的坑服务端主动Ping的时候记得维护一个isAlive标志位。先把标志位置为false然后发Ping如果客户端正常客户端会自动回Pong服务端的pong事件回调里再把isAlive置回true。下一次定时器检查时如果isAlive还是false说明这个客户端已经失联直接terminate()。这个逻辑能有效清理死连接避免连接池里堆满僵尸连接。前端实现使用原生JavaScript封装一个可复用的客户端类class WSClient { constructor(url, options {}) { this.url url; this.ws null; this.heartbeatTimer null; this.reconnectTimer null; this.isManualClose false; this.reconnectAttempts 0; this.maxReconnectAttempts options.maxReconnectAttempts || 10; this.heartbeatInterval options.heartbeatInterval || 20000; this.onMessage options.onMessage || function() {}; } connect() { this.ws new WebSocket(this.url); this.ws.onopen () { console.log(连接已建立); this.reconnectAttempts 0; this.startHeartbeat(); }; this.ws.onmessage (event) { // 收到心跳回复时单独处理其他消息交给业务回调 try { const data JSON.parse(event.data); if (data.type pong) { console.log(心跳回复正常); return; } this.onMessage(data); } catch (e) { this.onMessage(event.data); } }; this.ws.onclose () { console.log(连接关闭); this.stopHeartbeat(); // 非主动关闭时触发重连 if (!this.isManualClose) { this.scheduleReconnect(); } }; this.ws.onerror (err) { console.error(WebSocket错误:, err); }; } // 发送心跳 startHeartbeat() { this.heartbeatTimer setInterval(() { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: ping, time: Date.now() })); } }, this.heartbeatInterval); } stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer null; } } // 指数退避重连 scheduleReconnect() { if (this.reconnectAttempts this.maxReconnectAttempts) { console.log(重连次数已达上限放弃重连); return; } const delay Math.min(Math.pow(2, this.reconnectAttempts) * 1000, 30000) Math.random() * 500; console.log(将在 ${Math.round(delay / 1000)} 秒后进行第 ${this.reconnectAttempts 1} 次重连); this.reconnectTimer setTimeout(() { this.reconnectAttempts; this.connect(); }, delay); } send(data) { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(typeof data string ? data : JSON.stringify(data)); return true; } console.warn(连接未就绪消息发送失败); return false; } close() { this.isManualClose true; if (this.ws) { this.ws.close(); } } } // 使用示例 const client new WSClient(ws://localhost:8080, { onMessage: (data) { console.log(收到消息:, data); } }); client.connect();这段前端代码里我把心跳、重连、消息分发都封装在一个类里业务方只需要实例化然后传入onMessage回调就行。这样写的好处是凡是新项目要接WebSocket直接复制这个类做少量定制不用每次重新搭一遍脚手架。3.2 二进制数据与文件传输的处理文本聊天是WebSocket最常见的场景但很多人的需求是传文件、传图片、传实时音频流。这时候就不能用字符串了得用二进制帧。WebSocket的二进制消息在浏览器端拿到的event.data是Blob对象在Node.js服务端拿到的是Buffer。直接发文件的做法是// 前端读取文件为ArrayBuffer并发送 const fileInput document.getElementById(fileInput); fileInput.addEventListener(change, (e) { const file e.target.files[0]; const reader new FileReader(); reader.onload () { client.ws.send(reader.result); // 发送ArrayBuffer }; reader.readAsArrayBuffer(file); }); // 后端接收Buffer做落地处理 ws.on(message, (data) { if (Buffer.isBuffer(data)) { // 二进制数据处理比如写入文件 fs.writeFileSync(received_${Date.now()}.bin, data); } });这里要注意一个问题协议层没有消息边界的概念。WebSocket底层虽然会分包但上层只提供一个onmessage事件一次send对应一次onmessage。如果文件很大客户端一次性发送几MB的数据WebSocket底层会把数据拆成多个帧传输但接收方收到时仍然会拼成一个完整的消息体。所以不需要自己在业务层做分片重组。那什么时候需要自己做分片呢当你上传超大文件比如几百MB时一次性发一个Buffer容易把内存打满而且如果中途断开整个文件就要重传。这时候建议业务层把文件切成小块比如每片1MB逐片发送每片带上序号服务端按序号拼接。这个思路和HTTP的分片上传、断点续传本质是一样的。3.3 权限校验与消息格式约定WebSocket的握手请求走HTTP所以理论上可以在握手阶段做鉴权。常见做法有三种。方案一是在Query参数里带token比如ws://api.example.com/ws?tokenxxxxx。服务端在upgrade事件或connection事件回调里读取URL上的token参数校验合法性。优点是简单缺点是一旦握手URL被日志记录token就泄露了所以一般配合短时有效的临时token使用。方案二是用子协议传身份。new WebSocket(url, [chat, auth-xxxxx])服务端可以从请求头里取出子协议字段做校验。这个方式比较隐蔽但用法相对冷门调试时容易被忽略。方案三是握手成功后第一条消息做鉴权。客户端连接建立后立即发送一条{type: auth, token: xxxxx}消息。服务端在收到鉴权消息之前不处理任何业务消息只允许这一条消息通过。这种方式最灵活token即使过期也可以在连接生命周期内不强制断开而是等服务端主动发一条未授权消息关闭连接。我建议按场景选企业内部系统、可控客户端环境用方案一就够了面向公网的开放平台建议方案一加方案三组合握手凭证用短期token连接建立后再做一次身份确认双保险。消息格式方面我习惯统一用{type: string, payload: any}的包装结构{type: chat, payload: {from: user_001, content: 你好}} {type: heartbeat, payload: {time: 1690000000}} {type: ack, payload: {msgId: uuid-123, status: received}}统一结构的好处非常多服务端可以按type分发到不同的处理函数客户端可以根据type决定渲染逻辑还是触发内部动作日志打点也方便。这个小事很多人一开始不注意消息格式写得天马行空后面服务端扩展和客户端适配都极其痛苦。常见问题与排查技巧实录4.1 连接频繁断开先看网络链路再查代码我做过的项目里最常见的WebSocket老断问题排查顺序基本固定。第一步先用浏览器的开发者工具看Network面板里WebSocket的帧记录。如果看到一段时间后有一条红色的关闭帧记录里的code字段是1006异常关闭说明连接不是正常关闭的多半是中间网络设备或服务端主动掐断。如果code是1000正常关闭说明是某一端主动正常关闭问题可能在业务代码的close()调用时机上。第二步确认链路中间是否有代理或负载均衡。前面提过很多负载均衡器的空闲超时是60秒如果你的应用不做心跳连接就会在1分钟左右被掐断这是高频问题。解决方式就是心跳间隔设在20到30秒。第三步看服务端日志。如果服务端完全没有收到连接被关闭前的异常堆栈那大概率是网络层的问题如果服务端抛了ECONNRESET之类的错误那可能是客户端异常退出也可能是服务端进程内存抖动导致连接被操作系统回收。4.2 消息乱序与重复消息WebSocket协议本身不保证消息顺序吗其实在TCP之上WebSocket是保证顺序的消息不会乱序。真正的问题是重复消息。发生重复消息的常见场景客户端发送一条消息后网络超时客户端认为发送失败触发重发但实际上服务端已经收到了于是服务端处理了两次。这是典型的重试导致的重复投递问题。解决方案是业务层做幂等每条消息带上唯一的msgId服务端在收到消息后先查缓存或数据库如果这个msgId已经处理过就直接丢弃不再重复执行逻辑。这个思路和处理HTTP请求的幂等性完全一致只是很多人把HTTP那套经验忘在了WebSocket上。4.3 服务端如何做单点消息推送WebSocket是长连接服务端天然知道哪个用户在哪台机器上有连接所以做实时推送很方便。但如果你的服务端是多实例部署比如K8s里跑了3个Pod就会遇到一个问题用户A连在实例1上用户B连在实例2上用户A给用户B发消息消息落在实例1上但实例2不知道这条消息。这个问题的通用解法是引入消息中间件或者Redis Pub/Sub做实例间的广播。实例1收到消息后发布到Redis Channel实例2和实例3订阅同一个Channel找到自己实例上的目标连接再推送。这样每个实例只维护自己实例上的连接跨实例的消息通过Redis转发业务逻辑完全无感。如果你的技术栈里已经有Kafka或RabbitMQ也可以直接复用。需要注意的一点是不要在代码里写死当前服务端持有全量连接多实例部署时这个假设就是错的。4.4 兼容性与降级策略WebSocket在现代浏览器里支持率已经很高但在某些内嵌Webview、老旧移动端、企业安全策略下WebSocket握手可能被拦截。这时候常见降级方案是轮询或SSE。关于轮询和SSE的区别简单说一下轮询客户端固定间隔发HTTP请求服务端有数据就返回没数据也返回。实现简单但浪费请求延迟取决于轮询间隔。SSE服务端向客户端单向推送基于HTTP长连接不支持客户端向服务端任意时刻发消息适合订阅通知类场景。WebSocket全双工双向实时开销最小但实现复杂度最高。如果你的业务是服务端主动通知客户端居多比如订单状态更新、消息提醒SSE其实就够用没必要上WebSocket。只有在双向高频互动场景比如聊天、协作编辑、实时白板、游戏对局WebSocket才真正发挥价值。这也是选型时值得先想清楚的一点不要因为WebSocket是热词就盲目引入先评估业务到底是单向通知还是双向交互。前端框架层如何在React和Vue里优雅接入5.1 React中封装useWebSocketReact项目里接入WebSocket直接在每个组件里new WebSocket()会让状态管理变得混乱。比较干净的做法是封装一个自定义Hook把连接状态、消息队列、心跳重连都收敛在一个地方。import { useEffect, useRef, useCallback, useState } from react; function useWebSocket(url, { onMessage, heartbeatInterval 20000 } {}) { const wsRef useRef(null); const [readyState, setReadyState] useState(WebSocket.CONNECTING); const onMessageRef useRef(onMessage); onMessageRef.current onMessage; useEffect(() { const ws new WebSocket(url); wsRef.current ws; ws.onopen () setReadyState(WebSocket.OPEN); ws.onclose () setReadyState(WebSocket.CLOSED); ws.onerror () setReadyState(WebSocket.CLOSED); ws.onmessage (event) { if (onMessageRef.current) { onMessageRef.current(JSON.parse(event.data)); } }; return () { ws.close(); }; }, [url]); // 发送消息 const send useCallback((data) { const ws wsRef.current; if (ws ws.readyState WebSocket.OPEN) { ws.send(typeof data string ? data : JSON.stringify(data)); } }, []); return { readyState, send }; } // 使用 const { readyState, send } useWebSocket(ws://localhost:8080, { onMessage: (msg) { console.log(收到, msg); } });有一类容易犯的错在onMessage回调里直接引用组件内的state造成闭包捕获了旧值。解决办法就是用useRef保存最新的回调每次渲染更新ref.current这样事件处理器拿到的永远是最新的。这是React里处理事件回调引用最新状态的标准姿势。5.2 Vue中封装useWebSocketVue 3的组合式API写法和React类似逻辑更直接import { ref, onMounted, onUnmounted } from vue; export function useWebSocket(url) { const ws ref(null); const readyState ref(WebSocket.CONNECTING); const messageList ref([]); onMounted(() { ws.value new WebSocket(url); ws.value.onopen () { readyState.value WebSocket.OPEN; }; ws.value.onmessage (event) { messageList.value.push(JSON.parse(event.data)); }; ws.value.onclose () { readyState.value WebSocket.CLOSED; }; }); onUnmounted(() { ws.value?.close(); }); function send(data) { if (ws.value?.readyState WebSocket.OPEN) { ws.value.send(typeof data string ? data : JSON.stringify(data)); } } return { readyState, messageList, send }; }Vue这里要注意的是如果你把messageList直接交给模板渲染数据量大了之后容易出现性能问题。实时聊天类应用建议用虚拟滚动组件或者只保留最近200条消息再往前的老消息存到本地历史里。这些细节决定了长列表场景下的流畅度不处理的话聊天记录超过几百条时页面会出现明显卡顿。5.3 状态管理消息分发需要全局还是局部有人习惯把WebSocket放在全局StoreRedux、Pinia里任何组件都能发消息、读消息。我的看法是连接和基础消息收发可以放全局业务逻辑尽量局部化。连接放全局可以确保整个应用只有一个WebSocket实例不会出现多个连接重复建立的问题。但每个页面关心的业务消息不同如果所有消息都进全局Store会让Store变得极其臃肿。更合理的做法是全局层维护连接状态已连接/断开/重连中、全局性消息比如系统通知、登录失效通知局部层每个页面或模块在收到消息后通过订阅/过滤机制只关心自己需要的type这个模型的好处是职责清晰。连接生命周期和全局通知由框架层管理业务模块各自处理自己的领域消息互不干扰。进阶优化集群部署、鉴权与监控告警6.1 连接管理做一张在线表真实生产环境里你不仅要知道有多少连接还需要知道某个用户是否在线、在哪个实例上。这时候需要维护一张在线状态表。最简单的方式是Redis HashHSET online_users user_001 ws_instance_1用户建立连接时写入断开时删除。这样其他服务比如发送通知的后台任务就可以通过查询这张表判断用户是否在线。如果你用的是云厂商的Redis注意给key设置过期时间防止客户端异常崩溃后没有走正常close流程在线表里留下脏数据。这里有个小的经验值TTL可以设为心跳间隔的3到5倍每次收到心跳就续期这样即使客户端异常死亡最多几秒内在线状态就能自动清理。6.2 鉴权流程的完整设计WebSocket服务端的鉴权架构我推荐这样分层。第一层是握手鉴权。客户端连接时带上短期token服务端在升级握手前校验。校验不通过就直接拒绝连接。这层拦截掉绝大多数非法请求。第二层是消息鉴权。连接建立后服务端对每条消息校验用户是否有权执行对应操作。比如普通用户不能向管理员群发消息这个校验放在业务处理层。第三层是数据权限。推送数据的时候服务端要确保只把属于该用户的数据推给该用户。尤其在多租户系统里A租户的数据绝不能推到B租户的连接上。这个校验可以抽象成统一的推送前过滤函数每次调用推送接口都过一遍。6.3 可观测性心跳即探活WebSocket服务不好调试出了问题很难复现所以监控告警必须做在前面。我建议至少监控这几个指标当前连接数判断服务容量是否接近上限每秒新建连接数突然暴涨可能意味着客户端在疯狂重连消息吞吐量每秒收发的消息量关注峰值和均值心跳超时率心跳超时的比例能反映网络质量和连接稳定性这些指标在Node.js里可以自行统计并定时上报到Prometheus或者直接打到业务日志里由日志系统聚合分析。告警规则可以设置成心跳超时率连续5分钟超过10%就触发告警连接数突降30%以上说明服务端可能发生了批量切断。有一点想提醒大家上线WebSocket服务之前一定要压测。WebSocket和HTTP最大的不同是连接是常驻的每秒1000个并发HTTP请求和1000个常驻WebSocket连接对服务端资源的消耗天差地别。压测时重点观察内存占用和文件描述符数很多新手项目就挂在连接数一上来内存先爆了。6.4 部署形态单机还是集群WebSocket服务部署形态上有两种选择。单机部署适合接入量不大、公司内部使用的场景。代码简单直接一个进程跑起来连接信息都在本地不需要考虑跨实例同步排查问题也容易。一旦要上集群就必须解决两个问题一是连接信息共享前面提到的Redis在线表就是答案二是跨实例消息转发用消息中间件或Redis Pub/Sub。这两个问题不解决集群部署就会变成每个实例是一个孤岛用户的连接分布在不同的岛上发消息时经常发不到对方所在的岛。集群部署还有一个细节会话保持。如果你的WebSocket服务前面有负载均衡器需要开启会话保持把同一个客户端的连接始终转发到同一个后端实例否则客户端每次重连都可能落到不同实例上在线状态表就会频繁抖动。很多云厂商的SLB默认就支持四层会话保持直接开启即可。写在最后的几点体会文章写到这WebSocket的入门到进阶主线就完整了。最后分享几点我在实际项目里沉淀下来的感触。第一WebSocket的复杂度不在协议本身而在连接治理。协议就那么几十页半天就能看完但心跳、重连、幂等、跨实例转发、在线状态管理这些才是真正决定一个实时系统稳不稳的东西。搞懂协议只是拿到了入场券。第二不要迷信实时这个词。遇到业务需求先问一句真的需要双向实时吗很多通知场景用SSE甚至轮询就够了。我见过不少项目为了一点轻微的通知延迟就把技术栈硬切到WebSocket结果运维复杂度翻了好几倍体验提升却微乎其微。技术选型永远服务于业务价值。第三调试工具一定要趁着开发期用熟。浏览器自带的Network面板、ws库的日志参数、线上环境的日志聚合这三样东西配合起来能解决绝大多数问题。等到线上事故才临时翻文档找调试方法那是把自己往火坑里推。WebSocket这个系列我会继续往下写下一篇打算重点拆解服务端如何做分布式推送、消息可靠性投递以及和消息队列的配合方案。如果你正打算把系统里的轮询改成实时推送或者已经在排查线上连接不稳定的问题欢迎关注后续内容。也欢迎在评论区聊聊你项目里遇过的WebSocket怪问题大家一起把坑踩平。
返回列表