Node.js ws模块如何识别连接事件对应客户端列表的已有连接
我在NodeJS中使用ws模块实现WebSocket服务器,客户端通过webSocket = new WebSocket(url);发起连接,服务端在connection事件中通过wsServer.clients.size打印当前客户端总数。
测试发现:
- 不同标签页/不同浏览器打开页面,连接数正常递增,符合预期
- 刷新页面时,
connection事件会正常触发,但wsServer.clients.size不会递增
我在连接建立时会给ws实例挂载用户名、房间码等自定义属性,但刷新时触发connection事件拿到的是全新的ws实例,没有这些自定义属性,连接数却没有增加,我想搞清楚两个问题:
- ws包是如何识别该连接为已有连接的?
- 如何在代码中判断这种刷新重连的场景,避免刷新时误触发“用户加入房间”的广播?
以下是我当前的服务端实现代码:
// On connection wsServer.on("connection", function (ws, req) { let { query } = url.parse(req.url, true); ws.userName = ("name" in query) ? query.name : null; ws.roomCode = ("roomCode" in query) ? query.roomCode : null; ws.userPIN = ("PIN" in query) ? query.PIN : null; console.log(ws.userName + " joined room " + ws.roomCode); console.log("Total number of clients = " + wsServer.clients.size); let data = { userName: "Server", message: ws.userName + " joined the room." }; let dataStr = JSON.stringify(data); // Loop through each client wsServer.clients.forEach((client) => { // Check if client is ready and in same room if ((client.readyState === WebSocket.OPEN) && (client.roomCode == ws.roomCode)) { client.send(dataStr); } }); // On message event ws.on("message", function (msg) { // Loop through each client wsServer.clients.forEach((client) => { // Check if client is ready and in same room if ((client.readyState === WebSocket.OPEN) && (client.roomCode == ws.roomCode)) { client.send(msg.toString()); } }); }); });
1. 根本不存在“ws识别已有连接”的逻辑,计数不增只是时序巧合
首先明确核心事实:ws库本身没有任何识别老用户、复用旧连接的内置逻辑,每一次完成WebSocket握手的新请求,都会生成一个全新的ws实例,加入到wsServer.clients这个Set集合中。
你观察到的刷新时连接数不涨,原因非常简单:
浏览器触发页面刷新时,会先主动关闭当前页面持有的所有旧WebSocket连接,这个关闭请求的到达时间,往往比新页面发起的新连接请求早一点(或几乎同时)。而ws库自带连接清理逻辑:只要连接触发close事件,就会自动把对应的失效ws实例从wsServer.clients集合里移除。
相当于旧连接刚被踢出集合,新连接就被加入集合,一减一增数值刚好和刷新前持平,才会给你造成“连接被识别复用”的错觉。你可以在connection和close事件中都打印带时间戳的日志,就能清晰看到「旧连接关闭被移除→新连接加入」的完整时序。
2. 识别刷新重连场景的实现方案
因为每次新建连接都是独立的全新实例,你不可能靠ws对象本身判断用户身份,必须自己实现跨连接的用户唯一识别逻辑,步骤如下:
第一步:客户端生成并持久化唯一用户ID
用户首次进入页面时,在客户端生成一个唯一ID,存在localStorage中长期保存,只要用户不主动清理浏览器缓存,这个ID就不会变。建立WebSocket连接时,把这个ID作为参数拼到连接URL里传给服务端:
// 页面初始化时读取本地存储的uid,不存在就生成新的 let uid = localStorage.getItem('chat_uid') if (!uid) { uid = crypto.randomUUID() localStorage.setItem('chat_uid', uid) } // 建立连接时带上uid,和原有参数一起传给服务端 const webSocket = new WebSocket(`${wsUrl}?uid=${uid}&name=${userName}&roomCode=${roomCode}&PIN=${pin}`)
第二步:服务端维护独立的在线用户映射表
不要只依赖wsServer.clients统计在线用户,单独维护一个Map结构存储在线用户信息,用客户端传上来的uid作为key,存储对应用户的用户名、房间号、当前连接实例等信息。
第三步:连接建立时判断是否为重连,加离线判断缓冲
新连接进来时,先查在线用户表里有没有当前uid的记录:
- 有记录:说明是老用户刷新/网络波动重连,替换旧连接引用,不触发加入广播
- 无记录:说明是全新用户加入,再触发加入广播
另外注意,网络波动、页面刷新时,旧连接的close事件可能比新连接的connection事件晚到几百毫秒,不要在连接关闭时立刻把用户标记为离线,留3-5秒的缓冲窗口,避免误判。
改造后的服务端核心代码参考:
const wsServer = new WebSocketServer({ /* 你的原有配置 */ }) // 全局在线用户表:key=用户uid,value={ws, userName, roomCode, userPIN} const onlineUsers = new Map() // 离线判断缓冲时间,单位毫秒 const OFFLINE_BUFFER = 3000 wsServer.on("connection", function (ws, req) { const { query } = url.parse(req.url, true); const uid = query.uid ws.userName = query.name || null ws.roomCode = query.roomCode || null ws.userPIN = query.PIN || null // 判断是否为老用户重连 const isReconnect = onlineUsers.has(uid) if (isReconnect) { const oldUserInfo = onlineUsers.get(uid) // 关闭旧的重复连接,避免脏连接占用资源 if (oldUserInfo.ws.readyState === WebSocket.OPEN) { oldUserInfo.ws.close(1000, 'Repeat connection') } } // 更新在线用户表,把连接引用替换为新的ws实例 onlineUsers.set(uid, { ws, userName: ws.userName, roomCode: ws.roomCode, userPIN: ws.userPIN }) console.log(`${ws.userName} 连接到房间 ${ws.roomCode}`, isReconnect ? '(重连)' : '(新用户)') console.log(`当前总连接数:${wsServer.clients.size},当前在线用户数:${onlineUsers.size}`) // 仅新用户首次加入时广播加入提示 if (!isReconnect) { const joinMsg = JSON.stringify({ userName: "Server", message: `${ws.userName} 加入了房间` }) wsServer.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN && client.roomCode === ws.roomCode) { client.send(joinMsg) } }) } // 原有消息转发逻辑 ws.on("message", function (msg) { wsServer.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN && client.roomCode === ws.roomCode) { client.send(msg.toString()) } }) }) // 连接关闭处理 ws.on('close', () => { // 延迟判断,避免刷新/网络闪断时误判离线 setTimeout(() => { const currentUser = onlineUsers.get(uid) // 如果当前uid对应的连接还是刚关闭的这个实例,说明确实没有新连接上来,判定为真离线 if (currentUser && currentUser.ws === ws) { onlineUsers.delete(uid) // 这里可以按需广播用户离开的提示 console.log(`${ws.userName} 离开房间 ${ws.roomCode}`) } }, OFFLINE_BUFFER) }) })
内容的提问来源于stack exchange,提问作者Martin Vaughan

