1. WebRTC基础回顾:实时通信的核心原理、三大API与信令服务器

各位同学,欢迎来到《WebRTC性能优化大师课》的第一章。

说实话,我见过太多人一上来就扎进代码里,结果连WebRTC为什么能「实时」都没搞明白。我自己刚接触时也踩过这个坑——调了一周的视频卡顿,最后发现是信令流程写错了。嗯,这节课我们就从根上把这些东西捋清楚。

1.1 实时通信的核心原理:到底「实时」在哪?

WebRTC的全称是Web Real-Time Communication。说白了,就是让浏览器之间能直接传音视频和数据,不需要中间服务器转发。

你想想看,传统的视频通话,数据要先到服务器,再转发给对方。这中间至少多走了一跳,延迟自然就上去了。WebRTC走的是P2P(点对点)路线,浏览器A直接连浏览器B。

核心目标: 端到端延迟控制在 400ms 以内,甚至更低。

但这里有个问题:A和B怎么找到对方?它们各自在局域网里,IP地址是私有的。这就引出了两个关键技术:

  • NAT穿透(ICE框架):通过STUN/TURN服务器,帮双方找到对方的公网地址。
  • SDP协商:双方交换各自的媒体能力(支持什么编码、分辨率、带宽等)。

我在项目中遇到过最头疼的情况:明明STUN服务器配置对了,但就是连不上。后来发现是防火墙把UDP端口封了。所以,TURN服务器作为中继兜底,是生产环境必须准备的。

1.2 三大API:WebRTC的「三驾马车」

WebRTC暴露给开发者的核心API就三个。我习惯把它们比作「采集-传输-数据」三条线。

1.2.1 getUserMedia:采集音视频

这个API负责打开摄像头和麦克风。代码很简单:

navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
}).then(stream => {
  // stream 就是本地视频流
  localVideo.srcObject = stream;
}).catch(err => {
  console.error('摄像头被拒绝:', err);
});
避坑指南: 我曾经在生产环境发现,某些安卓浏览器在getUserMedia成功后,如果用户切到后台再切回来,视频流会黑屏。解决方案是监听 `ended` 事件,重新获取流。

这里有个细节:video: true 默认使用前置摄像头。如果你想指定后置,可以传 facingMode: { exact: 'environment' }

1.2.2 RTCPeerConnection:建立P2P连接

这是WebRTC最核心的API。它负责ICE协商、媒体流传输、带宽自适应等所有底层工作。

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'turn:your-turn-server.com', credential: 'password' }
  ]
});

// 添加本地流
localStream.getTracks().forEach(track => {
  pc.addTrack(track, localStream);
});

// 监听远程流
pc.ontrack = event => {
  remoteVideo.srcObject = event.streams[0];
};

// 创建Offer
pc.createOffer().then(offer => pc.setLocalDescription(offer));

你想想看,这个API背后做了多少事:

  • 收集所有可能的网络候选(本地IP、公网IP、中继地址)
  • 与远端交换SDP
  • 选择最优路径建立连接
  • 实时监控网络质量,动态调整码率
注意: RTCPeerConnection 实例是有状态的。创建后不能直接复用。如果你要重新连接,必须 new 一个新的实例。我见过有人试图复用同一个pc对象,结果ICE状态永远卡在「connecting」。

1.2.3 RTCDataChannel:传数据,不传媒体

很多人以为WebRTC只能传视频。其实它还能传任意二进制数据。RTCDataChannel就是干这个的。

const dc = pc.createDataChannel('myChannel', {
  ordered: false,        // 允许乱序到达,提高速度
  maxRetransmits: 0      // 不重传,丢包就丢
});

dc.onopen = () => {
  dc.send('Hello from peer A!');
};

dc.onmessage = event => {
  console.log('收到:', event.data);
};

我在做文件传输工具时,就用了DataChannel。它的延迟比WebSocket低得多,因为走的是P2P路径。但要注意:DataChannel的可靠性取决于你设置的参数。如果你需要100%可靠,就设 ordered: truemaxRetransmits: -1

1.3 信令服务器的作用与搭建

前面说了,WebRTC是P2P的。但建立连接之前,双方需要交换一些「元数据」——比如SDP和ICE候选。这个交换过程,就叫信令。

信令服务器本质上就是一个消息中转站。它不参与媒体流传输,只负责传递控制信息。

信令服务器的作用:
  • 房间管理:谁在哪个房间,谁要和谁连
  • 消息转发:把A的Offer转发给B,把B的Answer转发给A
  • ICE候选交换:把A的ICE候选发给B,反之亦然

最简单的信令服务器,用Node.js+WebSocket就能搭:

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const rooms = {};

wss.on('connection', ws => {
  ws.on('message', message => {
    const data = JSON.parse(message);
    
    if (data.type === 'join') {
      // 加入房间
      if (!rooms[data.room]) rooms[data.room] = [];
      rooms[data.room].push(ws);
    } else if (data.type === 'offer' || data.type === 'answer' || data.type === 'candidate') {
      // 转发给房间内其他成员
      rooms[data.room].forEach(client => {
        if (client !== ws) {
          client.send(JSON.stringify(data));
        }
      });
    }
  });
});

这个例子只有30行代码,但已经能跑通一个基本的信令流程了。当然,生产环境还需要考虑:

  • 用户认证(谁是谁)
  • 房间容量限制
  • 断线重连时的状态恢复
我的经验: 信令服务器最好用WebSocket而不是HTTP轮询。因为信令消息是双向的、实时的。HTTP轮询会增加至少200ms的延迟,这在实时通信中是不可接受的。

1.4 本章知识体系总览

下面这张图,把WebRTC的核心流程串起来了。我建议你多看几遍,理解每一步的先后顺序。

WebRTC 核心流程 浏览器 A 浏览器 B 信令服务器 Offer 转发Offer Answer 转发Answer ICE候选收集 ICE候选收集 ICE候选交换(经信令) P2P 媒体/数据通道 浏览器 信令 P2P连接

从这张图你能看到:信令服务器只在连接建立前起作用。一旦P2P通道打通,媒体流和数据就直接在浏览器之间传输了。这也是WebRTC低延迟的根本原因。

1.5 本章小结

好,我们快速回顾一下这节课的核心:

  • 实时通信的核心:P2P直连 + NAT穿透 + SDP协商
  • 三大API:getUserMedia(采集)、RTCPeerConnection(传输)、RTCDataChannel(数据)
  • 信令服务器:只传控制信息,不传媒体。用WebSocket实现最简单

下一节课,我们会深入RTCPeerConnection的内部机制,看看ICE协商到底是怎么工作的。到时候我会分享一个我踩过的坑——ICE重启导致连接中断的案例。


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