10、Mesh架构实战:实现一个简单的多用户聊天室、管理多个PeerConnection

Mesh架构,说白了就是「每个人跟每个人直接连」。

你想想看,一个房间里4个人聊天,每个人都要跟其他3个人建立独立的连接。这就是Mesh。我最早接触WebRTC时,第一个实战项目就是写这个。当时觉得挺简单,结果一上手才发现——管理多个PeerConnection,坑比想象中多。

10.1 Mesh架构的核心逻辑

Mesh架构不依赖中心服务器转发数据。每个客户端既是发送者,也是接收者。每个PeerConnection负责跟一个远端用户通信。

举个例子:用户A要跟B、C、D聊天,A需要维护3个PeerConnection。B收到A的数据,同时也要跟C、D建立连接。这样一来,N个用户就需要N*(N-1)/2个连接。

核心要点:Mesh架构适合小规模房间(通常不超过6人)。人数多了,上行带宽和CPU压力会指数级增长。

我在项目中遇到过一个问题:用户开了摄像头,Mesh下每个人都要上传一份视频流给所有其他人。6个人同时开视频,上行带宽直接爆了。所以后来我们做了限制——Mesh只用于文本和文件传输,视频用SFU。

10.2 多用户聊天室的设计思路

我们先理清几个关键角色:

  • 信令服务器:负责交换SDP和ICE候选者。Mesh架构下,信令服务器不传媒体数据,只做协调。
  • 房间管理器:维护当前房间的用户列表。新用户加入时,通知所有老用户。
  • PeerConnection管理器:每个用户本地维护一个Map,key是远端用户ID,value是RTCPeerConnection实例。

嗯,这里要注意:信令服务器可以用WebSocket实现。我个人习惯用Socket.IO,因为它自带房间管理和自动重连,省不少事。

10.3 管理多个PeerConnection的难点

管理多个连接,说白了就是三个问题:

  1. 连接生命周期:什么时候创建?什么时候关闭?
  2. 状态同步:一个连接断开了,其他连接要不要受影响?
  3. 资源释放:离开房间时,所有连接必须干净地关闭,否则内存泄漏。

我曾经踩过的坑:没有及时关闭PeerConnection,导致浏览器标签页切后台后,摄像头灯还亮着。用户投诉说「你们是不是在偷拍我?」——其实只是连接没释放。

所以,我建议你用一个统一的连接管理器来封装所有操作。不要到处new RTCPeerConnection。

10.4 代码实现:连接管理器

下面是我常用的一个连接管理器模板。它负责创建、存储、销毁所有PeerConnection。

class PeerConnectionManager {
  constructor(localUserId) {
    this.localUserId = localUserId;
    this.peers = new Map(); // key: remoteUserId, value: RTCPeerConnection
    this.dataChannels = new Map(); // key: remoteUserId, value: RTCDataChannel
  }

  createPeerConnection(remoteUserId, config) {
    // 避免重复创建
    if (this.peers.has(remoteUserId)) {
      console.warn(`与 ${remoteUserId} 的连接已存在`);
      return this.peers.get(remoteUserId);
    }

    const pc = new RTCPeerConnection(config);

    // 监听ICE候选者
    pc.onicecandidate = (event) => {
      if (event.candidate) {
        // 通过信令服务器发送给远端
        this.sendCandidate(remoteUserId, event.candidate);
      }
    };

    // 监听连接状态变化
    pc.onconnectionstatechange = () => {
      console.log(`与 ${remoteUserId} 的连接状态: ${pc.connectionState}`);
      if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
        this.handleDisconnect(remoteUserId);
      }
    };

    // 保存到Map
    this.peers.set(remoteUserId, pc);
    return pc;
  }

  createDataChannel(remoteUserId, label) {
    const pc = this.peers.get(remoteUserId);
    if (!pc) {
      throw new Error(`与 ${remoteUserId} 的连接不存在`);
    }

    const dc = pc.createDataChannel(label, {
      ordered: true
    });

    dc.onopen = () => {
      console.log(`数据通道 ${label} 已打开`);
    };

    dc.onmessage = (event) => {
      // 处理收到的消息
      this.handleMessage(remoteUserId, event.data);
    };

    this.dataChannels.set(remoteUserId, dc);
    return dc;
  }

  closeAll() {
    // 关闭所有数据通道
    this.dataChannels.forEach((dc) => {
      dc.close();
    });
    this.dataChannels.clear();

    // 关闭所有PeerConnection
    this.peers.forEach((pc) => {
      pc.close();
    });
    this.peers.clear();

    console.log('所有连接已关闭');
  }

  handleDisconnect(remoteUserId) {
    console.log(`与 ${remoteUserId} 的连接断开`);
    // 可以在这里触发UI更新或重连逻辑
    this.peers.delete(remoteUserId);
    this.dataChannels.delete(remoteUserId);
  }
}

