1. 引言
实时通信(Real-Time Communication,RTC)是互联网应用的重要能力,覆盖音视频通话、直播连麦、在线会议、远程协作等场景。WebRTC(Web Real-Time Communication)是当前实现浏览器与移动端实时音视频通信的主流技术方案。本文围绕 RTC 与 WebRTC 通路展开,梳理整体架构、核心协议、媒体传输链路以及实践中的关键问题。
2. RTC 与 WebRTC 的基本概念
RTC 是一类技术的统称,强调低延迟、双向、实时的数据交换能力。WebRTC 则是 Google 主导、由 W3C 和 IETF 标准化的开源实时通信框架,让浏览器、移动应用之间无需安装插件即可进行音视频通话和数据传输。
WebRTC 的核心能力包括:
- 音视频采集与渲染:通过浏览器原生 API 获取摄像头、麦克风、屏幕共享等媒体流。
- 媒体编解码:内置 Opus、VP8、VP9、H.264 等编解码器,兼顾音质、画质与带宽。
- 网络传输:基于 SRTP 加密传输媒体数据,支持 UDP、TCP 等多种传输方式。
- NAT 穿透:借助 STUN、TURN 服务器解决内网穿透问题。
3. WebRTC 整体通路架构
一条完整的 WebRTC 通路通常由信令层、媒体层和网络穿透层三部分组成。信令负责建立会话,媒体负责传输音视频数据,网络穿透负责打通端到端连接。这三层各司其职,又相互配合,共同构成一条从「发起方」到「接收方」的完整实时通信链路。
我们可以把这条通路想象成一次「打电话」的过程:信令层负责拨号和接听(建立会话),媒体层负责说话和听声(传输音视频),网络穿透层则负责在复杂的网络环境中找到一条能真正连通的路。下面这张图展示了三层之间的协作关系:
flowchart TD A[发起方] -->|信令| B[信令服务器] B -->|信令| C[接收方] A -->|媒体流| D[P2P 连接] C -->|媒体流| D A -->|STUN 请求| E[STUN 服务器] C -->|STUN 请求| E A -->|TURN 中继| F[TURN 服务器] C -->|TURN 中继| F F -->|媒体转发| D从图中可以看到,信令服务器只负责「牵线搭桥」,不参与媒体数据的传输;真正的音视频数据通过 P2P 连接直接传输,当 P2P 无法打通时,才借助 TURN 服务器进行中继转发。这种「信令与媒体分离」的设计,是 WebRTC 高效且灵活的关键所在。
4. 信令通路
信令是 WebRTC 会话建立的起点,负责交换会话描述(SDP)、网络候选(ICE Candidate)以及媒体控制信息。WebRTC 标准本身没有规定信令协议,开发者可以基于 WebSocket、HTTP 或自定义 TCP 协议实现。
如果把一次 WebRTC 通话比作一场「相亲」,那么信令通路就是中间牵线的「红娘」:双方并不直接认识,需要红娘帮忙传递「自我介绍」(Offer SDP)、「回应」(Answer SDP)以及「联系方式」(ICE Candidate)。红娘只负责传话,不参与双方之后的相处——正如信令服务器只转发消息,不承载媒体数据。
典型的信令流程如下:
- 发起方创建 RTCPeerConnection,生成 Offer SDP。
- 发起方通过信令服务器将 Offer 发送给接收方。
- 接收方收到 Offer 后创建 Answer SDP,并通过信令服务器回传。
- 双方交换 ICE Candidate,完成网络路径探测。
- 连接建立后,媒体流开始传输。
这个流程看似简单,却承载着整个会话能否建立的关键。信令通路是否稳定、消息是否有序可靠,直接决定了后续媒体通路能否顺利打通。因此在实际项目中,信令服务器的选型和部署往往需要格外重视。
5. 媒体通路
媒体通路是 WebRTC 中真正承载音视频数据的核心链路。它负责从采集端获取媒体流,经过编码压缩、加密封装,再通过网络传输到接收端,最终解码渲染。与信令通路不同,媒体通路对延迟、丢包和抖动极其敏感,因此其协议栈和算法设计都围绕「实时、可靠、低延迟」展开。
一条完整的媒体通路可以概括为:采集 → 编码 → 加密(SRTP)→ 封装(RTP)→ 传输 → 接收(Jitter Buffer)→ 解密 → 解码 → 渲染。下面这张图展示了媒体数据从发送端到接收端的完整流向:
flowchart LR A[采集设备] -->|原始音视频| B[编码器] B -->|压缩后数据| C[SRTP 加密] C -->|加密后数据| D[RTP 封装] D -->|RTP 包| E[网络传输] E -->|RTP 包| F[Jitter Buffer] F -->|有序数据| G[SRTP 解密] G -->|解密后数据| H[解码器] H -->|原始音视频| I[渲染播放] E -->|RTCP 反馈| J[拥塞控制] J -->|码率调整| B5.1 RTP/RTCP 报文结构
RTP(Real-time Transport Protocol,实时传输协议)是媒体数据的承载协议,运行在 UDP 之上。每个 RTP 报文由固定头部和负载组成,固定头部至少 12 字节,关键字段如下:
- 版本号(V):2 位,当前版本为 2。
- 填充位(P):1 位,置 1 表示报文末尾包含填充字节。
- 扩展位(X):1 位,置 1 表示头部后紧跟扩展头。
- CSRC 计数(CC):4 位,表示 CSRC 标识符的个数。
- 标记位(M):1 位,用于标记帧边界等特殊事件。
- 负载类型(PT):7 位,标识媒体编码格式,如 Opus 为 111、VP8 为 96。
- 序列号(Sequence Number):16 位,每发送一个包递增 1,接收端据此检测丢包和乱序。
- 时间戳(Timestamp):32 位,反映媒体采样时刻,接收端据此恢复播放节奏。
- 同步源标识(SSRC):32 位,唯一标识一个媒体流来源。
RTCP(RTP Control Protocol,RTP 控制协议)与 RTP 配合工作,负责传输质量统计和会话控制。RTCP 报文周期性发送,常见类型包括:
- SR(Sender Report):发送端报告,包含发送包数、字节数以及时间戳映射,用于同步音视频和计算往返时延。
- RR(Receiver Report):接收端报告,包含丢包率、累计丢包数、抖动和延迟等信息,反馈给发送端用于拥塞控制。
- SDES(Source Description):描述源信息,如 CNAME(规范名称),用于关联同一会话中的多个媒体流。
- BYE:通知对端会话结束。
5.2 SRTP 加密流程
SRTP(Secure RTP)在 RTP 基础上增加了加密、认证和防重放保护,确保媒体内容在传输过程中不被窃听或篡改。SRTP 的加密流程如下:
- 密钥协商:在信令阶段,双方通过 DTLS-SRTP 握手协商出 SRTP 主密钥(Master Key)和会话密钥(Session Key)。
- 加密:发送端使用 AES 算法对 RTP 负载进行加密,默认采用 AES-CM(Counter Mode)模式,将 RTP 的 SSRC、序列号等字段作为加密盐,生成密钥流与明文异或得到密文。
- 认证:对 RTP 头部和加密后的负载计算 HMAC-SHA1 认证标签,附加在报文末尾,防止数据被篡改。
- 防重放:接收端维护一个重放窗口,根据序列号判断收到的包是否重复或过期,丢弃重放包。
- 解密与验证:接收端先验证认证标签,确认数据完整后,再用相同的会话密钥解密负载,还原原始 RTP 数据。
通过这套流程,SRTP 在几乎不增加额外延迟的前提下,为媒体数据提供了端到端的机密性、完整性和防重放保护。
5.3 Jitter Buffer 工作原理
网络传输存在不确定性,数据包到达接收端的间隔并不均匀,这种时间上的波动称为抖动(Jitter)。如果直接按到达顺序播放,会导致声音忽快忽慢、画面卡顿。Jitter Buffer(抖动缓冲)正是为解决这一问题而设计的。
Jitter Buffer 的核心思想是:接收端先将收到的 RTP 包暂存在缓冲区中,等待一段时间(称为缓冲深度),再按正确的序列号和播放时间戳依次取出播放。其工作流程如下:
- 缓存排队:收到的 RTP 包按序列号排序后放入缓冲区,等待播放时机。
- 抖动估计:根据 RTCP RR 报告和本地到达间隔,动态估算当前网络的抖动程度。
- 深度调整:抖动较大时增大缓冲深度以吸收波动,抖动较小时减小缓冲深度以降低延迟。
- 丢包处理:若某个包超时未到,根据编解码器能力选择静音填充、前向纠错(FEC)恢复或重传请求。
- 平滑播放:按时间戳节奏从缓冲区取出数据交给解码器,保证播放的连续性和稳定性。
Jitter Buffer 在延迟和流畅度之间需要做权衡:缓冲越深,抗抖动能力越强,但端到端延迟越大;缓冲越浅,延迟越低,但更容易因抖动导致卡顿。WebRTC 中的 NetEq 模块就是针对音频的 Jitter Buffer 实现,它结合语音活动检测和舒适噪声生成,在弱网下也能保持较好的听感。
5.4 拥塞控制算法(GCC)
拥塞控制的目标是根据网络状况动态调整发送码率,避免网络过载导致丢包和延迟飙升。WebRTC 默认采用 GCC(Google Congestion Control)算法,它综合基于丢包和基于延迟两种信号来估计可用带宽。
GCC 的核心机制如下:
- 基于丢包的拥塞检测:发送端根据 RTCP RR 报告中的丢包率判断网络是否拥塞。当丢包率超过阈值时,判定为拥塞,按比例降低码率;当丢包率较低时,逐步尝试提升码率。
- 基于延迟的拥塞检测:接收端通过计算数据包到达间隔的延迟梯度(Trendline Estimator),判断网络队列是否正在堆积。若延迟持续上升,说明网络即将拥塞,提前降低码率。
- 码率调整策略:GCC 采用 AIMD(加性增、乘性减)策略——网络良好时线性增加码率,检测到拥塞时乘性降低码率,从而快速响应网络变化。
- 与编码器联动:估算出的可用带宽会反馈给编码器,编码器据此调整目标码率、分辨率和帧率,实现「按带宽自适应编码」。
除了 GCC,WebRTC 还支持 REMB(Receiver Estimated Maximum Bitrate)等机制,由接收端估算带宽并通过 RTCP 反馈给发送端。这些算法共同保证了在带宽波动、弱网环境下,通话仍能维持可用的音视频质量。
6. 实战:构建最小 WebRTC 通话应用
下面通过一个最小可运行的示例,演示如何用 Node.js + WebSocket 搭建信令服务器,并用 HTML + JavaScript 实现两个浏览器标签页之间的实时音视频通话。整个流程围绕三个关键步骤展开:创建 RTCPeerConnection、交换 SDP、处理 ICE 候选。
6.1 信令服务器(Node.js + WebSocket)
信令服务器只负责转发消息,不处理媒体数据。这里使用 ws 库实现一个简单的 WebSocket 服务,将任意一端发来的消息广播给另一端。
// server.js const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); // 简单房间:最多容纳两个客户端 let clients = []; wss.on('connection', (ws) => { clients.push(ws); console.log('客户端已连接,当前数量:', clients.length); // 转发消息给对端 ws.on('message', (message) => { const data = JSON.parse(message); console.log('收到消息:', data.type); // 把消息转发给另一个客户端 const peer = clients.find((c) => c !== ws); if (peer && peer.readyState === WebSocket.OPEN) { peer.send(JSON.stringify(data)); } }); ws.on('close', () => { clients = clients.filter((c) => c !== ws); console.log('客户端已断开,当前数量:', clients.length); }); }); console.log('信令服务器已启动,监听 8080 端口');运行前先安装依赖:npm install ws,然后执行node server.js启动服务。
6.2 客户端(HTML + JavaScript)
客户端负责采集音视频、创建 RTCPeerConnection、交换 SDP 和处理 ICE 候选。下面是一个可直接在浏览器中打开的页面。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>最小 WebRTC 通话</title> </head> <body> <video id="local" autoplay playsinline muted></video> <video id="remote" autoplay playsinline></video> <button id="start">开始通话</button> <script> const ws = new WebSocket('ws://localhost:8080'); const localVideo = document.getElementById('local'); const remoteVideo = document.getElementById('remote'); let pc = null; // 1. 创建 RTCPeerConnection,并监听 ICE 候选 function createPeerConnection() { pc = new RTCPeerConnection(); // 收集到 ICE 候选后,通过信令发送给对端 pc.onicecandidate = (event) => { if (event.candidate) { ws.send(JSON.stringify({ type: 'candidate', candidate: event.candidate })); } }; // 收到对端媒体流后渲染到页面 pc.ontrack = (event) => { remoteVideo.srcObject = event.streams[0]; }; return pc; } // 2. 发起方:创建 Offer 并交换 SDP async function startCall() { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = stream; pc = createPeerConnection(); stream.getTracks().forEach((track) => pc.addTrack(track, stream)); // 创建 Offer SDP 并设置为本地描述 const offer = await pc.createOffer(); await pc.setLocalDescription(offer); // 通过信令服务器发送 Offer ws.send(JSON.stringify({ type: 'offer', sdp: pc.localDescription })); } // 3. 接收方:收到 Offer 后创建 Answer async function handleOffer(data) { pc = createPeerConnection(); await pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); ws.send(JSON.stringify({ type: 'answer', sdp: pc.localDescription })); } // 处理信令消息 ws.onmessage = async (event) => { const data = JSON.parse(event.data); if (data.type === 'offer') { await handleOffer(data); } else if (data.type === 'answer') { // 发起方设置远端描述 await pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); } else if (data.type === 'candidate') { // 双方添加 ICE 候选 await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); } }; document.getElementById('start').addEventListener('click', startCall); </script> </body> </html>6.3 关键步骤说明
- 创建 RTCPeerConnection:通过
new RTCPeerConnection()建立连接对象,并注册onicecandidate和ontrack回调,分别处理候选收集和远端媒体流渲染。 - 交换 SDP:发起方调用
createOffer生成 Offer,接收方调用createAnswer生成 Answer,双方通过信令服务器交换后,用setLocalDescription和setRemoteDescription完成协商。 - ICE 候选处理:双方在收集到候选后通过信令发送给对方,收到后用
addIceCandidate添加,最终打通端到端媒体通路。
6.4 运行与验证
完成上述代码后,按照以下步骤运行并验证通话是否建立成功:
- 启动信令服务器:在项目目录下执行
npm install ws安装依赖,然后运行node server.js,看到「信令服务器已启动,监听 8080 端口」即表示服务正常。 - 打开两个浏览器标签页:分别访问
index.html,建议使用 Chrome 或 Edge 浏览器,并确保两个页面都能访问摄像头和麦克风权限。 - 发起通话:在第一个标签页点击「开始通话」,浏览器会弹出权限请求,允许后本地视频画面会显示在
local视频元素中。 - 接收通话:第二个标签页会自动收到 Offer 并创建 Answer,随后远端视频画面会显示在
remote视频元素中。 - 验证媒体流:如果两个标签页都能看到对方的视频画面,说明 SDP 协商和 ICE 候选交换均已成功,P2P 媒体通路已经建立。
在验证过程中,可以打开浏览器开发者工具(F12)的 Console 面板,观察 WebSocket 消息的收发日志。信令服务器端也会打印收到消息的类型,方便排查问题。
6.5 常见问题排查
实际运行中可能会遇到以下问题,这里给出对应的排查思路:
- 页面无法获取摄像头或麦克风:检查浏览器是否授予了媒体权限,确认没有其他应用占用摄像头。若在非 localhost 环境下访问,需要配置 HTTPS 才能使用
getUserMedia。 - WebSocket 连接失败:确认信令服务器已启动且端口未被占用,检查客户端连接的地址和端口是否与服务器一致。
- 双方无法建立 P2P 连接:在 localhost 环境下通常可以直接连通。若部署到公网,需要配置 STUN 服务器(如 Google 的
stun:stun.l.google.com:19302),否则 NAT 后的设备无法发现彼此。 - 画面卡顿或延迟高:检查网络带宽是否充足,可尝试降低分辨率或帧率。弱网环境下可考虑部署 TURN 服务器作为中继。
- 只有一方能看到画面:检查
ontrack回调是否正确触发,确认双方都执行了addTrack并成功交换了 SDP。
6.6 扩展建议
在音视频通话的基础上,WebRTC 还提供了RTCDataChannel,用于在两端之间直接传输文本、文件等非媒体数据,且同样走 P2P 加密通道,不经过信令服务器。下面在 6.2 的客户端代码基础上,补充一个实时文本聊天的完整示例。
首先在createPeerConnection中创建数据通道,并监听对端创建的数据通道:
// 在 createPeerConnection 中补充数据通道逻辑 function createPeerConnection() { pc = new RTCPeerConnection(); // 收集到 ICE 候选后,通过信令发送给对端 pc.onicecandidate = (event) => { if (event.candidate) { ws.send(JSON.stringify({ type: 'candidate', candidate: event.candidate })); } }; // 收到对端媒体流后渲染到页面 pc.ontrack = (event) => { remoteVideo.srcObject = event.streams[0]; }; // 监听对端创建的数据通道 pc.ondatachannel = (event) => { setupDataChannel(event.channel); }; return pc; } // 初始化数据通道 let dataChannel = null; function setupDataChannel(channel) { dataChannel = channel; dataChannel.onopen = () => { console.log('数据通道已打开'); appendMessage('系统', '数据通道已建立,可以开始聊天'); }; dataChannel.onmessage = (event) => { appendMessage('对端', event.data); }; dataChannel.onclose = () => { console.log('数据通道已关闭'); }; } // 发起方:在 startCall 中创建数据通道 async function startCall() { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = stream; pc = createPeerConnection(); stream.getTracks().forEach((track) => pc.addTrack(track, stream)); // 发起方主动创建数据通道 setupDataChannel(pc.createDataChannel('chat')); // 创建 Offer SDP 并设置为本地描述 const offer = await pc.createOffer(); await pc.setLocalDescription(offer); // 通过信令服务器发送 Offer ws.send(JSON.stringify({ type: 'offer', sdp: pc.localDescription })); } // 发送聊天消息 function sendMessage() { const input = document.getElementById('messageInput'); const text = input.value.trim(); if (text && dataChannel && dataChannel.readyState === 'open') { dataChannel.send(text); appendMessage('我', text); input.value = ''; } } // 在页面中追加消息 function appendMessage(sender, text) { const log = document.getElementById('chatLog'); const line = document.createElement('div'); line.textContent = sender + ':' + text; log.appendChild(line); }对应的 HTML 页面需要增加聊天输入框和消息展示区域:
<!-- 在 body 中补充聊天界面 --> <div id="chatLog" style="height: 150px; overflow-y: auto; border: 1px solid #ccc; padding: 8px;"></div> <input id="messageInput" type="text" placeholder="输入消息..." /> <button id="sendBtn">发送</button> <script> // 绑定发送按钮 document.getElementById('sendBtn').addEventListener('click', sendMessage); document.getElementById('messageInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); </script>运行方式与音视频通话完全一致:先启动信令服务器,再打开两个标签页。第一个标签页点击「开始通话」后,数据通道会随 Offer 一起协商建立;第二个标签页通过ondatachannel回调自动接收。两端即可在聊天框中实时收发文本消息,消息直接通过 P2P 加密通道传输,不经过服务器。
数据通道还支持二进制数据,可用于传输文件。只需将dataChannel.send()的参数改为Blob或ArrayBuffer,并在onmessage中接收后通过URL.createObjectURL生成下载链接即可。需要注意的是,数据通道默认使用可靠有序模式(类似 TCP),也可通过createDataChannel('name', { ordered: false, maxRetransmits: 0 })配置为不可靠模式(类似 UDP),适合对实时性要求高、可容忍少量丢失的场景。
这个最小示例已经跑通了 WebRTC 通话的核心链路,在此基础上可以按需扩展:
- 房间管理:当前信令服务器只支持两个客户端,可扩展为多房间、多用户模型,支持群组会议。
- 屏幕共享:通过
getDisplayMedia获取屏幕流,作为额外的媒体轨道加入连接。 - 数据通道:使用
RTCDataChannel传输文本、文件等非媒体数据,实现聊天或协作功能。 - ICE 服务器配置:在生产环境中配置 STUN/TURN 服务器,提升 NAT 穿透成功率和弱网下的连接稳定性。
- 信令可靠性:为信令消息增加确认和重传机制,或改用成熟的信令服务(如 Socket.IO、PeerJS)以提升可靠性。
媒体通路负责音视频数据的采集、编码、传输和解码渲染。采集端通过 getUserMedia 获取媒体流,经过编码器压缩后,通过 SRTP 加密并封装为 RTP 包发送。接收端收到 RTP 包后解密、解码并渲染到页面。
媒体传输的关键环节包括:
- RTP/RTCP:RTP 承载媒体数据,RTCP 负责统计丢包、延迟和带宽等信息。
- SRTP:对 RTP 数据进行加密,保证媒体内容的安全。
- Jitter Buffer:接收端通过抖动缓冲消除网络抖动,保证播放流畅。
- 拥塞控制:根据网络状况动态调整码率,避免网络拥塞导致卡顿。
6. 网络穿透通路
现实网络中,多数终端位于 NAT 或防火墙之后,无法直接建立 P2P 连接。WebRTC 通过 ICE 框架解决这一问题,ICE 会依次尝试主机候选、反射候选和中继候选。
网络穿透涉及以下关键组件:
| 组件 | 作用 |
|---|---|
| STUN | 帮助终端发现自己的公网地址和端口,用于建立 P2P 连接。 |
| TURN | 当 P2P 无法打通时,通过中继服务器转发媒体数据,保证连接可用。 |
| ICE | 统一管理候选地址的收集、排序和连通性检测,选择最优传输路径。 |
7. 实践中的关键问题
在实际项目中,RTC 与 WebRTC 通路的落地还需要关注以下问题:
- 信令服务器选型:需要支持高并发、低延迟的消息转发,并保证消息有序可靠。
- TURN 服务器部署:在跨运营商、跨地域场景下,合理部署 TURN 节点以降低中继延迟。
- 带宽与码率控制:根据网络状况动态调整分辨率、帧率和码率,保证通话质量。
- 多端互通:Web 端、iOS、Android 端需要统一信令协议和媒体协商策略。
- 弱网优化:通过丢包重传、前向纠错(FEC)和冗余编码提升弱网下的通话体验。
8. 总结
RTC 与 WebRTC 通路涉及信令、媒体、网络穿透三个核心环节。理解整体架构和协议栈,是构建稳定实时通信应用的基础。在实际开发中,需要结合业务场景合理设计信令服务、部署 STUN/TURN 节点,并针对弱网环境做好码率控制和容错策略。