You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 11:27:22