房间管理机制:房间概念、创建房间、加入房间、离开房间、房间列表维护

聊到 WebRTC 信令服务器,房间管理绝对是个绕不开的核心话题。我刚开始做第一个多人视频会议项目时,就栽在房间管理上——用户一多,房间状态就乱套了。后来我花了整整两天重构了房间模块,才彻底搞明白这里面的门道。

说白了,房间就是一个虚拟的容器。它把一群想要互相通信的 peer 组织在一起。没有房间,你就没法控制谁和谁通话,也没法管理多人场景下的连接拓扑。

房间的概念

房间本质上是一个数据结构。它至少包含:

  • 房间 ID:唯一标识,通常用 UUID 或自增数字
  • 房间名称:用户可读的名字,比如「技术部周会」
  • 成员列表:当前在房间里的 peer 连接信息
  • 创建时间:用于超时清理
  • 房间状态:等待中、通话中、已关闭

我在项目中习惯用 Map 来存房间,key 是房间 ID,value 是房间对象。这样查找和删除都很快。

核心原则:房间是信令服务器的第一等公民。所有信令消息的流转,都以房间为上下文。

创建房间

创建房间的流程其实很简单。客户端发一个 create-room 消息,服务端生成房间 ID,存起来,然后返回给客户端。

嗯,这里要注意:创建房间的人,要不要自动加入?我个人建议自动加入。否则用户创建完房间还得再发一次 join,多此一举。

// 服务端处理创建房间
socket.on('create-room', (data, callback) => {
  const roomId = generateRoomId(); // 生成唯一ID
  const room = {
    id: roomId,
    name: data.name || `房间-${roomId}`,
    members: [],
    createdAt: Date.now(),
    status: 'waiting'
  };
  
  rooms.set(roomId, room);
  
  // 创建者自动加入房间
  room.members.push({
    socketId: socket.id,
    userId: data.userId,
    joinedAt: Date.now()
  });
  
  socket.join(roomId);
  
  callback({ success: true, roomId });
  
  // 广播房间列表更新
  broadcastRoomList();
});

我曾经踩过一个坑:没有限制房间数量。结果有人写脚本疯狂创建房间,直接把服务器内存打爆了。后来我加了一个上限,每个用户最多创建 5 个房间,这才消停。

小技巧:房间 ID 最好用短字符串,比如 6 位随机字母数字组合。太长的话,用户在 URL 里复制粘贴很痛苦。

加入房间

加入房间的逻辑稍微复杂一点。因为涉及到信令交互——新加入的人需要知道房间里已经有哪些人,老成员也需要知道来了新人。

你想想看,如果 A 和 B 已经在房间里了,C 要加入。C 需要和 A、B 分别建立连接。所以服务端要做两件事:

  1. 把 C 的信息推送给 A 和 B
  2. 把 A 和 B 的信息推送给 C
socket.on('join-room', (data, callback) => {
  const room = rooms.get(data.roomId);
  
  if (!room) {
    return callback({ success: false, error: '房间不存在' });
  }
  
  if (room.members.length >= MAX_MEMBERS) {
    return callback({ success: false, error: '房间已满' });
  }
  
  // 获取当前成员列表(排除自己)
  const existingMembers = room.members.map(m => ({
    socketId: m.socketId,
    userId: m.userId
  }));
  
  // 添加新成员
  const newMember = {
    socketId: socket.id,
    userId: data.userId,
    joinedAt: Date.now()
  };
  room.members.push(newMember);
  socket.join(data.roomId);
  
  // 通知老成员:新人来了
  socket.to(data.roomId).emit('user-joined', {
    userId: data.userId,
    socketId: socket.id
  });
  
  // 返回现有成员给新人
  callback({ success: true, members: existingMembers });
});

我记得有一次线上事故,就是因为加入房间时没有做重复检查。同一个用户点了两次加入,结果房间里出现了两个相同的 ID,信令消息全乱套了。从那以后,我每次加入前都会检查是否已经在房间里。

注意:加入房间时一定要做幂等处理。同一个 socket 重复加入同一个房间,应该直接返回成功,而不是报错。

离开房间

离开房间的处理,比加入要更谨慎。因为涉及到资源清理和通知扩散。

