一、P2P连接建立:从零到一的完整旅程
说实话,WebRTC最让我着迷的部分,就是它那个看似魔法般的P2P连接建立过程。你想想看,两个设备可能隔着半个地球,中间有NAT、有防火墙、有各种乱七八糟的网络设备,但WebRTC硬是能找到一条路让它们直接通话。
我个人习惯把整个流程拆成四个阶段来理解:
- 准备阶段 — 创建PeerConnection,配置ICE
- 收集阶段 — 获取本地媒体和网络候选
- 交换阶段 — 通过信令交换SDP和候选
- 连接阶段 — ICE协商,建立P2P通道
下面这张图,是我自己画的一个整体流程图,建议你先看一遍,有个全局印象。
二、创建PeerConnection:一切的起点
嗯,这里要注意。创建PeerConnection不是简单new一个对象就完事了。配置项里最关键的是ICE服务器地址。我见过太多新手直接传个空配置,结果局域网能通,一到公网就抓瞎。
核心要点:PeerConnection是WebRTC的"大脑",它管理着整个连接的生命周期。
来看一段我常用的初始化代码:
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'your-username',
credential: 'your-credential'
}
],
iceCandidatePoolSize: 10
};
const pc = new RTCPeerConnection(config);
这里有个坑,我踩过。STUN服务器是公开的,但TURN服务器需要自己搭或者买服务。千万别在生产环境只用STUN,因为大概有15%左右的网络环境,STUN搞不定,必须靠TURN中继。
三、媒体流的获取与绑定
创建好PeerConnection之后,下一步就是获取媒体流。说白了,就是打开摄像头和麦克风。
// 获取本地媒体流
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 将媒体流添加到PeerConnection
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
我个人习惯在addTrack之后,立刻监听ontrack事件。为什么?因为远端媒体流到达时,你需要第一时间把它渲染到页面上。
pc.ontrack = (event) => {
// 远端媒体流来了
remoteVideo.srcObject = event.streams[0];
};
小技巧:如果你只需要传输数据而不需要音视频,可以用DataChannel。它不需要getUserMedia,直接通过PeerConnection创建即可。
四、SDP交换:Offer/Answer模型
这是整个流程中最容易出问题的环节。SDP交换说白了就是双方互相告诉对方:"我能支持什么编码、我的IP地址是什么、我有哪些能力"。
流程是这样的:
- 发起方调用createOffer()生成SDP
- 发起方调用setLocalDescription()设置本地SDP
- 发起方通过信令通道把SDP发给接收方
- 接收方调用setRemoteDescription()设置远端SDP
- 接收方调用createAnswer()生成应答SDP
- 接收方调用setLocalDescription()设置本地SDP
- 接收方把应答SDP发回给发起方
- 发起方调用setRemoteDescription()设置远端SDP
你看,一共8步,少一步都不行。我曾经在调试一个项目时,发现连接总是建立不起来,查了半天,原来是setRemoteDescription的时机不对,在createOffer之前就调用了。
// 发起方
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交换必须通过你自己的信令服务器完成。WebRTC不提供信令传输机制,你需要用WebSocket、HTTP或者其他方式来实现。
五、ICE候选收集与连通性检查
SDP交换完成后,真正的"穿透"工作才开始。ICE(Interactive Connectivity Establishment)会收集所有可能的网络路径,然后逐一测试哪些能通。
ICE候选有三种类型:
| 类型 | 优先级 | 说明 |
|---|---|---|
| host | 最高 | 本地IP地址,局域网内直连 |
| srflx | 中等 | 通过STUN获取的公网IP,NAT穿透 |
| relay | 最低 | 通过TURN服务器中继,兜底方案 |
ICE的收集过程是异步的,通过onicecandidate事件来获取:
pc.onicecandidate = (event) => {
if (event.candidate) {
// 通过信令发送给远端
signalingChannel.send({
type: 'candidate',
candidate: event.candidate
});
}
};
这里有个细节,我刚开始做的时候也忽略过。onicecandidate可能会触发多次,每次只返回一个候选。你需要把所有候选都收集完,然后批量或者逐个发送给远端。远端收到后,调用addIceCandidate()添加。
六、连接状态监控
连接建立不是一锤子买卖。网络环境随时可能变化,你需要监控连接状态,及时处理异常。
pc.oniceconnectionstatechange = () => {
console.log('ICE状态:', pc.iceConnectionState);
switch(pc.iceConnectionState) {
case 'connected':
// P2P连接建立成功
break;
case 'disconnected':
// 连接断开,可以尝试重新连接
break;
case 'failed':
// 连接彻底失败,需要重建PeerConnection
break;
}
};
我记得有一次线上事故,用户反馈通话突然中断。查日志发现ICE状态变成了failed,但代码里没有做任何恢复处理。从那以后,我每次都会加上failed状态的重连逻辑。
七、完整流程总结
好了,我们把整个流程串起来看一遍:
- 创建PeerConnection — 配置STUN/TURN服务器
- 获取媒体流 — getUserMedia + addTrack
- 创建Offer — 发起方生成SDP并设置本地描述
- 信令交换 — 通过信令服务器传递SDP
- 创建Answer — 接收方生成应答SDP
- ICE候选收集 — 双方收集并交换候选地址
- 连通性检查 — ICE对候选进行配对测试
- P2P连接建立 — 选择最佳路径,开始传输媒体流
整个过程看起来步骤多,但其实WebRTC帮我们做了大部分工作。你只需要关注信令交换和状态监控,剩下的ICE协商、NAT穿透、候选排序,浏览器都帮你搞定了。
一句话总结:WebRTC的P2P连接建立,就是"收集能力→交换信息→测试连通→建立连接"这四个步骤的循环迭代。
下一章,我会深入讲讲ICE候选的收集细节,以及STUN/TURN到底是怎么工作的。到时候我会分享一些我在实际项目中遇到的NAT穿透案例,很有意思。
公众号:蓝海资料掘金营,微信deep3321