RTCPeerConnection 基础:创建实例、配置服务器、ICE 候选者收集与交换

好,咱们正式开始动手了。这一章要聊的,是 WebRTC 最核心的一个对象——RTCPeerConnection。说白了,它就是两个浏览器之间的一条“虚拟网线”。没有它,数据通道、音视频流都无从谈起。

我个人习惯把 PeerConnection 比作一个“电话交换机”。你拨号、振铃、接通,背后全是它帮你搞定的。咱们今天就把这个交换机拆开看看。

1. 创建 PeerConnection 实例

创建实例本身很简单,一行代码的事:

const pc = new RTCPeerConnection();

但这里有个坑——默认配置下,它只能连局域网。为什么?因为没给人家配“导航”啊。你想想看,两个设备在不同的 NAT 后面,互相不知道对方的公网 IP,怎么连?

所以,实际项目中我们几乎不会用空参数。我会这样写:

const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:your-turn-server.com:3478',
      username: 'user',
      credential: 'pass'
    }
  ]
};
const pc = new RTCPeerConnection(config);

重要:STUN 服务器是“问路”的,TURN 服务器是“当中间人”的。能直连就别用 TURN,因为 TURN 会消耗服务器带宽,成本不低。

我在项目中遇到过一个问题:只配了 STUN,结果某些企业网络环境下死活连不上。后来抓包一看,原来是 UDP 被防火墙封了。加了 TURN 服务器之后,走 TCP 中继才搞定。

2. 配置 STUN/TURN 服务器

咱们展开聊聊 STUN 和 TURN 的区别。我画了张图,一看就明白:

STUN vs TURN 工作流程 STUN 模式(直连) Peer A STUN 服务器 ① 问我的公网IP ② 返回公网IP Peer A Peer B ③ 直接P2P连接 TURN 模式(中继) Peer A TURN 服务器 ① 数据发给TURN Peer B TURN 服务器 ② 转发给Peer B ⚠ 所有流量经过服务器中转

看到区别了吧?STUN 只是帮你“照镜子”找到自己的公网 IP,然后两个 Peer 直接连。TURN 则是把所有数据都通过服务器中转——这是最后的保底方案。

我的建议:生产环境中至少配 2 个 STUN 服务器(比如 Google 的和你自己搭的),再加 1-2 个 TURN 服务器。我曾经只配了一个 STUN,结果那台服务器挂了,所有用户都连不上——血的教训。

3. ICE 候选者收集

ICE 是什么?全称 Interactive Connectivity Establishment。名字挺唬人,其实就是“尝试所有可能的连接方式”。

浏览器会收集三种候选者:

候选者类型 来源 优先级 说明
host 本机网卡 最高 局域网 IP,比如 192.168.1.x
srflx STUN 服务器 中等 公网 IP,NAT 映射后的地址
relay TURN 服务器 最低 通过 TURN 中继的地址

收集过程是异步的。浏览器会触发 onicecandidate 事件:

pc.onicecandidate = (event) => {
  if (event.candidate) {
    // 把这个 candidate 通过信令服务器发给对方
    signalingServer.send({
      type: 'candidate',
      candidate: event.candidate
    });
  } else {
    // candidate 为 null 表示收集完毕
    console.log('所有 ICE 候选者已收集完成');
  }
};

嗯,这里要注意:candidate 为 null 不代表连接已经建立,只是说本地收集完了。真正的连接建立,要等双方交换完候选者之后。

4. ICE 候选者交换

收集到候选者之后,怎么让对方知道?答案是:通过你的信令服务器。

流程大概是这样的:

  1. Peer A 收集到候选者,通过信令发给 Peer B
  2. Peer B 收到后,调用 pc.addIceCandidate() 添加进去
  3. 反过来也一样,Peer B 的候选者发给 Peer A

代码实现:

// 收到远程候选者时
function handleRemoteCandidate(candidateData) {
  const candidate = new RTCIceCandidate(candidateData);
  pc.addIceCandidate(candidate)
    .then(() => console.log('添加远程候选者成功'))
    .catch(err => console.error('添加失败:', err));
}

注意:一定要在 setRemoteDescription() 完成之后再添加候选者。顺序错了,浏览器会报错。我曾经在这个坑里爬了半天——先加了候选者,后设的远程描述,结果 ICE 状态一直卡在 "checking"。

5. ICE 连接状态监控

我习惯在项目中监控 ICE 连接状态,方便排查问题:

pc.oniceconnectionstatechange = () => {
  const state = pc.iceConnectionState;
  console.log('ICE 状态:', state);
  
  switch(state) {
    case 'checking':
      // 正在尝试连接,正常
      break;
    case 'connected':
      // 已建立连接,但可能还有更好的候选者
      break;
    case 'completed':
      // 连接已建立,且已选出最佳候选者
      break;
    case 'failed':
      // 连接失败,需要重试或降级
      console.error('ICE 连接失败,尝试重启 ICE');
      pc.restartIce();
      break;
    case 'disconnected':
      // 短暂断开,可能还能恢复
      break;
    case 'closed':
      // 连接已关闭
      break;
  }
};

这里有个小技巧:failed 状态不一定是死局。我遇到过网络抖动导致 ICE 失败,调用 restartIce() 之后又重新连上了。所以别一看到 failed 就放弃,给个重试机会。

6. 完整示例:创建并配置 PeerConnection

把上面这些串起来,一个完整的初始化流程是这样的:

const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' },
    {
      urls: 'turn:your-turn.example.com:3478',
      username: 'demo',
      credential: 'demo123'
    }
  ],
  iceCandidatePoolSize: 10  // 预收集候选者数量
};

const pc = new RTCPeerConnection(config);

// 监控 ICE 状态
pc.oniceconnectionstatechange = () => {
  console.log('ICE 状态:', pc.iceConnectionState);
};

// 收集候选者
pc.onicecandidate = (event) => {
  if (event.candidate) {
    sendToSignalingServer(event.candidate);
  }
};

// 收到远程候选者
function onRemoteCandidate(candidate) {
  pc.addIceCandidate(new RTCIceCandidate(candidate));
}

个人经验:iceCandidatePoolSize 这个参数很多人忽略。设成 10 左右,可以让浏览器提前收集候选者,减少连接建立时间。我在一个实时协作项目中用了这个参数,连接速度提升了大约 30%。

好了,这一章的内容就是这些。PeerConnection 的创建和配置是 WebRTC 的基石,后面的数据通道、音视频通话都建立在它之上。把这一章吃透,后面的路就好走了。


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