19、WebRTC入门:WebRTC架构、信令与NAT穿透、P2P连接建立

WebRTC,全称是Web Real-Time Communication。说白了,就是让浏览器之间能直接传音视频数据,不需要经过服务器中转。我最早接触WebRTC是在2015年,当时公司要做一款在线教育产品,要求低延迟互动。嗯,那时候WebRTC还没完全标准化,踩了不少坑。但今天回头看,它已经是实时音视频领域的事实标准了。

19.1 WebRTC整体架构

WebRTC的架构,我习惯把它分成三层来看:

  • API层:暴露给开发者的JavaScript接口,比如getUserMediaRTCPeerConnectionRTCDataChannel
  • 引擎层:包括音频引擎、视频引擎、网络传输引擎。这部分是C++实现的,负责编解码、降噪、丢包重传等脏活累活。
  • 底层:包括音频采集/渲染、视频采集/渲染、网络I/O。这部分直接跟硬件和操作系统打交道。

你想想看,为什么WebRTC能火?因为它把最复杂的部分——音视频编解码、网络抖动缓冲、拥塞控制——都封装好了。开发者只需要调用几个API,就能实现点对点通话。我在项目中见过太多团队试图自己造轮子,最后都老老实实回来用WebRTC。

核心要点:WebRTC不是单一协议,而是一整套解决方案。它包含了音视频采集、编解码、网络传输、NAT穿透等所有环节。

WebRTC 架构分层图 应用层 (Application) getUserMedia | RTCPeerConnection | RTCDataChannel 引擎层 (Engine) 音频引擎 Opus编解码 降噪/回声消除 视频引擎 VP8/VP9/H264 Jitter Buffer 网络引擎 ICE/STUN/TURN DTLS/SRTP 底层 (Hardware & Network) 音频采集/渲染 | 视频采集/渲染 | UDP/TCP Socket

19.2 信令:建立连接前的握手

很多人第一次接触WebRTC时都会问:不是说P2P吗?为什么还需要服务器?

原因很简单:两个浏览器之间要建立连接,总得先知道对方在哪吧?信令就是干这个的。它负责交换三个关键信息:

  1. 会话描述(SDP):包含音视频编解码能力、IP地址、端口等信息。
  2. 网络候选(ICE Candidate):告诉对方自己有哪些可用的网络路径。
  3. 密钥信息:用于DTLS加密协商。

WebRTC规范并没有规定信令怎么传。你可以用WebSocket、HTTP轮询、甚至用鸽子送信(开玩笑的)。我在项目中一般用WebSocket,因为它双向实时,延迟低。

个人经验:信令服务器虽然简单,但千万别掉以轻心。我曾经遇到过信令丢失导致连接失败的情况——客户端发了offer,但answer没收到。后来加了ACK确认机制才解决。

19.3 NAT穿透:为什么需要STUN/TURN

现实世界中的设备,大部分都在NAT后面。你想想看,家里的路由器、公司的防火墙,都是NAT。它们把内网IP映射成公网IP,但同时也阻止了外部主动发起的连接。

WebRTC解决这个问题的方案是ICE框架,它包含三种候选类型:

候选类型 来源 成功率 说明
Host 本机网卡 只有局域网内可用
Srflx STUN服务器 能穿透大部分NAT
Relay TURN服务器 所有流量经服务器中转

STUN的原理很简单:客户端向STUN服务器发请求,服务器返回客户端的公网IP和端口。这样客户端就知道自己在公网上的地址了。

但STUN不是万能的。有些对称NAT,STUN也穿透不了。这时候就得用TURN——所有音视频数据都经过服务器中转。说白了,TURN是最后的保底方案。

避坑指南:我曾经在一个项目中只部署了STUN,没部署TURN。结果有20%的用户连不上——他们都在对称NAT后面。后来加了TURN服务器,连接率才到99%以上。记住:生产环境一定要部署TURN。

19.4 P2P连接建立:ICE流程详解

P2P连接的建立,我习惯把它分成四个步骤:

  1. 收集候选:双方各自收集Host、Srflx、Relay候选。
  2. 交换候选:通过信令通道交换候选列表。
  3. 连通性检查:双方互相发送STUN binding请求,测试哪对候选能连通。
  4. 选最优路径:从所有连通的候选对中,选择优先级最高的。

ICE的连通性检查很有意思。它会按照候选对的优先级排序,从高到低依次尝试。一旦找到能连通的,就停止检查。这个过程通常只需要几百毫秒。

// 伪代码:ICE连通性检查流程
function iceConnectivityCheck(localCandidates, remoteCandidates) {
    // 1. 生成候选对
    let pairs = [];
    for (let local of localCandidates) {
        for (let remote of remoteCandidates) {
            pairs.push({ local, remote, priority: computePriority(local, remote) });
        }
    }
    
    // 2. 按优先级排序
    pairs.sort((a, b) => b.priority - a.priority);
    
    // 3. 依次检查
    for (let pair of pairs) {
        let result = sendStunBinding(pair.local, pair.remote);
        if (result.success) {
            return pair;  // 找到可用路径
        }
    }
    
    return null;  // 所有路径都失败
}

关键点:ICE的优先级计算有一套复杂的公式。简单来说,Relay候选优先级最低,Host候选优先级最高。因为直连肯定比中转快。

19.5 完整的连接建立时序

把信令、NAT穿透、ICE流程串起来,完整的P2P连接建立是这样的:

WebRTC P2P连接建立时序 客户端A 信令服务器 客户端B ① 创建Offer (SDP) 转发Offer ② 返回Answer (SDP) 转发Answer ③ 收集ICE候选 收集ICE候选 ④ 交换候选 交换候选 ⑤ ICE连通性检查 (STUN) ⑥ P2P连接建立 (DTLS+SRTP) ⑦ 音视频数据传输 时间

嗯,这里要注意:步骤⑤的ICE连通性检查,是直接在客户端A和客户端B之间进行的,不经过信令服务器。这也是为什么它叫P2P——信令服务器只负责牵线搭桥,真正的数据流是直连的。

我的建议:调试WebRTC连接问题时,一定要看ICE状态变化。Chrome的chrome://webrtc-internals页面非常有用,能看到所有候选、连通性检查结果、选中的路径。我排查问题时,90%的线索都来自这里。

19.6 实际项目中的注意事项

最后分享几个我在项目中踩过的坑:

  • STUN/TURN服务器要就近部署:如果用户在欧洲,服务器在美国,延迟会很高。最好用云服务商提供的全球节点。
  • ICE重启:网络切换时(比如WiFi切4G),ICE会重新协商。这时候要处理好状态迁移,否则会断连。
  • 信令超时:信令交换必须在合理时间内完成。我一般设置30秒超时,超时后提示用户检查网络。
  • 编解码协商:不是所有浏览器都支持H264。我习惯优先用VP8,兼容性最好。

WebRTC入门其实不难,难的是把各种边界情况处理好。但只要你理解了信令、NAT穿透、ICE这三个核心概念,剩下的就是经验积累了。


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