3. RTCPeerConnection 入门:连接状态机、ICE 框架简介、STUN/TURN 服务器配置
好,咱们进入正题。RTCPeerConnection 是整个 WebRTC 的核心,说白了就是浏览器里那个负责点对点连接的对象。你调它的 API,它帮你搞定 NAT 穿透、加密传输、音视频数据收发。我刚开始接触时也觉得它像个黑盒,但摸清它的状态机和 ICE 流程后,其实没那么玄乎。
3.1 连接状态机:从 New 到 Closed 的完整旅程
RTCPeerConnection 内部维护了一套状态机,用来描述连接的生命周期。你想想看,一个连接从创建到销毁,中间要经历信令交换、候选收集、连通性检测,每一步都有对应的状态。
我习惯把状态机分成两个维度来看:连接状态(connectionState)和 ICE 连接状态(iceConnectionState)。前者是更高层的抽象,后者更贴近 ICE 协议本身。实际开发中,我主要监听 connectionstatechange 事件。
| 状态 | 含义 | 触发时机 |
|---|---|---|
new |
连接刚创建,还没开始 ICE | 调用构造函数后 |
connecting |
正在收集候选、进行连通性检查 | 调用 setLocalDescription 后 |
connected |
至少一条候选路径可用 | ICE 连通性检查通过 |
disconnected |
连接暂时中断,但还在尝试恢复 | ICE 超时或丢包严重 |
failed |
连接彻底失败,无法恢复 | ICE 检查全部超时 |
closed |
连接已关闭 | 调用 close() |
核心要点: disconnected 和 failed 的区别在于——前者还有救,后者没救了。我曾经在项目中遇到网络闪断,状态在 disconnected 和 connected 之间来回跳,这时候千万别直接关连接,给它一点恢复时间。
3.2 ICE 框架简介:它到底在干什么?
ICE(Interactive Connectivity Establishment)是 WebRTC 的穿透引擎。它的任务只有一个:帮两个端点找到一条能通的路。你想想看,两个设备可能都在 NAT 后面,没有公网 IP,怎么找到对方?
ICE 的流程可以概括为三步:
- 收集候选地址:每个端点收集自己可能的 IP 和端口组合,包括本地地址、STUN 反射地址、TURN 中继地址。
- 交换候选:通过信令通道把候选列表发给对端。
- 连通性检查:两端互相发 STUN 绑定请求,测试哪一对候选能通。
我画了一张图,帮你把 ICE 框架的层次理清楚:
我的经验: 候选优先级是 ICE 选路的关键。host 候选优先级最高,relay 最低。但别以为 host 一定最快——有些局域网环境虽然能通,但延迟反而比公网中继高。我在一个跨国项目中就遇到过这种情况,最后强制走 TURN 反而更稳定。
3.3 STUN / TURN 服务器配置
STUN 和 TURN 是 ICE 的左右手。STUN 帮你拿到公网反射地址,TURN 则是在 P2P 不通时当中继。配置其实很简单,就是往 iceServers 数组里塞 URL。
const config = {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'pass'
}
]
};
const pc = new RTCPeerConnection(config);
这里有几个坑,我一个个说:
- STUN 服务器不需要认证,但 TURN 必须要有用户名和密码。我曾经见过有人把 STUN 的配置也加了 credential,结果浏览器直接报错。
- URL 协议要写对:STUN 用
stun:前缀,TURN 用turn:或turns:(TLS 加密)。 - 端口别搞错:STUN 默认 3478,TURN 也是 3478,但有些服务商用 5349 做 TLS 端口。
避坑指南: 我曾经在生产环境只配了一个 STUN 服务器,结果有 30% 的用户连不上——因为他们都在对称 NAT 后面,STUN 拿到的反射地址根本没用。后来加了 TURN 服务器,连接成功率才提到 99% 以上。记住:生产环境必须配 TURN,别省这个钱。
3.4 ICE 候选收集与连通性检查
当你调用 setLocalDescription 后,浏览器会自动开始收集候选。每个候选会通过 onicecandidate 回调告诉你。你需要把这些候选通过信令通道发给对端。
pc.onicecandidate = (event) => {
if (event.candidate) {
// 通过信令通道发送给对端
signalingChannel.send({
type: 'candidate',
candidate: event.candidate
});
}
};
对端收到后,调用 addIceCandidate 添加进来。ICE 层会维护一个候选对列表,然后逐一发送 STUN 绑定请求做连通性检查。
我习惯把候选收集的过程想象成「撒网」——你撒出去的候选越多,捞到鱼的概率越大。但也不是越多越好,候选太多会导致连通性检查时间变长。实际项目中,我一般会设置 iceCandidatePoolSize 来控制并发数。
| 候选类型 | 优先级 | 典型延迟 | 适用场景 |
|---|---|---|---|
| host | 最高 | < 1ms | 局域网内直连 |
| srflx | 中 | 10-50ms | 公网直连(非对称 NAT) |
| relay | 最低 | 50-200ms | 对称 NAT / 防火墙限制 |
关键点: ICE 连通性检查是并发的,不是串行的。浏览器会同时尝试多个候选对,谁先通就用谁。所以即使你配了 TURN,只要 host 或 srflx 能通,就不会走中继——这能省下不少带宽费用。
3.5 状态机实战:监听连接变化
最后,我建议你在项目中一定要监听连接状态的变化,并做相应的处理。下面是我常用的模板:
pc.onconnectionstatechange = () => {
switch (pc.connectionState) {
case 'connected':
console.log('连接已建立');
break;
case 'disconnected':
console.warn('连接中断,尝试恢复...');
// 可以启动重连计时器
break;
case 'failed':
console.error('连接失败,需要重建');
// 关闭旧连接,创建新连接
pc.close();
createNewConnection();
break;
case 'closed':
console.log('连接已关闭');
break;
}
};
嗯,这里要注意:disconnected 状态触发后,浏览器会尝试恢复,默认超时是 30 秒。如果 30 秒内没恢复,就会变成 failed。我在项目中遇到过一种情况——用户切换了 Wi-Fi,状态从 connected 跳到 disconnected,然后又跳回 connected,整个过程不到 5 秒。所以别在 disconnected 时立刻重建连接,给它一点时间。
RTCPeerConnection 的状态机、ICE 框架和 STUN/TURN 配置,这三块是 WebRTC 的基石。你把这些搞清楚了,后面处理媒体流、数据通道、重连策略都会顺手很多。说白了,连接都建不起来,后面全是白搭。