用户管理:用户 ID 生成、连接状态跟踪与房间映射

用户管理这块,说白了就是信令服务器的「户口本」。你想想看,一个 WebRTC 房间里有好几个人,谁是谁?谁在线谁掉线了?谁在哪个房间?这些信息如果理不清,后面所有逻辑都会乱套。

我在早期做一个小型视频会议项目时,就吃过这个亏。当时图省事,直接用随机字符串当用户 ID,结果日志里全是乱码,排查问题简直要命。后来才老老实实把用户管理这块做扎实了。

用户 ID 生成策略

用户 ID 怎么生成?我个人习惯分两种情况考虑:

  • 临时用户:比如匿名访问、快速加入会议。用 UUID 或 nanoid 生成即可。
  • 注册用户:用业务系统里的用户 ID,比如数据库自增 ID 或邮箱。

信令服务器里,我建议统一用 userId 字段,不管哪种场景。这样后续做状态跟踪时,接口设计更干净。

核心原则:用户 ID 必须全局唯一,且在会话期间不可变。掉线重连后,同一个用户应该用同一个 ID。

来看一段简单的 ID 生成代码:

// 使用 nanoid 生成临时用户 ID
const { nanoid } = require('nanoid');

function generateUserId(type = 'temp') {
  if (type === 'temp') {
    return 'user_' + nanoid(12); // 例如:user_3kf9a2m1x7bq
  }
  // 注册用户由外部传入
  return null;
}

嗯,这里要注意:nanoid 的碰撞概率极低,但如果你做的是金融级应用,建议加个时间戳前缀。

用户连接状态跟踪

用户连上信令服务器后,状态会变。我把它分为三种:

状态 含义 触发时机
online WebSocket 连接正常 连接建立时
offline 连接断开 WebSocket close/error
reconnecting 正在重连 检测到断线后

我在项目中遇到过一个问题:用户网络闪断,服务器立刻标记为 offline,然后房间里的其他人看到对方消失了。等用户重连回来,又要重新走一遍加入流程。用户体验很差。

后来我加了一个「心跳超时」机制:

  • 客户端每 5 秒发一次心跳
  • 服务器 15 秒没收到心跳,才标记 offline
  • 中间如果心跳恢复,状态保持 online

小技巧:用 Map 来存储用户状态,比用对象快得多。尤其是几千人同时在线的场景。

// 用户状态存储
const userStates = new Map();

function updateUserState(userId, state) {
  userStates.set(userId, {
    state,
    lastHeartbeat: Date.now(),
    socketId: socket.id
  });
}

// 心跳检测
function checkHeartbeat() {
  const now = Date.now();
  for (const [userId, info] of userStates) {
    if (now - info.lastHeartbeat > 15000) {
      userStates.set(userId, { ...info, state: 'offline' });
      // 通知房间内其他用户
      notifyRoom(userId, 'offline');
    }
  }
}

用户与房间的映射关系

这是最核心的部分。一个用户可以在多个房间吗?理论上可以,但实际项目中我建议限制为「一个用户同时只能在一个房间」。为什么?

你想想看,如果用户同时在房间 A 和房间 B 说话,音频流怎么处理?视频流往哪发?信令逻辑会变得极其复杂。我见过有人这么设计,最后调试到崩溃。

映射关系我用两层 Map 来维护:

  1. roomUsers:房间 → 用户列表
  2. userRoom:用户 → 所在房间
// 房间与用户的映射
const roomUsers = new Map(); // key: roomId, value: Set<userId>
const userRoom = new Map();  // key: userId, value: roomId

function joinRoom(userId, roomId) {
  // 如果用户已在其他房间,先离开
  if (userRoom.has(userId)) {
    leaveRoom(userId);
  }

  // 加入新房间
  if (!roomUsers.has(roomId)) {
    roomUsers.set(roomId, new Set());
  }
  roomUsers.get(roomId).add(userId);
  userRoom.set(userId, roomId);
}

function leaveRoom(userId) {
  const roomId = userRoom.get(userId);
  if (!roomId) return;

  const users = roomUsers.get(roomId);
  if (users) {
    users.delete(userId);
    if (users.size === 0) {
      roomUsers.delete(roomId); // 房间没人了,清理掉
    }
  }
  userRoom.delete(userId);
}

注意:用户断开连接时,一定要调用 leaveRoom。我曾经漏掉这个,结果房间里残留了大量「幽灵用户」,新加入的人能看到一堆不存在的参与者。

整体数据流

我把用户管理的核心流程画了一张图,方便你理解:

用户管理核心数据流 客户端 A 客户端 B 客户端 C 信令服务器 用户状态管理 房间映射管理 内存数据 userStates Map roomUsers Map userRoom Map socketId 映射 状态通知 客户端通过 WebSocket 连接信令服务器,服务器维护三张映射表 用户状态变更时,服务器主动通知房间内其他客户端

实际项目中的避坑指南

我曾经在一个项目中,用户量到了 5000 人同时在线时,服务器内存暴涨。排查后发现是用户断开时没有清理 Map 里的旧数据。这里分享几个经验:

  • 连接断开一定要清理:WebSocket 的 close 事件里,把用户从所有 Map 中移除。
  • 定期做健康检查:每隔 30 秒扫描一次 userStates,把超过 30 秒没心跳的用户强制下线。
  • 房间人数上限:我一般限制一个房间最多 50 人。超过时返回错误码,而不是默默拒绝。
  • 日志要打全:用户加入、离开、状态变化,全部打日志。排查问题时这些日志就是救命稻草。

最后一个小建议:用户 ID 不要用 socket.id。因为 socket.id 每次重连都会变,而用户 ID 应该保持不变。把 socket.id 作为临时索引,用户 ID 作为业务主键,两者分开管理。

用户管理这块,说白了就是「增删改查」四个字。但每个细节都关系到系统的稳定性。把上面这些点做好了,你的信令服务器就成功了一半。

公众号:蓝海资料掘金营,微信 deep3321