4、RTCPeerConnection详解:核心对象、ICE框架与Candidate、Offer/Answer模型

各位同学,今天我们来啃一块硬骨头——RTCPeerConnection。说实话,我刚接触WebRTC的时候,最头疼的就是这个对象。它太庞大了,像个瑞士军刀,什么功能都往里塞。但你别怕,我们把它拆开来看,其实就三块:连接管理网络穿透媒体协商

我个人习惯把RTCPeerConnection比作一个「电话交换机」。你拨号、等待对方接听、建立通话,每一步都有对应的API。咱们今天就把这三块讲透。

4.1 RTCPeerConnection对象:你的连接管家

先看一个最简单的创建方式:

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }
  ]
});

嗯,这里要注意。这个构造函数的参数叫RTCConfiguration,最常用的就是iceServers。我在项目中遇到过,有人只配了一个STUN服务器,结果在复杂网络环境下死活连不上。后来加了TURN服务器才搞定。

RTCPeerConnection身上挂着一堆事件,我列几个你一定会用到的:

事件名 触发时机 我常用的场景
icecandidate ICE代理发现新候选地址时 收集Candidate并通过信令发送给对方
track 远端添加了媒体轨道时 把收到的视频流挂到video标签上
connectionstatechange 连接状态变化时 显示"已连接/已断开"状态提示
datachannel 收到数据通道请求时 建立非媒体数据传输

你想想看,这些事件其实就是整个连接的生命周期。从创建到销毁,每一步都有回调让你干预。

4.2 ICE框架与Candidate:网络穿透的幕后英雄

为什么需要ICE?说白了,你的电脑在局域网里,对方的电脑可能在另一个局域网里。中间隔着NAT、防火墙,怎么找到对方?ICE就是干这个的。

ICE的全称是Interactive Connectivity Establishment,交互式连接建立。它背后有三个协议:

  • STUN:帮你发现自己的公网IP和端口。我习惯叫它「照镜子」。
  • TURN:如果P2P打不通,就通过服务器中转。这是保底方案。
  • Candidate:每一个可能的连接地址,就是一个Candidate。

一个Candidate长这样:

candidate:842163049 1 udp 16777215 192.168.1.100 54321 typ host

我来拆解一下:

  • 842163049:Foundation,用来去重
  • 1:组件ID,1表示RTP,2表示RTCP
  • udp:传输协议
  • 16777215:优先级,越高越优先
  • 192.168.1.100:IP地址
  • 54321:端口
  • typ host:类型,host是本地地址,srflx是反射地址,relay是中继地址

我曾经在一个项目中,发现Candidate一直收集不全。排查了半天,原来是STUN服务器地址写错了。嗯,这种低级错误我也犯过。

核心要点:ICE框架会收集三种Candidate:host(本地)、srflx(公网映射)、relay(中继)。优先级从高到低排列,优先尝试P2P直连。

ICE的状态机也很重要。它从new开始,经过checkingconnected,最终到completedfailed。我建议你在开发时,把iceConnectionState的变化打印出来,调试起来会轻松很多。

4.3 Offer/Answer模型与SDP交换

这是WebRTC最核心的机制。说白了,就是双方坐下来商量:「我用什么编码器?你支持什么分辨率?咱们怎么传数据?」

流程很简单:

  1. 发起方创建Offer(SDP)
  2. 发起方设置本地描述
  3. 发起方通过信令把Offer发给对方
  4. 接收方设置远端描述
  5. 接收方创建Answer(SDP)
  6. 接收方设置本地描述
  7. 接收方把Answer发回给发起方
  8. 发起方设置远端描述

代码实现大概是这样的:

// 发起方
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 通过信令发送 offer 给对方

// 接收方
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// 通过信令发送 answer 给对方

// 发起方
await pc.setRemoteDescription(answer);

你可能会问:SDP里面到底写了什么?我截取一段给你看:

v=0
o=- 123456 2 IN IP4 0.0.0.0
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104
a=rtpmap:111 opus/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000

这里面包含了媒体类型(audio/video)、编码器(opus/ISAC)、传输协议等信息。你不需要手写SDP,浏览器会帮你生成。但你要能看懂,因为调试时经常要检查SDP内容。

小技巧:在Chrome开发者工具里,输入pc.localDescription.sdp就能看到当前生成的SDP。我调试时经常这么干。

4.4 完整流程的SVG图解

下面这张图,把整个流程串起来了。我建议你多看几遍,把每个步骤对应到代码上。

RTCPeerConnection 完整流程 发起方 (Offerer) 接收方 (Answerer) 1. createOffer() Offer (SDP) 2. setRemoteDescription() 3. createAnswer() Answer (SDP) 4. setRemoteDescription() ICE Candidate 交换(持续进行) host Candidate → srflx Candidate → relay Candidate(按优先级尝试) ✅ P2P 连接建立

关键点:Offer/Answer交换的是SDP,ICE Candidate交换的是网络地址。两者是并行的,但SDP交换必须先完成,ICE才能开始真正的连接尝试。

4.5 避坑指南

我做了这么多年WebRTC开发,踩过的坑能写一本书。这里挑几个最常见的:

坑1:setLocalDescription和setRemoteDescription的顺序不能乱。必须先设置本地描述,再设置远端描述。我曾经在代码里把顺序写反了,结果ICE一直卡在checking状态,排查了整整一天。

坑2:ICE Candidate的收集是异步的。你调用createOffer之后,Candidate不会立刻全部出现。要监听icecandidate事件,等它触发时再发送。我见过有人直接在createOffer后面取candidate,结果取到的是空的。

建议:开发阶段把pc.onicecandidatepc.oniceconnectionstatechange都打上日志。这样你能看到Candidate的收集进度和ICE状态变化,调试起来一目了然。

好了,RTCPeerConnection的核心内容就这些。你把它理解成「连接管家+网络探路者+协商主持人」三个角色,就很好记了。代码写起来其实就那么几行,关键是理解背后的原理。


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