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 只是帮你“照镜子”找到自己的公网 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 候选者交换
收集到候选者之后,怎么让对方知道?答案是:通过你的信令服务器。
流程大概是这样的:
- Peer A 收集到候选者,通过信令发给 Peer B
- Peer B 收到后,调用
pc.addIceCandidate()添加进去 - 反过来也一样,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