Karp 的技术博客

你有没有想过,当你和朋友视频通话时,视频流是怎么从你的摄像头传到对方屏幕的?在 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 的连接建立分为两个阶段:

  1. 信令阶段:通过信令服务器交换 SDP 和 ICE 候选(服务器仅做"牵线搭桥")
  2. 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 连接建立,直接通信 ════════════════════►│

六步建立连接:

  1. 获取本地媒体流(摄像头/麦克风)
  2. Peer A 创建 RTCPeerConnection,调用 createOffer() 生成 SDP
  3. Peer A 通过信令服务器将 Offer 发送给 Peer B
  4. Peer B 收到 Offer,调用 createAnswer() 生成应答 SDP
  5. 双方交换 ICE 候选地址
  6. 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音视频数据加密
数据传输DTLSDataChannel 数据加密
密钥协商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 连接原理都能让你在系统设计时做出更好的决策。


参考资料

版权属于:karp
作品采用:本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
更新于: 2026年03月10日 14:36
0

目录

来自 《WebRTC P2P:浏览器间实时通信的底层原理与实践》