WebRTC 实现浏览器 P2P 实时通信
WebRTC 是什么?为什么需要它?
WebRTC (Web Real-Time Communication) 是一项开源技术,允许浏览器和移动应用之间直接进行无插件的实时音视频通话和数据共享。它的核心价值在于“点对点”(P2P),这意味着流媒体和数据无需经过中间服务器中转,从而大幅降低延迟,提升私密性。
在你构建直播连麦、在线会议、实时文件传输或多人游戏时,WebRTC 就是那项让浏览器直接对话的关键技术。
建立 P2P 连接的核心流程
WebRTC 建立点对点连接并不是一步到位的魔法,而是一个精心编排的“协商-连接”过程。整体流程可概括为:
- 媒体捕获: 获取本地摄像头/麦克风或屏幕画面。
- 信令交换: 交换会话控制信息(发起/结束通话)和网络配置(SDP 与 ICE 候选项)。
- 连接建立: 穿透 NAT 与防火墙,建立点对点信道。
- 媒体与数据传输: 实时发送音视频,或利用数据通道发送任意数据。
第一步:捕获本地媒体流
首先,从用户的摄像头和麦克风获取 MediaStream 对象,这是整个实时通信的起点。
const localVideo = document.getElementById('localVideo');
async function startLocalStream() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 将本地流绑定到视频元素,让用户看到自己
localVideo.srcObject = stream;
return stream;
} catch (error) {
console.error('无法访问媒体设备:', error);
}
}
navigator.mediaDevices.getUserMedia(constraints)会触发浏览器的权限弹窗。- 进阶用法:可对视频分辨率、帧率、设备 ID 等进行精确约束,如
video: { width: 1280, height: 720 }。
第二步:建立 RTCPeerConnection
RTCPeerConnection 是 WebRTC API 的核心对象,负责建立、维护并关闭点对点连接。它同时管理着音视频轨道的发送与接收。
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' } // 免费的 STUN 服务器
]
};
const peerConnection = new RTCPeerConnection(configuration);
iceServers至关重要:里面会配置 STUN/TURN 服务器地址,用于 NAT 穿透。- 创建
RTCPeerConnection后,需要立即将上一步获取的本地流中的每一个轨道添加进去。
// 假设 webrtcStream 是第一步获取到的 stream
webrtcStream.getTracks().forEach(track => {
peerConnection.addTrack(track, webrtcStream);
});
- 添加轨道的同时,会触发
onnegotiationneeded事件,这是发起信令交换的起点。
第三步:理解信令与“Offer/Answer”协商
点对点连接需要双方知道如何连接到对方,这就需要交换网络配置信息,这个过程叫“信令”。WebRTC 没有规定信令方式,你可以使用 WebSocket、Socket.IO 或任何双向通信机制。
协商过程(以一端发起为例):
- 发起方创建 Offer: 调用
createOffer()生成一个 SDP(Session Description Protocol)描述,包含媒体格式、编解码器、传输协议等信息。 - 设置本地描述: 将生成的 Offer 通过
setLocalDescription()保存到自己的连接对象。 - 发送 Offer: 通过信令服务器将 Offer SDP 字符串发给另一端。
- 接收方响应: 收到 Offer 后,通过
setRemoteDescription()保存远端描述,然后调用createAnswer()生成 Answer SDP。 - 设置本地答案并回传: 用
setLocalDescription()保存 Answer,并将 Answer 通过信令服务器发回给发起方。 - 发起方完成协商: 收到 Answer 后,通过
setRemoteDescription()设置远端描述,双方媒体能力协商完毕。
发起方代码示例:
async function createOffer() {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// 通过信令通道将 peerConnection.localDescription 发送给对方
signaling.send({ type: 'offer', sdp: peerConnection.localDescription });
}
接收方监听与应答:
signaling.on('offer', async (offerSdp) => {
await peerConnection.setRemoteDescription(new RTCSessionDescription(offerSdp));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
signaling.send({ type: 'answer', sdp: peerConnection.localDescription });
});
signaling.on('answer', async (answerSdp) => {
await peerConnection.setRemoteDescription(new RTCSessionDescription(answerSdp));
});
第四步:ICE 候选与 NAT 穿透
仅有 SDP 无法真正连接,因为设备大多位于路由器之后,需要找到一条可用的网络路径。ICE(Interactive Connectivity Establishment) 框架负责这件事。
RTCPeerConnection会持续收集本地可能的 IP 和端口信息,包装成“ICE 候选项”。- 每当发现一个新的候选项,会触发
onicecandidate事件。你需要将这些候选项通过信令服务器发给对端。 - 对端收到后,调用
addIceCandidate()将其添加到自己的连接对象中。
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
signaling.send({ type: 'candidate', candidate: event.candidate });
} else {
// 所有候选项已收集完毕
console.log('ICE 收集完成');
}
};
// 接收端监听候选项
signaling.on('candidate', async (candidate) => {
try {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
} catch (e) {
console.error('添加 ICE 候选失败:', e);
}
});
常见 ICE 服务器类型:
- STUN 服务器: 帮助获取公网 IP 地址,纯穿透用,流量小,免费方案即可。
- TURN 服务器: 在严格的 NAT 策略或对称 NAT 下,中转所有数据流,压力大,需要自建或使用云服务。配置时在
iceServers中同时填写urls、username和credential。
第五步:接收远端视频并展示
当连接协商成功,对端的媒体轨会自动抵达。你需要监听 ontrack 事件来渲染远端的视频画面。
const remoteVideo = document.getElementById('remoteVideo');
peerConnection.ontrack = (event) => {
// 一个连接可能有多条流,通常取第一条
if (remoteVideo.srcObject !== event.streams[0]) {
remoteVideo.srcObject = event.streams[0];
}
};
至此,基础的音视频实时通信便已建立。
第六步:使用数据通道(DataChannel)发送任意数据
除了音视频,WebRTC 还支持直接在点对点之间传输低延迟的二进制或文本数据,非常适合游戏状态同步、聊天消息、文件分享等场景。
数据通道可以双向创建,通常由发起方调用 createDataChannel():
const dataChannel = peerConnection.createDataChannel('chat');
dataChannel.onopen = () => console.log('数据通道打开');
dataChannel.onmessage = (event) => {
// event.data 是发送方用 send() 传递的数据
document.getElementById('messages').innerText += event.data + '\n';
};
// 发送消息
document.getElementById('sendBtn').onclick = () => {
const msg = document.getElementById('msgInput').value;
dataChannel.send(msg);
};
接收端需要监听 ondatachannel 事件来获取通道对象:
peerConnection.ondatachannel = (event) => {
const receiveChannel = event.channel;
receiveChannel.onmessage = (e) => {
document.getElementById('messages').innerText += e.data + '\n';
};
};
一个完整的信令交互时序(用于巩固理解)
为了方便排查问题,可对照以下简化时序:
- 用户 A:获取本地流 -> 创建
RTCPeerConnection-> 添加轨 -> 创建 Offer -> 发送 Offer。 - 信令服务器:转发 Offer 给用户 B。
- 用户 B:获取本地流 -> 创建连接 -> 添加轨 -> 接收 Offer 并设置远端描述 -> 创建 Answer -> 发送 Answer。
- 信令服务器:转发 Answer 给用户 A。
- A 与 B 并行:收集 ICE 候选并互相发送,对方收到后立即添加候选项。
- 网络路径打通后,双方
ontrack触发,视频出现;onopen触发,数据通道可用。
常见问题与调试技巧
- 无法看到远端画面: 优先检查 ICE 连接状态。通过
peerConnection.oniceconnectionstatechange打印状态变化,若一直为checking或disconnected,往往是 IP/端口无法连通,需要确保 TURN 服务器配置正确。 - 只有声音没有视频: 检查信令中传递的 SDP 是否包含视频编码,并确认
addTrack时机正确(在setLocalDescription之前)。 - 跨浏览器兼容性: 主流浏览器均支持 WebRTC,但建议使用
adapter.js垫片来消除不同实现的细微差异。 - 部署环境限制: 在本地 localhost 测试时,浏览器可能绕过 ICE 直接连接;一旦部署到 HTTPS 服务器(WebRTC 强制要求安全上下文),就必须配置有效的 STUN/TURN 服务器。
进阶方向
学完基础流程后,你可以继续探索:
- 多人群组方案: Mesh(每两个人建连接)、SFU(选择性转发单元)、MCU(多点控制单元)。
- 屏幕共享: 使用
getDisplayMedia替换getUserMedia。 - 音视频处理: 利用 Web Audio API 和 Canvas 对实时流进行降噪、美颜等处理。
- 服务质量控制: 通过
getStats()API 监控丢包、延迟、码率,动态调整视频质量。
这篇教程带你走完了浏览器 P2P 通信最核心的路径:获取媒体、建立连接、交换信令、穿透 NAT 以及数据的双向传递。亲手把这几个模块用代码串联起来之后,你便真正掌握了 WebRTC 的精髓。