ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

WebSocket长连接实战:心跳检测、断线重连与房间广播

WebSocket长连接实战:心跳检测、断线重连与房间广播

简介:面向 WebSocket 即时通信入门与跨语言联调场景的实例源码包,包含 HTML5 客户端与 Java 服务端两个可运行工程,适合前端、Java 后端初学者及需要快速验证长连接通信的开发者。客户端基于浏览器 WebSocket API,运行需 Tomcat 7 以上;服务端仅依赖 JDK 7 以上环境,可直接启动并用任意语言客户端测试,也支持用该客户端对接其他语言服务端,便于排查握手、连接保持与消息收发问题。压缩包共 16 个文件,约 16KB,以工程配置文件、Java 源码与编译类、JSP 页面、XML 配置及客户端脚本为主,另含 IDE 元数据,结构轻量、导入方便。已有 1264 人学习下载。通过对照客户端与服务端代码,读者可掌握 WebSocket 连接建立、消息推送和双向通信的基本流程,并据此扩展实时通知、聊天等场景;对于想弄清握手升级与连接保持细节的开发者,也可直接导入工程对照调试,减少从零搭建环境的时间。

1. 从一次「连接还在,消息没了」说起

联调那天,浏览器的 Network 面板里那条 WebSocket 客户端与服务端之间的连接明晃晃标着 101 Switching Protocols,看起来一切正常,可对面十分钟没收到一条消息。抓包才发现,连接早在第 60 秒就被中间层静默掐断,前端还停在OPEN状态傻等,send()调用全部落进黑洞。这类事故很少是 WebSocket 协议本身的问题,绝大多数出在连接池、心跳探活、断线重连这几段胶水代码上——协议只负责「连上」,连住是工程活。

这份 WebSocket 客户端与服务端实例源码,补的正是「连上」之后那段路:服务端的连接注册与踢重、房间广播、ping/pong 探活,浏览器端的指数退避重连、离线消息队列,外加一个 Node 写的多连接压测脚本。刚学会new WebSocket(url)、一上线就掉线的人,和想给现有 HTTP 服务补一条长连接通道的后端,都能直接用。

2. 双端工程结构:一条消息从发出到落地的完整路径

先把结构看明白再动手,比一上来就抄ws.send()强。长连接项目和普通 HTTP 接口最大的差别在于:HTTP 是一次请求一次响应,无状态;WebSocket 是一条常驻的 TCP 通道,谁在线上、谁掉线了、掉线期间的业务消息去哪,全得自己记账。所以这份实例把代码拆成了「连接管理」「房间」「协议」三块,而不是全塞进一个index.js。

2.1 目录结构与三份入口文件

websocket-demo/ ├── server/ │ ├── index.js # HTTP 服务 + WebSocket 升级入口 │ ├── connection.js # 连接池、心跳标记、踢重 │ ├── room.js # 房间维护与广播 │ └── protocol.js # 消息解析、类型路由、错误兜底 ├── client-browser/ │ ├── index.html # 演示页,带输入框和消息列表 │ └── ws-client.js # 浏览器端封装:重连 + 队列 + 心跳 ├── client-node/ │ └── bench.js # Node 客户端,兼压测脚本 └── package.json

三份入口各管一件事:server/index.js是服务端唯一进程入口,负责把 HTTP 请求和 Upgrade 请求分开处理;client-browser/ws-client.js是给页面用的封装类,业务代码只跟它打交道;client-node/bench.js不参与业务,专门用来在本地起几十上百条连接,验证服务端在并发下的表现。

我一般会把connection.js和room.js的边界划得很清楚:前者只认「用户」,后者只认「房间」。想加私聊就走clients.get(userId),想加群发就走rooms.get(roomId),两条路径不互相污染。真到要换成 Redis 存连接状态那天,改的也只是这两个文件。

依赖只有两个:运行时用ws这个库(Node 生态里最常用的 WebSocket 实现),调试时用wscat命令行客户端。package.json里"type"保持默认的 CommonJS,示例代码直接用require,省掉一层模块系统的解释成本。

2.2 消息协议:别让线上跑裸字符串

原型阶段最容易犯的错,是ws.send('hello')发裸字符串,接收端拿msg === 'hello'判断。等到要加「谁发的」「第几条」「什么时候发的」就傻眼,只能靠字符串拼接,越写越像正则解析现场。这份实例约定了统一的信封结构,业务数据一律塞进data:

