6、RTCPeerConnection:连接建立流程、ICE框架、STUN/TURN服务器配置

聊到 WebRTC 的核心,RTCPeerConnection 绝对是绕不开的主角。说白了,它就是两个浏览器之间建立音视频通话的「管道」。我刚开始接触 WebRTC 时,总觉得这玩意儿很玄乎,不就是个连接嘛。后来踩了不少坑才明白,这条管道的建立过程,远比想象中复杂。

今天我们就来拆解一下,RTCPeerConnection 到底是怎么把两端连起来的。我会结合我自己的项目经验,把 ICE 框架、STUN/TURN 这些概念讲透。

6.1 连接建立流程:从 Offer 到 Answer

先看整体流程。两端要通话,得先「握手」。这个握手过程,我习惯把它分成三步:

  1. 创建 Offer:发起方(比如 A)调用 createOffer(),生成一个 SDP 描述。
  2. 设置本地描述:A 调用 setLocalDescription(),把自己的 SDP 存下来。
  3. 发送给远端:A 通过信令服务器把 SDP 发给 B。
  4. 创建 Answer:B 收到后,调用 setRemoteDescription(),然后 createAnswer() 生成应答 SDP。
  5. 设置本地描述:B 调用 setLocalDescription(),再把 Answer 发回给 A。
  6. 最终确认:A 收到 Answer 后,调用 setRemoteDescription()

嗯,这里要注意:SDP 里包含了音视频的编解码信息、网络地址、ICE 候选者等等。说白了,就是告诉对方「我支持什么格式,我在哪,怎么连我」。

核心要点:Offer/Answer 模型是 WebRTC 的信令基础。没有这个握手,后面的 ICE 根本跑不起来。

6.2 ICE 框架:穿透 NAT 的利器

流程走完了,但有个现实问题:A 和 B 可能都在内网,或者有防火墙。直接连?连不上。这时候 ICE 就登场了。

ICE(Interactive Connectivity Establishment)说白了就是一套「找路」机制。它会收集所有可能的网络路径,然后一个个试,直到找到一条能通的。

我遇到过最典型的场景:两个用户都在公司内网,中间有严格的防火墙。ICE 会尝试三种路径:

  • Host 候选者:本机 IP 地址,比如 192.168.1.10。内网直连最快,但跨网段基本没戏。
  • Server Reflexive 候选者:通过 STUN 服务器获取的公网 IP 和端口。能穿透大部分 NAT。
  • Relay 候选者:通过 TURN 服务器中转。这是最后的保底方案。

ICE 会把这些候选者排序,优先尝试 Host,不行再试 STUN,最后才用 TURN。我曾经在一个项目中,发现 ICE 一直卡在 Host 候选者上,死活连不上。后来排查发现是 NAT 类型太严格,STUN 也搞不定,最后强制配置了 TURN 才解决。

我的经验:ICE 的候选者收集阶段,建议设置合理的超时时间。默认 30 秒太长,我一般设 10 秒。如果 10 秒内没找到可用路径,直接降级到 TURN。

6.3 STUN/TURN 服务器配置

STUN 和 TURN 是 ICE 的「左右手」。STUN 负责帮你发现公网地址,TURN 负责帮你中转数据。配置起来其实不复杂,但坑不少。

6.3.1 STUN 服务器

STUN 服务器的作用很简单:你发个请求过去,它告诉你「你的公网 IP 是 xxx,端口是 yyy」。然后 ICE 把这个地址作为 Server Reflexive 候选者。

配置示例:

const pcConfig = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }
  ]
};

const pc = new RTCPeerConnection(pcConfig);

Google 的公共 STUN 服务器可以免费使用,但生产环境我建议自建。原因很简单:公共服务器不稳定,而且有频率限制。我之前有个项目,高峰期几千个用户同时用 Google 的 STUN,结果大量请求超时,连接成功率直接掉到 70%。

6.3.2 TURN 服务器

TURN 服务器就复杂一些。它不仅要帮你发现地址,还要帮你转发音视频数据。说白了,就是当 P2P 走不通时,TURN 充当中间人。

配置示例:

const pcConfig = {
  iceServers: [
    { urls: 'stun:stun.example.com:3478' },
    {
      urls: 'turn:turn.example.com:3478',
      username: 'user',
      credential: 'pass'
    }
  ]
};

注意,TURN 需要认证。用户名和密码一般由服务端动态生成,定期更换。我见过有人把密码写死在客户端,结果被滥用,流量费直接爆表。

避坑指南:我曾经在生产环境遇到过一个问题——TURN 服务器配置了多个地址,但 ICE 只用了第一个。排查发现是 urls 字段写成了字符串,而不是数组。正确的写法是 urls: ['turn:server1', 'turn:server2']

6.4 ICE 状态机与事件监听

ICE 不是一步到位的。它会经历多个状态:newcheckingconnectedcompleted。如果中间出错,会进入 faileddisconnected

我建议你监听 iceconnectionstatechange 事件,实时跟踪状态变化:

pc.oniceconnectionstatechange = () => {
  console.log('ICE 状态:', pc.iceConnectionState);
  if (pc.iceConnectionState === 'failed') {
    // 尝试重启 ICE
    pc.restartIce();
  }
};

嗯,这里有个小技巧:当状态变成 disconnected 时,不要立刻判定失败。网络抖动可能导致短暂断开,等几秒再处理。我一般设 5 秒的延迟。

6.5 知识体系图

下面这张图总结了 RTCPeerConnection 连接建立的核心逻辑。你可以看到从 Offer/Answer 到 ICE 候选者收集,再到最终连接的全过程。

RTCPeerConnection 连接建立流程 1. Offer/Answer 握手 createOffer → setLocalDescription 2. ICE 候选者收集 Host → STUN → TURN 3. 连接建立 connected → completed 关键组件说明 • SDP:包含音视频编解码、网络地址、ICE 候选者等信息 • ICE:负责找到两端之间的最优网络路径 • STUN:帮助发现公网 IP 和端口(穿透 NAT) • TURN:当 P2P 不通时,作为中继转发数据 • ICE 状态机:new → checking → connected → completed → failed 注:实际项目中,STUN/TURN 服务器需要根据网络环境动态配置

6.6 实战建议

最后,分享几个我在项目中积累的经验:

  • 优先使用 STUN:TURN 会消耗服务器带宽和流量,能直连就别中转。我一般只在 ICE 失败后才启用 TURN。
  • 动态配置 TURN 凭证:用户名和密码定期更换,避免被滥用。可以用 REST API 从服务端获取。
  • 监听 ICE 候选者:通过 onicecandidate 事件收集候选者,并通过信令通道交换。注意,候选者可能分批到达。
  • 处理 ICE 重启:当网络切换(比如从 WiFi 切到 4G)时,调用 restartIce() 重新建立连接。

我的习惯:在开发阶段,我会把 ICE 候选者和状态变化都打印到控制台。这样一旦连接出问题,能快速定位是哪个环节卡住了。生产环境则建议用日志服务收集,方便排查。

RTCPeerConnection 的连接建立,说白了就是一场「找路」游戏。Offer/Answer 是地图,ICE 是导航,STUN/TURN 是路标。只要把这三样配好,大部分网络环境都能搞定。

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