用户离开的原因有很多:主动关闭页面、网络断开、浏览器崩溃。每种情况都要处理好。

// 主动离开
socket.on('leave-room', (data, callback) => {
  const room = rooms.get(data.roomId);
  if (!room) return;
  
  // 从成员列表中移除
  room.members = room.members.filter(m => m.socketId !== socket.id);
  socket.leave(data.roomId);
  
  // 通知其他人
  socket.to(data.roomId).emit('user-left', {
    socketId: socket.id,
    userId: data.userId
  });
  
  // 如果房间空了,清理掉
  if (room.members.length === 0) {
    rooms.delete(data.roomId);
  }
  
  callback({ success: true });
});

// 被动断开
socket.on('disconnect', () => {
  // 遍历所有房间,找到这个 socket 所在的房间
  rooms.forEach((room, roomId) => {
    const index = room.members.findIndex(m => m.socketId === socket.id);
    if (index !== -1) {
      room.members.splice(index, 1);
      socket.to(roomId).emit('user-left', {
        socketId: socket.id
      });
      
      if (room.members.length === 0) {
        rooms.delete(roomId);
      }
    }
  });
});

我曾经遇到过一个诡异的问题:用户断开后,房间里的其他人收不到离开通知。查了半天才发现,是 disconnect 事件里忘记调用 socket.to(roomId) 了。这种低级错误,写代码时真的很容易忽略。

房间列表维护

房间列表的维护,说白了就是增删改查。但有几个细节值得注意:

  • 实时性:房间列表变化时,要主动推送给所有客户端
  • 过滤:不要把已满的房间、已关闭的房间展示给用户
  • 排序:按创建时间倒序,最新的房间排前面
  • 分页:如果房间数量多,要做分页,不然一次传输太多数据
// 广播房间列表
function broadcastRoomList() {
  const roomList = [];
  
  rooms.forEach(room => {
    if (room.status === 'active' || room.status === 'waiting') {
      roomList.push({
        id: room.id,
        name: room.name,
        memberCount: room.members.length,
        maxMembers: MAX_MEMBERS,
        createdAt: room.createdAt
      });
    }
  });
  
  // 按创建时间排序
  roomList.sort((a, b) => b.createdAt - a.createdAt);
  
  io.emit('room-list-update', roomList);
}

我习惯每 30 秒做一次房间清理。把那些创建超过 1 小时、且没有成员的僵尸房间删掉。不然时间久了,内存里全是没人用的房间对象。

建议:房间列表的更新不要过于频繁。每次增删成员时更新一次就够了。如果每秒都在广播,客户端渲染压力会很大。

房间状态流转图

下面这张图展示了房间从创建到销毁的完整生命周期。我画这张图的时候,特意把各种边界情况都标出来了。

房间生命周期状态流转 等待中 Waiting 通话中 Active 已关闭 Closed 创建房间 加入成功 全员离开 超时/手动关闭 所有人离开 状态说明 • 等待中:房间已创建,至少有一人在房间内,但尚未开始通话 • 通话中:房间内有两人或以上,正在进行媒体协商和数据传输 • 已关闭:房间内无人,或被管理员手动关闭,资源已释放 注:虚线箭头表示非正常流转路径,通常由超时或异常触发 房间超时时间:创建后 1 小时无活动自动关闭 最大成员数:每个房间最多 8 人(可根据业务调整)

总结一下房间管理的要点

操作 关键点 常见坑
创建房间 生成唯一ID,创建者自动加入 忘记限制房间数量
加入房间 通知新旧成员,返回现有成员列表 重复加入未做幂等处理
离开房间 清理成员,通知其他人,空房间删除 disconnect 事件漏掉通知
列表维护 实时推送,过滤无效房间,排序分页 僵尸房间占用内存

房间管理看起来简单,但实际做起来细节很多。我建议你在写代码之前,先把状态流转图画清楚。把各种边界情况都列出来,比如:房间满了怎么办?创建者离开了房间要不要销毁?这些都想清楚了,写代码就是水到渠成的事。

嗯,关于房间管理就先聊到这里。如果你在实际开发中遇到什么奇怪的问题,欢迎来公众号找我聊聊。


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