字段类型说明
typestring消息类型,路由依据:auth/join/chat/ping/pong/system
seqnumber客户端自增序号,用于去重和确认
tsnumber毫秒时间戳,服务端算 RTT、排查时序问题都靠它
dataobject业务载荷,不同类型结构不同
codenumber仅下行错误帧使用,如4001被顶号、4003无权限

type是整套协议的核心。服务端收到帧后先看type决定往哪个处理函数走,未知类型统一回一条{ type: 'error', code: 4000 },而不是静默丢弃——丢消息不报错,是排查成本最高的一种设计。

seq配合服务端的ack做去重。客户端重连后会重发队列里的消息,如果服务端不认seq,用户就会看到同一条聊天记录出现两遍。简单做法是服务端给每个连接维护一个最近 100 条seq的 Set,命中就跳过。

ts不要用服务端时间覆盖,客户端原样带上来,服务端在pong里回填同一个ts,这样算出来的往返延迟才准。

2.3 连接生命周期:握手、鉴权、就绪、关闭

一条 WebSocket 连接从生到死要经过四道关,每道关都有对应的失败码:

  1. TCP 连接 + HTTP Upgrade:客户端发GET /?token=xxx,带Upgrade: websocket。服务端在这一步校验 token,不合格直接回 401 并断开,不要让它升到 WebSocket 层再close()——那时候已经占了一个连接对象。
  2. 升级成功,触发connection:此时readyState为OPEN,但业务上还不算「就绪」,因为用户身份可能还没确认。
  3. 首帧鉴权或业务注册:用 URL 参数带 token 的做法最省事,但 token 会进网关和访问日志;讲究一点的做法是连上后第一帧发{ type: 'auth', data: { token } },服务端在规定时间内收不到就踢。
  4. 关闭:正常关闭用1000,被顶号用4001,心跳超时用terminate()直接掐 TCP(关不掉的不必客气)。

close事件里的code值得单独看一眼。1006是「异常关闭,没有收到关闭帧」,几乎全部由网络中断、中间层空闲超时、服务端进程被杀引起;1000才是双方好好说再见。线上盯1006的占比,比看任何监控曲线都直观。

3. 服务端核心拆解:连接池、房间广播与心跳检测

服务端代码看起来不多,但每一段都对应一个线上事故。下面按「请求怎么进来 → 连接怎么登记 → 消息怎么分发 → 死连接怎么清」的顺序拆。

3.1 用 HTTP 服务接管 Upgrade,把鉴权放在握手阶段

// server/index.js const http = require('http'); const { WebSocketServer } = require('ws'); const { register, unregister } = require('./connection'); const { route } = require('./protocol'); const server = http.createServer((req, res) => { // 普通 HTTP 请求仍走这里:健康检查、静态页都靠它 res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('ws server ok\n'); }); // noServer: true 表示不自动挂载,握手升级由我们自己接管 const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 }); server.on('upgrade', (req, socket, head) => { const url = new URL(req.url, 'http://localhost'); const token = url.searchParams.get('token'); if (!token) { // 握手阶段直接拒绝,比连上之后再 close 省一个连接对象 socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); return; } wss.handleUpgrade(req, socket, head, (ws) => { ws.userId = token; // 真实项目里这里换成验签结果 wss.emit('connection', ws, req); }); }); wss.on('connection', (ws) => { register(ws); ws.on('message', (raw) => route(ws, raw)); ws.on('close', () => unregister(ws)); }); server.listen(8080, '0.0.0.0', () => { console.log('listening on 0.0.0.0:8080'); });

noServer: true是这段代码的关键。默认模式下ws会把 Upgrade 逻辑直接挂到 server 上,你就失去了在握手阶段做鉴权的机会。手动接管后,token校验失败可以直接写一个 HTTP 响应再destroy,客户端看到的是标准的 401,而不是一个莫名其妙的1006。

maxPayload必须设。不设的话,一个恶意客户端发 2GB 帧就能把进程内存吃掉。64KB 对文本聊天足够,要传文件就走分片或干脆换 HTTP 上传,别拿长连接当文件通道。

server.listen(8080, '0.0.0.0')里的0.0.0.0也是血泪经验:只监听127.0.0.1的话,宿主机外面、容器外面一律连不上,本地自测却完全正常,排查半天。

3.2 连接池:userId 到 ws 的映射与踢重

