你有没有想过,当你和朋友视频通话时,视频流是怎么从你的摄像头传到对方屏幕的?在 WebRTC 出现之前,实时音视频通信依赖服务器中转,延迟高、带宽成本大。而 WebRTC(Web Real-Time Communication) 让浏览器可以直接建立点对点(P2P)连接,无需插件、无需服务器中转媒体流,彻底改变了 Web 实时通信的格局。
本文将从原理到代码,带你深入理解 WebRTC P2P 的完整技术体系。
一、什么是 WebRTC?
WebRTC 是由 Google 主导、W3C 和 IETF 标准化的开放技术规范,允许浏览器和移动应用通过简单的 JavaScript API 实现:
- 🎥 音视频通话(点对点传输,低延迟)
- 📂 任意数据传输(DataChannel,可传文件、消息)
- 🖥️ 屏幕共享
- 🎮 实时游戏数据同步
它的核心优势在于 P2P:两端直接通信,媒体数据不经过服务器,带宽消耗小、延迟极低(通常 < 100ms)。
二、WebRTC 的核心架构
┌─────────────────────────────────────────────┐
│ WebRTC 架构 │
│ │
│ Browser A Signaling Server │
│ ┌────────┐ SDP/ICE ┌──────────┐ │
│ │ Peer │◄────────────►│ Signal │ │
│ │ A │ │ Server │ │
│ └────┬───┘ └──────────┘ │
│ │ ▲ │
│ │ P2P Media/Data │ │
│ │◄──────────────────────►│ │
│ ┌────┴───┐ ┌──────────┐ │
│ │ Peer │◄────────────►│ Signal │ │
│ │ B │ SDP/ICE │ Server │ │
│ └────────┘ └──────────┘ │
│ Browser B │
└─────────────────────────────────────────────┘WebRTC 的连接建立分为两个阶段:
- 信令阶段:通过信令服务器交换 SDP 和 ICE 候选(服务器仅做"牵线搭桥")
- P2P 阶段:连接建立后,媒体流和数据直接在两端之间传输
三、关键协议栈
理解 WebRTC,必须了解其底层依赖的几个核心协议:
3.1 ICE(Interactive Connectivity Establishment)
ICE 解决的核心问题是:两台设备如何找到彼此并建立连接?
现实网络中,大多数设备都在 NAT(网络地址转换)后面,没有公网 IP。ICE 通过收集多种"候选地址"来尝试打洞:
| 候选类型 | 说明 |
|---|---|
| Host | 本机局域网 IP,直连最快 |
| Server Reflexive | 经 STUN 服务器获取的公网 IP |
| Relay | 经 TURN 服务器中继(最后备选) |
ICE 会对所有候选对进行连通性检测,选出最优路径。
3.2 STUN(Session Traversal Utilities for NAT)
STUN 服务器帮助客户端发现自己的公网 IP 和端口,成本极低(仅用于地址发现,不中转数据)。
Client → STUN Server: "我的公网 IP 是什么?"
STUN Server → Client: "你的公网 IP 是 203.0.113.5:54321"3.3 TURN(Traversal Using Relays around NAT)
当 P2P 直连失败(例如对称型 NAT),TURN 服务器作为中继转发所有数据。代价是带宽成本增加,但保证了连通性。
3.4 SDP(Session Description Protocol)
SDP 是一种文本格式,用于描述媒体会话的参数,包括:
- 支持的音视频编解码器(VP8、H.264、Opus 等)
- 媒体方向(发送/接收/双向)
- ICE 候选地址
- 加密参数(DTLS 证书指纹)
v=0
o=- 46117317 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=rtpmap:111 opus/48000/2
a=sendrecv四、P2P 连接建立流程(信令过程)
WebRTC 连接的建立遵循严格的"提议-应答"(Offer-Answer)模型:
Peer A Signaling Server Peer B
│ │ │
│── getUserMedia() ────────│ │
│ │ │
│── createOffer() ────────►│ │
│ (生成 SDP Offer) │── 转发 Offer ───────────►│
│ │ │── createAnswer()
│ │◄── 转发 Answer ───────────│ (生成 SDP Answer)
│◄── setRemoteDescription()│ │
│ │ │
│── 收集 ICE 候选 ──────────│── 转发 ICE candidates ──►│
│◄──────────────────────── │── 转发 ICE candidates ───│
│ │ │
│◄════════ P2P 连接建立,直接通信 ════════════════════►│六步建立连接:
- 获取本地媒体流(摄像头/麦克风)
- Peer A 创建
RTCPeerConnection,调用createOffer()生成 SDP - Peer A 通过信令服务器将 Offer 发送给 Peer B
- Peer B 收到 Offer,调用
createAnswer()生成应答 SDP - 双方交换 ICE 候选地址
- ICE 连通性检测完成,P2P 连接建立 ✅
五、核心 API 代码实战
5.1 获取本地媒体流
// 请求摄像头和麦克风权限
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
audio: true
});
// 将本地视频显示在页面上
const localVideo = document.getElementById('localVideo');
localVideo.srcObject = stream;5.2 创建 RTCPeerConnection
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }, // 免费 STUN 服务器
{
urls: 'turn:your-turn-server.com:3478', // TURN 中继服务器
username: 'user',
credential: 'password'
}
]
};
const peerConnection = new RTCPeerConnection(configuration);
// 将本地流的每个 track 添加到连接
stream.getTracks().forEach(track => {
peerConnection.addTrack(track, stream);
});
// 监听远端流
peerConnection.ontrack = (event) => {
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = event.streams[0];
};5.3 发起方:创建 Offer
// 发起方 (Caller)
async function createOffer() {
// 监听 ICE 候选,收集后通过信令服务器发送给对方
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
signalingServer.send({
type: 'ice-candidate',
candidate: event.candidate
});
}
};
// 创建并设置本地 SDP Offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// 通过信令服务器发送 Offer 给对方
signalingServer.send({
type: 'offer',
sdp: offer
});
}5.4 接收方:处理 Offer 并回复 Answer
// 接收方 (Callee)
async function handleOffer(offer) {
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
signalingServer.send({
type: 'ice-candidate',
candidate: event.candidate
});
}
};
// 设置远端描述(对方的 Offer)
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
// 创建并设置本地 Answer
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// 通过信令服务器发送 Answer 给对方
signalingServer.send({
type: 'answer',
sdp: answer
});
}5.5 添加 ICE 候选
// 收到对方的 ICE 候选时添加
async function handleIceCandidate(candidate) {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}5.6 DataChannel:传输任意数据
// 发起方创建 DataChannel
const dataChannel = peerConnection.createDataChannel('chat', {
ordered: true // 保证顺序
});
dataChannel.onopen = () => console.log('DataChannel 已开启');
dataChannel.onmessage = (e) => console.log('收到消息:', e.data);
// 发送数据
dataChannel.send('Hello, P2P!');
dataChannel.send(JSON.stringify({ type: 'file', name: 'photo.jpg' }));
// 接收方监听 DataChannel
peerConnection.ondatachannel = (event) => {
const receiveChannel = event.channel;
receiveChannel.onmessage = (e) => {
console.log('接收到:', e.data);
};
};六、完整信令服务器示例(Node.js + WebSocket)
// server.js - 信令服务器(仅负责转发,不处理媒体流)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const rooms = new Map(); // roomId -> [ws1, ws2]
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const data = JSON.parse(message);
switch (data.type) {
case 'join':
// 加入房间
if (!rooms.has(data.roomId)) {
rooms.set(data.roomId, []);
}
rooms.get(data.roomId).push(ws);
ws.roomId = data.roomId;
break;
case 'offer':
case 'answer':
case 'ice-candidate':
// 转发信令给房间内的其他人
const room = rooms.get(ws.roomId) || [];
room.forEach(peer => {
if (peer !== ws && peer.readyState === WebSocket.OPEN) {
peer.send(JSON.stringify(data));
}
});
break;
}
});
ws.on('close', () => {
// 清理房间
const room = rooms.get(ws.roomId);
if (room) {
const index = room.indexOf(ws);
if (index > -1) room.splice(index, 1);
}
});
});
console.log('信令服务器运行在 ws://localhost:8080');七、NAT 穿透:P2P 连接的最大挑战
现实中并非所有 P2P 连接都能成功建立,关键在于 NAT 类型:
NAT 类型 P2P 成功率
───────────────────────────
Full Cone ✅ 高(最容易穿透)
Restricted Cone ✅ 较高
Port Restricted ⚠️ 中等
Symmetric NAT ❌ 低(需要 TURN 中继)实际统计:约 85% 的连接可以通过 ICE 直接建立 P2P,剩余 15% 需要 TURN 中继。因此生产环境必须部署 TURN 服务器作为兜底。
推荐开源 TURN 服务器:coturn
# 安装 coturn
apt-get install coturn
# 基础配置 /etc/turnserver.conf
listening-port=3478
fingerprint
lt-cred-mech
user=webrtc:yourpassword
realm=yourdomain.com八、安全性
WebRTC 在设计上强制要求加密,所有传输默认安全:
| 层级 | 协议 | 说明 |
|---|---|---|
| 媒体传输 | SRTP | 音视频数据加密 |
| 数据传输 | DTLS | DataChannel 数据加密 |
| 密钥协商 | DTLS-SRTP | 密钥在 P2P 连接中直接协商 |
⚠️ 注意:信令服务器本身不在 WebRTC 规范内,需要开发者自行保证信令通道的安全(使用 WSS/HTTPS)。
九、常见应用场景
| 场景 | 技术点 |
|---|---|
| 视频会议 | getUserMedia + 多路 P2P 或 SFU 架构 |
| P2P 文件传输 | DataChannel + ArrayBuffer 分片 |
| 在线游戏 | DataChannel(unreliable 模式,低延迟) |
| 远程桌面 | getDisplayMedia + 视频轨道 |
| 实时字幕 | DataChannel 传输 STT 结果 |
多人会议架构选择
2人通话: A ←──P2P──→ B (纯 P2P,最优)
3-4人会议: A ←─ P2P ─→ B (Mesh,每人需多路连接)
↕ ↕
C ←─ P2P ─→ D
大规模会议: 所有人 ──→ SFU 服务器 ──→ 分发 (推荐,如 mediasoup)十、调试技巧
Chrome 提供了内置的 WebRTC 调试工具:
# 在浏览器地址栏打开
chrome://webrtc-internals
# 可以查看:
# - ICE 连接状态和候选地址
# - SDP 协商内容
# - 实时音视频统计(码率、丢包率、延迟)
# - DataChannel 状态// 代码中监听连接状态变化
peerConnection.onconnectionstatechange = () => {
console.log('连接状态:', peerConnection.connectionState);
// new → connecting → connected → disconnected → failed → closed
};
peerConnection.oniceconnectionstatechange = () => {
console.log('ICE 状态:', peerConnection.iceConnectionState);
};
// 获取实时统计数据
const stats = await peerConnection.getStats();
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
console.log('视频丢包率:', report.packetsLost / report.packetsReceived);
console.log('帧率:', report.framesPerSecond);
}
});总结
WebRTC P2P 技术的精妙之处在于:
- 🔗 信令与媒体分离:服务器只负责"牵线",数据直接在端之间流动
- 🧩 协议协同:ICE + STUN + TURN 三者配合,解决复杂网络环境下的连通性
- 🔐 安全内置:SRTP + DTLS 强制加密,无需额外配置
- 🌐 浏览器原生支持:无需插件,现代浏览器开箱即用
WebRTC 已经成为实时通信领域不可绕过的基础技术。无论是构建视频会议、文件传输还是实时游戏,深入理解其 P2P 连接原理都能让你在系统设计时做出更好的决策。