这段代码的核心思路就一个:统一管理,避免散落。你想想看,如果每个地方都new一个RTCPeerConnection,出问题了你都不知道哪个没关。

10.5 信令流程:用户加入与离开

当一个新用户加入房间时,流程是这样的:

  1. 新用户通过WebSocket发送「join」消息到信令服务器。
  2. 信令服务器广播「user-joined」给房间内所有老用户。
  3. 每个老用户创建一个新的PeerConnection,并发送Offer给新用户。
  4. 新用户收到Offer后,创建Answer,并回复。
  5. 双方交换ICE候选者,连接建立。

离开时更简单:用户发送「leave」消息,信令服务器广播「user-left」。每个老用户调用closeAll()中对应的那个连接。

小技巧:我习惯在「user-left」消息中附带一个reason字段。比如「timeout」表示网络断开,「manual」表示主动离开。这样UI可以显示不同的提示。

10.6 数据通道的消息格式

Mesh架构下,所有消息都通过RTCDataChannel传输。我建议定义一个统一的消息格式:

{
  type: 'chat',        // 消息类型:chat, file, system
  from: 'user123',     // 发送者ID
  to: 'all',           // 接收者:'all' 或 具体用户ID
  payload: {
    text: '大家好',
    timestamp: 1700000000
  }
}

为什么要加to字段?因为Mesh架构下,你可以实现「私聊」功能。消息先发给所有连接,但只有目标用户才显示。其他用户直接丢弃。

我在项目中遇到过一个问题:有人发了一条消息,结果所有客户端都显示了一遍。排查了半天,发现是消息广播逻辑写错了——每个PeerConnection收到消息后,又转发给了其他连接。说白了就是「消息风暴」。解决方案很简单:每个消息只处理一次,不转发

10.7 核心流程图

下面这张图展示了Mesh架构下,3个用户之间的连接关系和数据流向:

Mesh架构连接示意图(3用户) 用户A ID: 001 用户B ID: 002 用户C ID: 003 PC #1 PC #2 PC #3 数据流向 每个连接双向传输数据 总连接数 = N*(N-1)/2 = 3条 每个用户维护 2 个 PeerConnection

你看,3个用户只需要3条连接。每个用户维护2个PeerConnection。数据直接在用户之间传输,不经过服务器。

10.8 实际项目中的注意事项

问题 原因 解决方案
连接数过多 Mesh架构下,N个用户需要N*(N-1)/2个连接 限制房间人数(建议≤6人)
上行带宽爆炸 每个用户都要上传数据给所有其他人 文本聊天没问题,视频建议用SFU
ICE候选者交换失败 NAT穿透失败,尤其是对称NAT 部署TURN服务器作为中继
内存泄漏 PeerConnection未正确关闭 使用连接管理器统一管理生命周期

特别注意:不要在onicecandidate回调中直接操作DOM或触发重渲染。ICE候选者可能触发几十次,频繁操作DOM会导致页面卡顿。我建议用防抖或批量处理。

10.9 总结

Mesh架构实现多用户聊天室,核心就是管理好多个PeerConnection。说白了,你只需要做好三件事:

  • 用Map统一管理所有连接
  • 信令服务器只做协调,不传媒体
  • 离开房间时,干净地释放所有资源

我个人觉得,Mesh架构是理解WebRTC最好的入门方式。它让你直面「多连接管理」这个核心问题。等你把Mesh玩熟了,再去学SFU和MCU,会轻松很多。

嗯,代码写完了,记得测试一下。打开两个浏览器标签页,模拟两个用户,发几条消息试试。如果一切正常,恭喜你——你已经掌握了Mesh架构的精髓。