// server/connection.js const clients = new Map(); // userId -> ws const HEARTBEAT_MS = 30_000; function register(ws) { const old = clients.get(ws.userId); if (old && old !== ws) { // 同一账号重复登录:踢掉旧连接,避免消息双发 old.close(4001, 'replaced by new connection'); } clients.set(ws.userId, ws); ws.isAlive = true; ws.on('pong', () => { ws.isAlive = true; }); // 收到 pong 才算活着 } function unregister(ws) { // 必须判断是不是同一个对象,否则会把新连接误删 if (clients.get(ws.userId) === ws) clients.delete(ws.userId); } const timer = setInterval(() => { for (const ws of clients.values()) { if (ws.isAlive === false) { ws.terminate(); // 直接掐 TCP,不陪它走关闭握手 continue; } ws.isAlive = false; // 先置 false,下个周期还没被 pong 翻回来就清 ws.ping(); } }, HEARTBEAT_MS); timer.unref(); // 不阻止进程退出 module.exports = { register, unregister, clients };

unregister里那句=== ws的判断是必须的。用户网络抖动重连时,新连接先注册、旧连接后触发close,如果close回调里无脑clients.delete(userId),删掉的就是新连接,表现是「刚连上几秒就收不到消息」,而且日志里一切正常。

old.close(4001)这个自定义关闭码也有讲究。标准码 1000-1015 是协议保留的,4000-4999 留给应用层,4001 表示「被新连接取代」。前端拿到这个码就不该自动重连,否则两台设备会互相踢,无限循环。

ws.isAlive的翻牌逻辑是:每 30 秒遍历一次,先把所有连接标记为「死」,然后发 ping;收到 pong 的回调把它翻回「活」。下一轮遍历时还是「死」的,说明这一个周期内对方毫无响应,直接terminate()。这个思路比自己维护超时计时器简单得多,也避开了定时器泄漏。

3.3 房间与广播:空房间回收和背压处理

// server/room.js const rooms = new Map(); // roomId -> Set<ws> function join(ws, roomId) { if (!rooms.has(roomId)) rooms.set(roomId, new Set()); rooms.get(roomId).add(ws); ws.roomId = roomId; } function leave(ws) { const set = rooms.get(ws.roomId); if (!set) return; set.delete(ws); if (set.size === 0) rooms.delete(ws.roomId); // 空房间及时回收 } function broadcast(roomId, payload, except) { const set = rooms.get(roomId); if (!set) return 0; const data = JSON.stringify(payload); // 序列化一次,别在循环里做 let sent = 0; for (const ws of set) { if (ws === except) continue; if (ws.readyState !== ws.OPEN) continue; // 半开连接不发 if (ws.bufferedAmount > 1 << 20) continue; // 积压超过 1MB 直接跳过 ws.send(data); sent++; } return sent; }

JSON.stringify提到循环外,是广播函数最容易忽略的性能点。一个 500 人的房间,放循环里就是 500 次序列化同一份数据。

bufferedAmount这个判断是背压(backpressure)的手动兜底。ws.send()是同步返回的,数据先进内核缓冲区,网络慢的客户端会一直积压。积压到几十兆,进程内存就开始不受控。超过 1MB 就跳过这个客户端,让他下次拉取历史消息补上,比把服务端拖死划算。

leave里删空房间那两行同样不能少。房间 Map 只增不减,跑一周就是典型的内存泄漏,用process.memoryUsage()看 heapUsed 一路向上不回落,八成就是它。

3.4 消息路由:一个入口,按 type 分发

// server/protocol.js const { join, leave, broadcast } = require('./room'); const handlers = { join(ws, msg) { join(ws, msg.data.roomId); broadcast(msg.data.roomId, { type: 'system', data: { text: `${ws.userId} 进入房间` }, }, ws); }, leave(ws, msg) { leave(ws); ws.send(JSON.stringify({ type: 'system', data: { text: '已离开' } })); }, chat(ws, msg) { broadcast(msg.data.roomId, { type: 'chat', seq: msg.seq, ts: msg.ts, data: { from: ws.userId, text: msg.data.text }, }, ws); }, ping(ws, msg) { // 浏览器端发不了协议层 ping,只能用业务帧做心跳 ws.send(JSON.stringify({ type: 'pong', ts: msg.ts })); }, }; function route(ws, raw) { let msg; try { msg = JSON.parse(raw); } catch { return ws.send(JSON.stringify({ type: 'error', code: 4000, data: 'bad json' })); } const handler = handlers[msg.type]; if (!handler) { return ws.send(JSON.stringify({ type: 'error', code: 4000, data: 'unknown type' })); } try { handler(ws, msg); } catch (err) { // 单个业务异常不能影响整条连接 console.error('handler error', msg.type, err.message); ws.send(JSON.stringify({ type: 'error', code: 4500, data: 'server error' })); } }

