如果你写过 WebRTC 的 Demo,你一定对 new RTCPeerConnection() 这行代码不陌生。它是整个 WebRTC 协议栈的"心脏",也是最复杂、最容易让人掉坑里的 API 对象。
很多初学者觉得 WebRTC 难,其实就难在这个对象封装了太多底层逻辑:从 SDP 协商、ICE 打洞、DTLS 握手到 RTP 媒体传输,所有的脏活累活都是它在干。
今天,结合我多年的踩坑经验,我们参考 webrtc.mthli.com 的经典理论,把这个"黑盒"打开,看看两个浏览器之间到底是如何建立起一条"超级管道"的。
一、 RTCPeerConnection:超级管家
在 WebRTC 中,PeerConnection (PC) 不仅仅是一个"连接",它更像是一个负责协调所有子系统的管家。
当你创建一个 PC 实例时,你其实启动了一套复杂的子系统集合:
- ICE Agent:负责找路(打洞)。
- DTLS Transport:负责安全加密。
- RTP/SCTP Engine:负责媒体和数据的打包发送。
我们的任务,就是通过 API 指挥这个管家,让它按照正确的顺序工作。
二、 灵魂伴侣的"求偶舞":Offer/Answer 机制
两个浏览器(Peer A 和 Peer B)互不相识,怎么连?它们需要交换"元数据"(SDP)。这个过程被称为 JSEP (JavaScript Session Establishment Protocol)。
你可以把它想象成"相亲交换简历":
- A (Offer):生成一份简历(SDP),写着"我有摄像头,支持 H.264,我的 IP 是..."
- Signal:通过服务器(信令服务器)把简历递给 B。
- B (Answer):看了 A 的简历,回一份简历(SDP),写着"好,我也支持 H.264,我这就打开麦克风..."
核心流程图解
text
Peer A (Offerer) Peer B (Answerer)
| |
| 1. createOffer() |
| 2. setLocalDescription(offer) |
| |
| 3. [信令发送 Offer SDP] ------------->|
| | 4. setRemoteDescription(offer)
| | 5. createAnswer()
| | 6. setLocalDescription(answer)
| |
|<------------- [信令发送 Answer SDP] |
| |
| 7. setRemoteDescription(answer) |
| |
[ P2P 连接建立,ICE 开始打洞,媒体流传输 ]
⚠️ 避坑指南 :
setLocalDescription() 不仅是保存 SDP,它还会触发 ICE 收集。很多新手忘了调这个,导致 ICE Candidate 一个都收不到。
三、 并行任务:ICE Candidate 的接力跑
在交换 SDP 的同时,PC 内部的 ICE Agent 已经在疯狂工作了。它会尝试各种方式找到自己的出口地址:
- Host:本机内网 IP。
- Srflx:通过 STUN 服务器拿到的公网 IP。
- Relay:通过 TURN 服务器拿到的中转 IP。
这就涉及到一个关键概念:Trickle ICE(滴漏模式)。
以前的协议要求必须等所有地址都收集齐了再发 SDP(这可能要好几秒)。现在的 WebRTC 允许"边收集边发"。每发现一个新地址(Candidate),PC 就会触发 onicecandidate 事件。
javascript
pc.onicecandidate = (event) => {
if (event.candidate) {
// 别犹豫,立刻通过信令发给对方!
signalingChannel.send(JSON.stringify({
'candidate': event.candidate
}));
}
};
对方收到后,调用 addIceCandidate() 把这个地址喂给自己的 ICE Agent。
四、 安全握手:DTLS 与 SRTP
当 ICE 打通了 UDP 通路后,你以为视频就开始播了吗?并没有。
WebRTC 强制要求加密。这时候,DTLS (Datagram Transport Layer Security) 登场了。它就像是 UDP 版的 TLS/SSL。
- 握手:Peer A 和 Peer B 在刚打通的 UDP 连路上进行 DTLS 握手,验证对方的身份(通过 SDP 中的指纹 Fingerprint)。
- 密钥导出 :握手成功后,双方会生成两套密钥。
- 一套给 SRTP:专门加密音视频流。
- 一套给 SCTP:专门加密 DataChannel 的数据。
这一步完全是自动的 ,应用层几乎无感。但如果你的 SDP 指纹不对,或者防火墙拦截了 DTLS 包,连接就会卡在 connecting 状态死活连不上。
五、 媒体与数据的载体:Transceiver
连接建立好了,总得传点什么吧?
1. 媒体流 (MediaStream)
早期的 API 是 addStream,现在官方强烈推荐使用 Transceiver (收发器) 模式。
pc.addTrack(track, stream) 实际上就是创建了一个 Transceiver。
Transceiver 允许你极其精细地控制流的方向:
transceiver.direction = 'sendrecv'(双向)transceiver.direction = 'recvonly'(只收不发,常用于观众端)
2. 数据通道 (DataChannel)
除了音视频,WebRTC 还能传文件、传游戏操作指令。
pc.createDataChannel("chat") 会建立一条基于 SCTP 的通道。它的牛逼之处在于:它既可以像 TCP 一样可靠,也可以像 UDP 一样低延迟(允许丢包),全看你怎么配。
六、 调试神技与最佳实践
作为资深开发,最后送给大家几条"保命"建议:
1. 拥抱 "Perfect Negotiation"
处理 SDP 冲突(比如双方同时发起呼叫)是 WebRTC 开发的噩梦。
现在社区推荐一种"完美协商"模式(Polite Peer),规定一方为"礼貌方",一方为"冲动方",通过状态机自动处理冲突,彻底告别 HaveLocalOffer 错误。
2. 状态监测的艺术
PC 有两个核心状态,别看混了:
connectionState:综合状态(包括 DTLS 等),看这个最准。iceConnectionState:只看物理链路通不通。
如果 iceConnectionState 是 connected 但 connectionState 是 failed,多半是 DTLS 握手挂了。
3. 唯一的真神:chrome://webrtc-internals
不管你遇到什么问题(黑屏、连不上、画质差),第一反应不应该是改代码,而是打开这个页面。
- 看
IceCandidatePair:到底选了哪条路?是不是走了中转(Relay)? - 看
VideoBwe:带宽估计是多少?是不是被限速了?
总结
RTCPeerConnection 就像是一个精密的瑞士军刀。它虽然 API 繁多,概念复杂,但一旦你理清了 Signaling -> ICE -> DTLS -> Media 这条主线,就会发现它设计得非常逻辑严密。
WebRTC 的学习曲线虽然陡峭,但当你第一次在两个网页间看到对方画面的那一刻,那种成就感是无与伦比的。
希望这篇博文能帮你理清 Peer Connection 的脉络