5、RTCPeerConnection入门:RTCPeerConnection对象、ICE框架简介、STUN/TURN服务器、连接状态管理

好,咱们进入正题。这一章是WebRTC的核心中的核心——RTCPeerConnection。说白了,它就是整个实时通信的“管道工”。没有它,你的音视频数据就只能待在本地,哪儿也去不了。

我记得刚开始接触WebRTC时,被一堆概念搞得晕头转向:ICE、STUN、TURN、候选者……感觉像在看天书。但后来我发现,只要抓住一条主线——“如何让两个浏览器互相找到对方并建立连接”——一切就清晰了。

核心思想:RTCPeerConnection 负责端到端的连接建立、媒体传输和连接维护。它封装了复杂的网络穿透逻辑,让你只需关注业务逻辑。

5.1 RTCPeerConnection 对象:一切从这里开始

创建 RTCPeerConnection 对象,是咱们所有工作的起点。代码其实很简单:

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

嗯,这里要注意:配置参数中的 iceServers 是关键。我见过不少新手直接传空数组,结果在复杂网络环境下死活连不上。你想想看,没有STUN/TURN服务器,就像让两个人蒙着眼睛在操场上互相找对方——纯靠运气。

RTCPeerConnection 对象本身是个“大管家”,它管理着:

  • 本地媒体流:通过 addTrack()addTransceiver() 添加
  • 远程媒体流:通过 ontrack 事件接收
  • 连接状态:通过 connectionState 属性监控
  • ICE 状态:通过 iceConnectionState 属性监控

我个人习惯在创建对象后,立刻注册所有事件监听器,免得后面手忙脚乱:

pc.onicecandidate = (event) => {
  if (event.candidate) {
    // 发送候选者给远端
    signalingChannel.send({
      type: 'candidate',
      candidate: event.candidate
    });
  }
};

pc.ontrack = (event) => {
  // 收到远端媒体流,显示到 video 元素
  remoteVideo.srcObject = event.streams[0];
};

pc.onconnectionstatechange = () => {
  console.log('连接状态变化:', pc.connectionState);
};

5.2 ICE 框架简介:网络穿透的“侦察兵”

ICE(Interactive Connectivity Establishment)——交互式连接建立。名字挺唬人,其实干的事很简单:帮两个端点找到一条能通的路

为什么会需要ICE?因为现实网络太复杂了:NAT、防火墙、代理……两个浏览器可能躲在不同的局域网里,直接连根本连不上。ICE 就是那个“侦察兵”,它会尝试各种路径:

  1. 本地候选者:本机IP地址,局域网内通信最快
  2. 反射候选者:通过STUN服务器获取的公网IP和端口
  3. 中继候选者:通过TURN服务器中转,最慢但最可靠

我在项目中遇到过一种情况:两个用户在同一个局域网内,但ICE却走了中继路径,导致延迟很高。后来发现是STUN服务器配置错了,反射候选者没生成。所以啊,调试时一定要看候选者列表,看看是不是走了最优路径。

ICE 候选者收集与连接建立流程 Peer A Peer B 信令通道 STUN 服务器 TURN 服务器 Peer A 候选者列表 • 本地: 192.168.1.5:49152 • 反射: 203.0.113.5:49152 (STUN) • 中继: 198.51.100.1:3478 (TURN) Peer B 候选者列表 • 本地: 10.0.0.8:49153 • 反射: 198.51.100.8:49153 (STUN) • 中继: 198.51.100.1:3478 (TURN) 候选者交换 连通性检查 → 选择最优路径

5.3 STUN/TURN 服务器:穿透与中继

STUN 和 TURN 是 ICE 框架的左膀右臂。简单来说:

服务器类型 作用 适用场景 成本
STUN 获取公网IP和端口(反射候选者) 大多数家庭/办公网络 免费,轻量
TURN 中继所有媒体数据 对称NAT、防火墙严格限制 需要带宽,通常收费

STUN 服务器:它只做一件事——告诉你“你从外面看是什么样子”。就像你站在镜子前,STUN 就是那面镜子。它不转发任何媒体数据,所以非常轻量。

TURN 服务器:这是最后的保底方案。当所有直连尝试都失败时,TURN 会作为中继,转发双方的音视频数据。代价是延迟增加、带宽成本上升。

我的建议:生产环境中一定要部署自己的 TURN 服务器(比如 coturn),不要完全依赖公共 STUN 服务器。我曾经因为公共 STUN 不稳定,导致用户频繁掉线,后来换成自建 TURN 才解决问题。

避坑指南:我曾经遇到过 TURN 服务器配置了但没生效的情况。排查后发现是 credential 字段写错了。记住:usernamecredential 必须与 TURN 服务器配置一致,否则认证失败。

5.4 连接状态管理:别让用户“断线”了还不知道

连接建立后,状态管理就是重中之重。RTCPeerConnection 提供了两个关键状态属性:

  • connectionState:整体连接状态(推荐使用)
  • iceConnectionState:ICE 连接状态(更底层)

我个人更推荐监听 connectionState,因为它更直观:

pc.onconnectionstatechange = () => {
  switch(pc.connectionState) {
    case 'new':
      console.log('连接初始化');
      break;
    case 'connecting':
      console.log('正在连接...');
      showStatus('正在建立连接...');
      break;
    case 'connected':
      console.log('连接成功!');
      showStatus('已连接');
      break;
    case 'disconnected':
      console.log('连接断开,尝试重连...');
      // 这里可以触发重连逻辑
      break;
    case 'failed':
      console.error('连接失败');
      showStatus('连接失败,请检查网络');
      // 提示用户刷新或重试
      break;
    case 'closed':
      console.log('连接已关闭');
      break;
  }
};

嗯,这里有个细节要注意:disconnected 和 failed 的区别。disconnected 表示连接暂时中断,可能还能恢复;failed 则表示彻底没戏了。我在项目中通常这样处理:

  • disconnected:启动一个 5 秒的定时器,如果 5 秒后还没恢复,就尝试重新协商
  • failed:直接提示用户,并清理连接资源

最佳实践:不要只依赖状态变化事件。建议配合心跳机制(比如每隔 3 秒发一个 ping 消息),双重保障连接的可靠性。我在一个视频会议项目中就吃过亏——状态显示 connected,但实际已经收不到数据了。

最后,分享一个我常用的调试技巧:在控制台打印完整的候选者信息,看看 ICE 到底走了哪条路:

pc.onicecandidate = (event) => {
  if (event.candidate) {
    console.log('ICE 候选者:', {
      type: event.candidate.type,      // host / srflx / relay
      protocol: event.candidate.protocol,
      address: event.candidate.address,
      port: event.candidate.port,
      priority: event.candidate.priority
    });
  }
};

看到 type: 'relay' 就说明走了 TURN 中继,延迟会高一些;看到 type: 'host' 就是局域网直连,速度最快。这个信息对排查性能问题特别有用。


好了,这一章的内容就到这里。RTCPeerConnection 是 WebRTC 的基石,理解了它,后面的路就好走了。记住:创建对象 → 配置 ICE → 监控状态,这三步走稳了,连接就不会出大问题。