9、建立P2P连接:完整连接流程、候选者交换、连接建立调试、常见连接问题

好,咱们今天来聊聊P2P连接建立这件事。说实话,这是整个WebRTC里最让人头疼,也最有意思的部分。我刚开始做的时候,光是让两个浏览器连上就折腾了两天。后来发现,其实只要把流程理清楚,问题就好解决了。

9.1 完整连接流程:从Offer到连接

一个完整的P2P连接,说白了就是三步走:

  1. 发起方创建Offer — 告诉对方“我想跟你连”
  2. 应答方创建Answer — 对方说“好,我准备好了”
  3. 双方交换候选者 — 互相告诉对方“我在哪,怎么找到我”

嗯,这里要注意,Offer和Answer里装的是什么呢?是SDP(Session Description Protocol)。它描述了你的媒体能力——支持什么编码、什么分辨率、网络参数等等。我见过不少新手以为SDP就是连接信息,其实它只是“我能做什么”,不是“我在哪”。

核心流程:

  • Peer A 创建 RTCPeerConnection
  • Peer A 调用 createOffer() 生成 SDP
  • Peer A 调用 setLocalDescription() 设置本地SDP
  • Peer A 通过信令服务器把SDP发给 Peer B
  • Peer B 调用 setRemoteDescription() 设置远端SDP
  • Peer B 调用 createAnswer() 生成应答SDP
  • Peer B 调用 setLocalDescription() 设置本地SDP
  • Peer B 把应答SDP发回给 Peer A
  • Peer A 调用 setRemoteDescription() 设置远端SDP

代码写起来大概是这样:

// Peer A - 发起方
const pc = new RTCPeerConnection(config);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 通过信令发送 offer 给 Peer B

// Peer B - 应答方
const pc = new RTCPeerConnection(config);
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// 通过信令发送 answer 给 Peer A

// Peer A 收到 answer
await pc.setRemoteDescription(answer);

我个人习惯在createOffer之前先把媒体流加上,这样生成的SDP会更完整。有一次我忘了加流,结果SDP里没有媒体信息,对面直接报错,排查了半天。

9.2 候选者交换:ICE的“找路”过程

Offer和Answer交换完了,接下来就是候选者交换。候选者(ICE Candidate)说白了就是“我在网络上的位置”。

ICE(Interactive Connectivity Establishment)会收集三种候选者:

类型 来源 优先级 说明
host 本机网卡 最高 局域网内直接通信
srflx STUN服务器 中等 公网IP,但可能被NAT限制
relay TURN服务器 最低 中继转发,兜底方案

为什么会收集这么多候选者?因为网络环境太复杂了。你在公司内网、家里WiFi、手机4G,每个场景下的候选者都不一样。ICE会尝试所有可能的组合,找到一条能通的路。

候选者交换的流程是这样的:

  1. 每个Peer收集自己的候选者
  2. 通过onicecandidate事件获取候选者
  3. 通过信令服务器发送给对方
  4. 对方调用addIceCandidate()添加
// 收集并发送候选者
pc.onicecandidate = (event) => {
  if (event.candidate) {
    // 通过信令发送 event.candidate 给对方
    signaling.send({
      type: 'candidate',
      candidate: event.candidate
    });
  }
};

// 接收并添加候选者
pc.addIceCandidate(candidate);

我曾经遇到过一个坑:候选者发送得太快,对方还没设置远端SDP就收到了候选者。这时候addIceCandidate会失败。解决办法很简单——把候选者缓存起来,等setRemoteDescription完成后再批量添加。

小技巧:可以用一个数组暂存候选者,在setRemoteDescription的回调里统一添加。或者用trickle ICE的方式,边收集边发,但要做好顺序控制。

9.3 连接建立调试:怎么看懂ICE状态

连接建立过程中,ICE状态会经历几个阶段。我建议你打开浏览器控制台,把日志级别调到最高,这样能看到ICE的详细过程。