三个try/catch的位置分别防三件事:解析失败防脏数据打崩进程,未知类型防前端版本不一致时静默丢消息,处理函数异常防一条业务错误拖垮整个连接。真在线上跑,第二层外面还要包一个限流,同一个连接每秒超过 20 帧就断开——长连接上的刷屏比 HTTP 接口好防得多,因为连接是有状态的。

4. 客户端核心拆解:重连退避、消息队列与 Node 压测端

服务端再怎么稳,客户端网络也会断——地铁进隧道、笔记本合盖、Wi-Fi 切 4G。客户端代码的职责就一句话:让用户感知不到断线。

4.1 浏览器端封装:状态机、指数退避与抖动

// client-browser/ws-client.js class WSClient { constructor(url, opts = {}) { this.url = url; this.queue = []; // 断线期间待发的消息 this.retry = 0; this.maxRetryDelay = opts.maxRetryDelay || 30_000; this.heartbeatMs = opts.heartbeatMs || 25_000; this.seq = 0; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.binaryType = 'arraybuffer'; // 二进制帧统一成 ArrayBuffer this.ws.onopen = () => { this.retry = 0; this.flush(); // 连上先把积压发出去 this.startHeartbeat(); }; this.ws.onmessage = (ev) => this.handle(ev.data); this.ws.onclose = () => { this.stopHeartbeat(); this.scheduleReconnect(); }; this.ws.onerror = () => this.ws.close(); // 出错后主动 close,触发重连 } scheduleReconnect() { // 指数退避 + 抖动:服务端重启时别让所有客户端同时扑上来 const base = Math.min(1000 * 2 ** this.retry, this.maxRetryDelay); const delay = base / 2 + Math.random() * base / 2; this.retry++; setTimeout(() => this.connect(), delay); } send(type, data) { const msg = { type, seq: ++this.seq, ts: Date.now(), data }; if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(msg)); } else { if (this.queue.length > 200) this.queue.shift(); // 队列封顶,丢最老的 this.queue.push(msg); } return msg.seq; } flush() { while (this.queue.length && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(this.queue.shift())); } } startHeartbeat() { // 浏览器发不了协议层 ping 帧,只能在业务层发 this.hb = setInterval(() => this.send('ping', null), this.heartbeatMs); } stopHeartbeat() { clearInterval(this.hb); } handle(raw) { let msg; try { msg = JSON.parse(raw); } catch { return; } if (msg.type === 'pong') return; // 心跳响应不往上抛 window.dispatchEvent(new CustomEvent('ws-message', { detail: msg })); } }

Math.random()那半段抖动必须加。不加的话,服务端发版重启,几千个客户端在同一毫秒发起重连,直接把服务打挂,形成「越挂越重连、越重连越挂」的雪崩。

onerror里主动close()也是踩坑改出来的。部分浏览器的onerror触发后不会自动触发onclose,不手动关一下,这条连接就永远停在CLOSING,重连逻辑一次都不会跑。

flush()的顺序有讲究:先flush再开心跳。反过来的话,心跳包可能插到积压消息前面,服务端会先收到ping后收到join,路由就乱了。

4.2 离线队列的边界:哪些消息值得等,哪些该丢

队列封顶 200 条、超了丢最老的,这个策略是故意的。聊天场景里,用户断网半小时后重连,把 500 条历史输入一次性刷上去没有意义,反而会冲垮服务端。丢老保新更符合直觉。

但队列不是万能的。必须区分两类消息:chat这种可以补发,ping这种补发毫无意义甚至可以清掉,auth这种必须在连接建立后立刻重发。示例里用send统一入队,真实项目我会给send加第三个参数{ persist: true },心跳类调用传false,直接丢弃不入队。

客户端侧服务端还应该有一个ack机制:服务端收到seq后回{ type: 'ack', seq },客户端收到才从本地待确认列表移除。不做ack的话,消息发出去了但服务端在处理中途崩溃,这条消息就永久丢了,用户还以为发出去了。

