5、P2P连接建立全流程:Offer/Answer模型、SDP协议深度解析、Candidate收集与排序
各位同学,今天我们来啃一块硬骨头——P2P连接建立的全流程。说实话,这是整个WebRTC体系里最容易让人懵圈的部分。我记得刚接触时,看着Offer、Answer、SDP、ICE Candidate这些术语在信令通道里飞来飞去,脑子里全是问号。
但别怕。咱们一步步拆解。说白了,整个流程就三件事:谈条件、找路径、建连接。今天我们就围绕这三件事展开。
5.1 Offer/Answer模型:谈判的艺术
Offer/Answer模型,说白了就是两个浏览器在谈条件。你支持什么编码?我支持什么编码?你用什么端口?我用什么端口?谈妥了才能干活。
我习惯把这个过程比作两个人在打电话前先确认对方用什么手机、信号好不好。嗯,虽然比喻不太严谨,但意思到了。
5.1.1 谁先发起?
在WebRTC里,发起方创建Offer,应答方返回Answer。但这里有个坑:谁当发起方?
- 呼叫场景: 主动拨号的一方是发起方
- 房间场景: 先加入房间的是发起方
- 特殊情况: 双方同时发起?嗯,这种情况我遇到过,需要业务层做冲突处理
5.1.2 流程详解
来看一个典型的Offer/Answer交换流程:
// 发起方(Caller)
const pc = new RTCPeerConnection(config);
const offer = await pc.createOffer(); // 创建Offer
await pc.setLocalDescription(offer); // 设置本地SDP
// 通过信令通道发送offer给对端
// 应答方(Callee)
const pc = new RTCPeerConnection(config);
await pc.setRemoteDescription(offer); // 设置远端SDP
const answer = await pc.createAnswer(); // 创建Answer
await pc.setLocalDescription(answer); // 设置本地SDP
// 通过信令通道发送answer给对端
// 发起方收到Answer
await pc.setRemoteDescription(answer); // 设置远端SDP
这里有个细节:setLocalDescription和setRemoteDescription的顺序不能乱。我曾经在调试一个bug时,发现连接总是建立失败,查了半天才发现是setRemoteDescription在setLocalDescription之前调用了。嗯,这种低级错误,犯过一次就记住了。
5.2 SDP协议深度解析:一张纸上的所有秘密
SDP(Session Description Protocol)虽然叫协议,但它其实就是一个文本格式的描述文件。你想想看,它要把双方的媒体能力、网络信息、安全参数全部写在一张纸上,然后互相交换。
来看一个真实的SDP片段:
v=0
o=- 1234567890 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9
c=IN IP4 0.0.0.0
a=rtpmap:111 opus/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=fmtp:111 minptime=10;useinbandfec=1
a=mid:0
a=sendrecv
别被这些字段吓到。我拆开来讲:
| 字段 | 含义 | 我的理解 |
|---|---|---|
v=0 |
SDP版本号 | 基本固定,不用管 |
o= |
会话发起者信息 | 包含会话ID和版本号 |
m=audio |
媒体描述行 | 告诉对方我支持哪些编码 |
a=rtpmap |
编码映射 | 把编码ID和名称对应起来 |
a=sendrecv |
媒体方向 | 我能发也能收 |
5.2.1 BUNDLE机制
你注意到上面SDP里的a=group:BUNDLE 0 1了吗?这是WebRTC的一个优化机制。说白了,就是把音频、视频、数据通道的多个流合并到同一个传输通道里。
为什么要这么做?因为每个媒体流都需要一个UDP端口,如果同时开音频、视频、屏幕共享,端口消耗很大。BUNDLE让它们共用一条连接,省端口、省资源。
5.3 Candidate收集与排序:找路的过程
SDP交换完了,双方知道了对方支持什么编码。但问题来了:怎么把数据传过去?
这就是ICE Candidate要做的事。Candidate,翻译过来就是“候选地址”。每个端都会收集自己可能的通信地址,然后发给对方。
5.3.1 三种Candidate类型
我习惯把Candidate分成三类,按优先级从高到低排列:
- host Candidate(主机候选):本机网卡IP,比如192.168.1.100。优先级最高,因为不走任何中转。
- srflx Candidate(反射候选):经过NAT映射后的公网IP。比如你的内网是192.168.1.100,经过路由器NAT后变成120.24.xxx.xxx。
- relay Candidate(中继候选):通过TURN服务器转发的地址。优先级最低,因为延迟大、带宽贵。
5.3.2 收集过程
Candidate收集是自动的,但我们可以监听事件来观察:
pc.onicecandidate = (event) => {
if (event.candidate) {
console.log('发现Candidate:', event.candidate.candidate);
// 通过信令通道发送给对端
signalingChannel.send({
type: 'candidate',
candidate: event.candidate
});
} else {
console.log('所有Candidate收集完毕');
}
};
这里有个细节:event.candidate为null时表示收集完成。我见过有人在这个回调里一直发空Candidate,导致对端解析出错。嗯,记得加个判空。
5.3.3 连接性检查与排序
双方交换完Candidate列表后,ICE就开始做连接性检查。说白了,就是尝试用各种组合去ping对方。
举个例子:
- 你的host Candidate(192.168.1.100)去连对方的host Candidate(192.168.1.200)
- 你的host Candidate去连对方的srflx Candidate
- 你的srflx Candidate去连对方的host Candidate
- ...以此类推
每对组合称为一个Candidate Pair。ICE会按优先级排序,优先检查高优先级的Pair。一旦某个Pair连通了,就建立连接,其他Pair不再检查。
iceTransportPolicy为relay强制走中继,或者调整防火墙规则。
5.4 全流程总结
来,我们把整个流程串起来。我画了一张图,帮你理清思路:
整个流程走下来,你会发现:SDP交换是谈条件,Candidate交换是找路。两者缺一不可。
好了,这一章的内容就到这里。记住,P2P连接建立是WebRTC最核心的环节,也是问题最多的地方。多动手调试,多抓包分析,慢慢就会形成自己的经验体系。