6、RTCPeerConnection:连接建立流程、ICE框架、STUN/TURN服务器配置
聊到 WebRTC 的核心,RTCPeerConnection 绝对是绕不开的主角。说白了,它就是两个浏览器之间建立音视频通话的「管道」。我刚开始接触 WebRTC 时,总觉得这玩意儿很玄乎,不就是个连接嘛。后来踩了不少坑才明白,这条管道的建立过程,远比想象中复杂。
今天我们就来拆解一下,RTCPeerConnection 到底是怎么把两端连起来的。我会结合我自己的项目经验,把 ICE 框架、STUN/TURN 这些概念讲透。
6.1 连接建立流程:从 Offer 到 Answer
先看整体流程。两端要通话,得先「握手」。这个握手过程,我习惯把它分成三步:
- 创建 Offer:发起方(比如 A)调用
createOffer(),生成一个 SDP 描述。 - 设置本地描述:A 调用
setLocalDescription(),把自己的 SDP 存下来。 - 发送给远端:A 通过信令服务器把 SDP 发给 B。
- 创建 Answer:B 收到后,调用
setRemoteDescription(),然后createAnswer()生成应答 SDP。 - 设置本地描述:B 调用
setLocalDescription(),再把 Answer 发回给 A。 - 最终确认: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 不是一步到位的。它会经历多个状态:new → checking → connected → completed。如果中间出错,会进入 failed 或 disconnected。
我建议你监听 iceconnectionstatechange 事件,实时跟踪状态变化:
pc.oniceconnectionstatechange = () => {
console.log('ICE 状态:', pc.iceConnectionState);
if (pc.iceConnectionState === 'failed') {
// 尝试重启 ICE
pc.restartIce();
}
};
嗯,这里有个小技巧:当状态变成 disconnected 时,不要立刻判定失败。网络抖动可能导致短暂断开,等几秒再处理。我一般设 5 秒的延迟。
6.5 知识体系图
下面这张图总结了 RTCPeerConnection 连接建立的核心逻辑。你可以看到从 Offer/Answer 到 ICE 候选者收集,再到最终连接的全过程。
6.6 实战建议
最后,分享几个我在项目中积累的经验:
- 优先使用 STUN:TURN 会消耗服务器带宽和流量,能直连就别中转。我一般只在 ICE 失败后才启用 TURN。
- 动态配置 TURN 凭证:用户名和密码定期更换,避免被滥用。可以用 REST API 从服务端获取。
- 监听 ICE 候选者:通过
onicecandidate事件收集候选者,并通过信令通道交换。注意,候选者可能分批到达。 - 处理 ICE 重启:当网络切换(比如从 WiFi 切到 4G)时,调用
restartIce()重新建立连接。
我的习惯:在开发阶段,我会把 ICE 候选者和状态变化都打印到控制台。这样一旦连接出问题,能快速定位是哪个环节卡住了。生产环境则建议用日志服务收集,方便排查。
RTCPeerConnection 的连接建立,说白了就是一场「找路」游戏。Offer/Answer 是地图,ICE 是导航,STUN/TURN 是路标。只要把这三样配好,大部分网络环境都能搞定。