4.3 Node 客户端:给联调一个可重复的入口

// client-node/bench.js const WebSocket = require('ws'); const N = Number(process.argv[2] || 50); // 连接数,默认 50 const DURATION = 10_000; // 每连接持续发心跳 10 秒 let connected = 0, received = 0, rttSum = 0; for (let i = 0; i < N; i++) { const ws = new WebSocket(`ws://127.0.0.1:8080/?token=bench-${i}`); ws.on('open', () => { connected++; const timer = setInterval(() => { ws.send(JSON.stringify({ type: 'ping', ts: Date.now(), data: null })); }, 1000); setTimeout(() => { clearInterval(timer); ws.close(); }, DURATION); }); ws.on('message', (buf) => { const msg = JSON.parse(buf); if (msg.type === 'pong') { received++; rttSum += Date.now() - msg.ts; } }); ws.on('close', () => { connected--; }); } setTimeout(() => { console.log(`连接 ${N} 收到 pong ${received} 平均 RTT ${(rttSum / received || 0).toFixed(1)}ms`); process.exit(0); }, DURATION + 1000);

用法是node client-node/bench.js 500。它干三件事:批量建连、每秒发一次心跳、统计平均往返延迟。500 条连接跑完看两个数——平均 RTT 是否随连接数线性增长(说明服务端有阻塞),以及连接数是否回到 0(说明有连接没正确关闭)。

这个脚本最大的价值不是压测,是让「复现问题」变成一条命令。用户反馈「连不上」,你把参数调到 1,看单条连接能不能通,比问用户「你刷新一下试试」高效得多。真要做压力测试,ws客户端在单进程下几千连接就吃满 CPU,得上多进程或换工具。

5. 启动与联调避坑:五条最容易翻车的连接问题

到这一步,代码该看的都看了,接下来把它跑起来,然后把那几个经典坑提前踩掉。

5.1 三条命令跑通最小闭环

# 1. 安装依赖 npm install ws # 2. 起服务端(建议用 npx nodemon 或 node --watch,改代码自动重启) node --watch server/index.js # 3. 另开一个终端,用 wscat 模拟一个客户端 npx wscat -c "ws://127.0.0.1:8080/?token=u1"

服务端起来后先别急着开浏览器,用wscat连一条,手敲{"type":"ping","ts":1},看到回{"type":"pong","ts":1}就说明路由通了。这一步能省掉一半「到底是前端问题还是后端问题」的扯皮。

浏览器端不用打包工具,npx serve client-browser或者用编辑器自带的静态服务起一个页面就行,然后在控制台里new WSClient('ws://127.0.0.1:8080/?token=u1'),调到ws-message事件就能看到下发的消息。

提示:页面用https://打开时,ws://会被浏览器按混合内容拦掉,必须换成wss://。本地开发用http://打开页面最省事。

5.2 帧级验证:DevTools 和 wscat 各看什么

浏览器 F12 的 Network 面板里,WS 标签页能看三样东西,值得养成习惯:

第一是握手请求的 Headers,确认Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key都在,响应码是 101。如果看到 200,说明请求根本没走到升级逻辑,大概率是中间层的路由规则把/全接走了。

第二是 Messages 列表,能看到每一帧的收发方向和内容。绿色向上是客户端发,白色向下是服务端发。排查「消息发出去没」这个问题,这一栏比任何日志都直接。

第三是连接时长。连接建立时间和关闭时间挨得很近,又没有关闭帧,就是被外部掐断。这时候要去看中间层(负载均衡、网关、容器入口)的空闲超时配置,常见默认值是 60 秒,比你的心跳间隔还短。

wscat在服务端视角看问题。npx wscat -l 8080起一个服务端,让你的 Web 客户端连它,就能快速确认客户端代码本身有没有问题。两边交叉验证,定位速度能快一倍。

5.3 五条踩坑记录,按现象逐条对照

现象一:握手 101 成功,两三秒后客户端收到 1006,没有任何关闭帧。原因是中间层空闲超时。多数负载均衡默认 60 秒没数据就断连接,而你的心跳设了 90 秒,永远等不到。解决办法是把心跳间隔压到 25~30 秒,同时在接入层把空闲超时调到 300 秒以上。两边都改,只改一边迟早复发。

