18、RTCPeerConnection入门:什么是RTCPeerConnection、信令(Signaling)概念、创建连接

好,咱们今天聊点实在的。

前面十几章,我们把音视频采集、本地预览、格式控制这些基本功都过了一遍。现在,是时候让两个浏览器真正“通上话”了。

这一章,我们聚焦一个核心对象——RTCPeerConnection。说白了,它就是WebRTC里负责点对点传输的“管道工”。没有它,你采集到的视频数据就只能在自己电脑上自嗨,传不到对面去。

18.1 什么是RTCPeerConnection

RTCPeerConnection,名字挺长,但拆开看就明白了:

  • RTC:实时通信(Real-Time Communication)
  • Peer:对等端,也就是参与通信的每个终端
  • Connection:连接,建立一条数据通路

合起来,就是“对等端之间的实时连接”。

我个人的理解是:它像一个黑盒子,你往里塞音视频流,它负责把数据编码、打包、穿越复杂的网络环境,最终送到对方手里。你不需要关心底层用了什么协议、怎么穿越NAT、怎么处理丢包——这些脏活累活,RTCPeerConnection都帮你干了。

核心要点:RTCPeerConnection不是用来“连接服务器”的,它是用来连接两个浏览器(或两个终端)的。中间没有服务器转发数据,只有信令服务器帮忙“牵线搭桥”。

我在项目中遇到过不少新手,以为RTCPeerConnection是连到某个云服务的API。其实不是。它建立的是端到端的直连通道,数据不经过中间服务器中转。这样做的好处是延迟低、带宽利用率高,但代价是——你得自己搞定“怎么找到对方”这个问题。

18.2 信令(Signaling)概念

好,问题来了:两个浏览器在不同的电脑上,怎么知道对方的存在?

这就引出了信令的概念。

信令,说白了就是“握手前的自我介绍”。在WebRTC的世界里,它负责交换三类信息:

  1. 会话描述(SDP):告诉对方“我支持哪些编解码器、我的IP地址是什么、我打算用什么格式传数据”。
  2. 网络信息(ICE候选):告诉对方“我有哪些可能的网络路径可以到达我”。
  3. 其他元数据:比如房间号、用户ID等业务层面的信息。

注意,WebRTC标准并没有规定信令该怎么传。你可以用WebSocket、HTTP轮询、甚至用鸽子送信——只要能把信息从A传到B就行。

我的建议:实际项目中,信令服务器通常用WebSocket实现。因为它双向、实时、开销小。我曾经用HTTP长轮询做过一版,结果延迟高得离谱,后来全改成WebSocket了。

信令的过程大致是这样的:

  • A创建Offer(一个SDP描述),发给信令服务器
  • 信令服务器转发给B
  • B收到后,创建Answer,发回给信令服务器
  • 信令服务器再转发给A
  • 双方交换ICE候选,找到最优的网络路径
  • 连接建立,开始传输数据

嗯,这里要注意:信令服务器只负责转发,不负责传输音视频数据。它就像个“媒人”,牵完线就退到一边了。

18.3 创建连接:从零到一

理论说完了,咱们动手写代码。

创建一个RTCPeerConnection,其实就三步:

  1. 创建RTCPeerConnection实例
  2. 添加音视频轨道
  3. 发起或响应连接

先看最基础的创建代码:

// 第一步:创建配置对象
const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }
  ]
};

// 第二步:创建连接实例
const pc = new RTCPeerConnection(config);

console.log('RTCPeerConnection 创建成功');

这段代码看着简单,但有几个坑我得提醒你:

  • iceServers 里至少要配一个STUN服务器,否则内网环境可能连不上。我刚开始做的时候忘了配,结果在办公室局域网里能通,回家就死活连不上——折腾了一下午才发现是STUN没配。
  • STUN服务器是公开的,Google那个 stun.l.google.com:19302 可以用,但生产环境建议自己搭或者用商业服务。

接下来,添加本地音视频流:

// 假设我们已经通过 getUserMedia 获取了本地流
// localStream 是一个 MediaStream 对象

localStream.getTracks().forEach(track => {
  pc.addTrack(track, localStream);
});

console.log('已添加', localStream.getTracks().length, '条轨道');

这里有个细节:addTrack 的第二个参数要传原始的MediaStream对象,而不是轨道本身。我见过有人写成 pc.addTrack(track, track),结果数据传不过去,因为RTCPeerConnection需要知道轨道属于哪个流。

最后,发起连接(以发起方为例):

// 创建Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

// 通过信令服务器发送给远端
// 假设 sendToSignalingServer 是你封装好的函数
sendToSignalingServer({
  type: 'offer',
  sdp: offer.sdp
});

console.log('Offer 已发送,等待 Answer...');

接收方收到Offer后,需要做:

// 接收方代码
await pc.setRemoteDescription(new RTCSessionDescription(offer));

const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);

sendToSignalingServer({
  type: 'answer',
  sdp: answer.sdp
});

你看,整个过程就是“你发我收、我发你收”的对称过程。一旦双方都设置好了本地和远端的描述,RTCPeerConnection就会自动开始尝试建立连接。

注意:createOffer/createAnswer 是异步操作,必须 await。而且 setLocalDescription 必须在发送之前完成。我曾经因为顺序搞反,导致SDP里缺少ICE信息,对方一直收不到候选地址,连接超时。

18.4 核心流程图

下面这张图,把整个信令和连接建立的过程串起来了。你多看几遍,心里就有谱了。

RTCPeerConnection 信令与连接建立流程 浏览器 A(发起方) 信令服务器 浏览器 B(接收方) ① 创建Offer ② 发送Offer ③ 转发Offer ④ 创建Answer ⑤ 发送Answer ⑥ 转发Answer ⑦ 交换ICE候选(ICE Candidate) 双方通过信令服务器交换网络路径信息,找到最优连接 ⑧ 连接建立!开始传输音视频数据 注:信令服务器只参与前7步,数据建立后不再经过服务器

18.5 几个容易踩的坑

最后,分享几个我实际开发中遇到的教训:

  • ICE候选没收集完就发OffercreateOffer 默认会等ICE候选收集完,但如果你设置了 iceCandidatePoolSize: 0,可能候选还没生成就发走了。建议用默认值。
  • 信令服务器没做去重:如果同一个消息被重复转发,RTCPeerConnection会报错。我踩过这个坑,后来在信令服务器加了个消息ID去重逻辑。
  • 忘记处理连接状态变化:监听 oniceconnectionstatechange 事件,当状态变成 disconnectedfailed 时,要主动重连或提示用户。

小技巧:调试阶段,可以在浏览器控制台打印 pc.iceConnectionStatepc.connectionState,实时查看连接状态。我习惯在状态变化时打个日志,方便定位问题。

好了,这一章的内容就到这。RTCPeerConnection的创建和信令交换,是WebRTC最核心的环节。你把它搞明白了,后面那些音视频传输、数据通道、屏幕共享什么的,都是在此基础上加功能而已。


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