ICE状态变化:

  • new — 刚创建,还没开始
  • checking — 正在检查候选者对
  • connected — 找到了可用连接
  • completed — 所有候选者检查完毕
  • failed — 没找到可用连接
  • disconnected — 连接中断
  • closed — 连接关闭

调试时我最常用的方法是监听这几个事件:

pc.oniceconnectionstatechange = () => {
  console.log('ICE状态:', pc.iceConnectionState);
};

pc.onicegatheringstatechange = () => {
  console.log('收集状态:', pc.iceGatheringState);
};

pc.onicecandidateerror = (event) => {
  console.error('候选者错误:', event.errorCode, event.errorText);
};

嗯,这里有个经验:如果ICE状态卡在checking不动,多半是候选者没交换成功。检查一下信令通道是不是正常,候选者有没有被正确序列化。

注意:onicecandidateerror这个事件很多人会忽略。它告诉你STUN或TURN服务器连不上,或者返回了错误。我曾经因为STUN服务器地址写错了,排查了整整一个下午。

9.4 常见连接问题:避坑指南

做WebRTC开发,连接问题是最常见的。我整理了几个高频问题,都是我在项目中踩过的坑。

问题1:连接建立失败,ICE状态为failed

原因通常是:

  • 没有配置STUN/TURN服务器
  • 防火墙阻止了UDP流量
  • 候选者没有正确交换

解决办法:检查ICE服务器配置,确保STUN地址可访问。如果网络环境严格,必须配置TURN服务器作为兜底。

问题2:连接建立成功,但媒体流不通

这个我遇到过好几次。原因往往是SDP里没有媒体信息,或者编解码器不匹配。检查一下createOffer时是否已经添加了媒体流,以及双方的SDP里是否有相同的编解码器。

问题3:候选者交换顺序问题

前面提到过,候选者来得太早会失败。我曾经写过一个demo,本地跑得好好的,部署到服务器就不行。后来发现是信令延迟导致候选者先于SDP到达。解决方案是等setRemoteDescription完成后再处理候选者。

问题4:NAT穿透失败

有些对称NAT环境下,STUN也搞不定。这时候只能靠TURN中继。我建议在生产环境中始终配置TURN服务器,别指望STUN能解决所有问题。

避坑指南:

  • 始终配置至少一个STUN服务器和一个TURN服务器
  • 候选者交换要等SDP设置完成后再进行
  • 监听onicecandidateerror,及时发现问题
  • 生产环境不要用Google的公共STUN服务器,不稳定
  • ICE状态变化要记录日志,方便排查

9.5 知识体系图

下面这张图总结了P2P连接建立的完整流程和关键节点:

P2P连接建立完整流程 Peer A(发起方) Peer B(应答方) 信令服务器 1. 创建RTCPeerConnection 2. createOffer() 3. setLocalDescription() 4. 收集ICE候选者 5. setRemoteDescription() 6. 添加远端候选者 1. 创建RTCPeerConnection 2. setRemoteDescription() 3. createAnswer() 4. setLocalDescription() 5. 收集ICE候选者 6. 添加远端候选者 Offer (SDP) Answer (SDP) 候选者 候选者 Peer A步骤 Peer B步骤 Offer Answer 候选者

这张图把整个流程串起来了。你看,左边是Peer A,右边是Peer B,中间是信令服务器。Offer和Answer走信令通道,候选者也是通过信令交换的。等双方都拿到了对方的SDP和候选者,ICE就开始检查,找到一条可用的路径,连接就建立了。

调试建议:在chrome://webrtc-internals里可以看到所有ICE候选者、状态变化和SDP信息。我每次调试必开这个页面,比console.log好用十倍。

好了,P2P连接建立这部分就讲到这里。记住,流程本身不复杂,复杂的是各种网络环境下的兼容性。多测试、多打日志、多看webrtc-internals,慢慢就有感觉了。

公众号:蓝海资料掘金营,微信deep3321