现象二:服务端跑一晚上内存涨到 2GB,重启就好。八成是close事件里没清连接池,或者房间 Map 只set不delete。改法是所有注册动作都配一个对称的注销动作,connection.js的unregister和room.js的leave必须挂在close回调上。再加一条:setInterval创建的定时器记得unref()或在下线时clearInterval。

现象三:广播给 100 人,总有几个人收不到,日志显示send成功。ws.send()成功只代表写进了内核缓冲区,不代表对方收到了。慢客户端积压到一定程度,连接会被中间层判定为异常。改法是广播前检查bufferedAmount,超过阈值就跳过,让对方重连后拉历史消息。同时确认readyState === OPEN,CLOSING状态的连接发出去也是白发。

现象四:浏览器端心跳定时器在,连接还是被判断为死连接。浏览器 WebSocket API 不暴露ping()方法,协议层的 ping/pong 是浏览器底层自动处理的,业务代码看不到。如果服务端只认协议层pong,而浏览器端只能发业务层ping,两边就对不上。改法是服务端同时处理两种:ws.on('pong')翻isAlive,业务层ping也翻一次,两条路都通。

现象五:本地连得好好的,部署到容器里就连不上。按顺序查三处:listen的地址是不是0.0.0.0;容器端口有没有映射出来;健康检查是不是打了 HTTP 路径却配了 WS 端口。还有一个隐蔽的:网关的健康检查会定期发 HTTP 请求,如果那个路径恰好被你的 Upgrade 逻辑接走了,连接会被当成异常关闭。

6. 二次开发:从能跑的示例到敢上线的服务

示例代码解决的是「跑通」,离「敢上线」还差三块。这三块不补,前面所有代码在真实流量面前都撑不过一周。

6.1 鉴权从 URL 参数挪到首帧

?token=xxx的问题不是不安全,是太容易泄漏——进浏览器历史、进网关访问日志、进 Referer。改造方式是连上后 3 秒内必须收到auth帧,超时直接close(4003):

// 改造点:握手阶段只建连,身份在首帧确认 wss.on('connection', (ws) => { const authTimer = setTimeout(() => { ws.close(4003, 'auth timeout'); }, 3000); ws.once('message', (raw) => { const { type, data } = JSON.parse(raw); if (type !== 'auth') return ws.close(4003, 'auth required'); const userId = verifyToken(data.token); // 换成你的验签逻辑 if (!userId) return ws.close(4003, 'invalid token'); clearTimeout(authTimer); ws.userId = userId; register(ws); // 鉴权通过才进连接池 }); });

注意register挪到了鉴权之后。之前的写法一建连就进池子,未鉴权连接也占着userId的槽位,被人刷连接就是内存泄漏。

6.2 多实例部署:广播不能只发本机

单机roomsMap 在多副本部署下必然出问题:用户 A 连在实例 1,用户 B 连在实例 2,两人同房间却互相收不到消息。改造方向是引入一个消息中间件,把广播从「遍历本地 Set」变成「发一条主题消息,各实例订阅后发给本机连接」:

改造点单机示例多实例做法
连接状态本地Map本地 Map + 实例 ID 前缀
广播遍历本地 Set发到消息中间件的房间主题
在线人数set.size各实例上报后汇总
踢重本地查 Map需要跨实例的会话表

迁移时把broadcast()抽成一个接口,单机实现直接遍历,集群实现发主题消息。上层路由代码一行不用改,这是当初把广播独立到room.js的回报。

6.3 上线前我会强制走一遍的验证清单

  1. 拔网线 30 秒再插回来,客户端能否自动重连、队列消息是否补发成功。
  2. 服务端kill -9,客户端重连时的退避曲线是否平滑、有没有出现同时重连。
  3. client-node/bench.js 1000跑完,服务端内存是否回落到基线。
  4. 用wscat手发一条 1MB 的帧,看maxPayload是否按预期拒绝。
  5. 中间层空闲超时改成 30 秒,看心跳是否仍能维持连接。

这套清单是我踩过两次「发版后十分钟集体掉线」之后固定下来的。WebSocket 的问题有个特点:本地永远正常,只在真实网络和真实中间层下暴露。从那以后,我每次改完连接相关代码,都会先把退避和心跳两个参数打印到日志里,确认它们和接入层的配置对得上,再谈上线。

希望帮到你。

本文还有配套的精品资源,点击获取

返回列表