---
title: WebRTC
url: https://doc.liz6.com/networking/10-real-time-communication/01-WebRTC
locale: zh
area: networking
tags:
- networking
- 实时通信
date: 2026-06-30
modified: 2026-07-16
description: 浏览器之间直接传输音视频——WebRTC 用 SDP 协商媒体参数、ICE 打洞穿越 NAT、DTLS-SRTP 加密传输。P2P 的"不经过服务器"是性能优势,也是调试噩梦。
---

# WebRTC

> 浏览器之间直接传输音视频——WebRTC 用 SDP 协商媒体参数、ICE 打洞穿越 NAT、DTLS-SRTP 加密传输。P2P 的"不经过服务器"是性能优势,也是调试噩梦。

## 概述

WebRTC（2011）是浏览器内置的实时通信标准，不需要插件就能实现视频通话、屏幕共享和 P2P 数据传输。它组合了多个协议：SDP 做媒体协商、ICE 做 NAT 穿透、DTLS-SRTP 做媒体加密、SCTP 做数据通道。WebRTC 的连接建立是全异步的——candidate 逐个到达、逐个验证——Trickle ICE 大幅减少了连接时间。

## 架构栈

<svg viewBox="0 0 720 370" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="WebRTC 架构栈:从浏览器 API 到底层传输协议">
  <defs><marker id="wrtcArchArrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="370" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">WebRTC 架构栈:从浏览器 API 到底层传输</text>

  <rect x="130" y="46" width="460" height="42" rx="6" fill="#4f46e5"/>
  <text x="360" y="71" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">Browser JS API(RTCPeerConnection, RTCDataChannel)</text>
  <line x1="360" y1="88" x2="360" y2="106" stroke="#475569" stroke-width="1.7" marker-end="url(#wrtcArchArrow)"/>

  <rect x="130" y="106" width="460" height="40" rx="6" fill="#4f46e5"/>
  <text x="360" y="130" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">WebRTC C++ stack(libwebrtc, Google)</text>
  <line x1="360" y1="146" x2="360" y2="164" stroke="#475569" stroke-width="1.7" marker-end="url(#wrtcArchArrow)"/>

  <rect x="20" y="164" width="130" height="60" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="85" y="186" text-anchor="middle" font-size="11.5" font-weight="700" fill="#3730a3">SDP</text>
  <text x="85" y="202" text-anchor="middle" font-size="10" fill="#4f46e5">媒体协商</text>
  <text x="85" y="215" text-anchor="middle" font-size="10" fill="#4f46e5">(offer/answer)</text>

  <rect x="158" y="164" width="130" height="60" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="223" y="186" text-anchor="middle" font-size="11.5" font-weight="700" fill="#115e59">ICE</text>
  <text x="223" y="202" text-anchor="middle" font-size="10" fill="#0f766e">NAT 穿透</text>
  <text x="223" y="215" text-anchor="middle" font-size="10" fill="#0f766e">(STUN + TURN)</text>

  <rect x="296" y="164" width="130" height="60" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="361" y="186" text-anchor="middle" font-size="11.5" font-weight="700" fill="#3730a3">DTLS</text>
  <text x="361" y="202" text-anchor="middle" font-size="10" fill="#4f46e5">密钥交换 →</text>
  <text x="361" y="215" text-anchor="middle" font-size="10" fill="#4f46e5">派生 SRTP 密钥</text>

  <rect x="434" y="164" width="130" height="60" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="499" y="186" text-anchor="middle" font-size="11.5" font-weight="700" fill="#115e59">SRTP</text>
  <text x="499" y="202" text-anchor="middle" font-size="10" fill="#0f766e">加密的</text>
  <text x="499" y="215" text-anchor="middle" font-size="10" fill="#0f766e">RTP 流</text>

  <rect x="572" y="164" width="128" height="60" rx="6" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="636" y="186" text-anchor="middle" font-size="11.5" font-weight="700" fill="#334155">SCTP</text>
  <text x="636" y="202" text-anchor="middle" font-size="10" fill="#475569">数据通道</text>

  <line x1="360" y1="224" x2="360" y2="246" stroke="#475569" stroke-width="1.7" marker-end="url(#wrtcArchArrow)"/>

  <rect x="210" y="246" width="135" height="40" rx="6" fill="#0d9488"/>
  <text x="277" y="270" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">UDP(首选)</text>
  <rect x="355" y="246" width="135" height="40" rx="6" fill="#f97316"/>
  <text x="422" y="270" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">TCP(fallback)</text>

  <rect x="60" y="304" width="600" height="50" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="325" font-size="12.5" fill="#3730a3">五个协议共同构成"传输层"——SDP/ICE 负责建连协商,DTLS/SRTP 管媒体安全,</text>
  <text x="76" y="343" font-size="12.5" fill="#3730a3">SCTP 管数据通道;全部跑在 UDP 之上,打不通才退到 TCP。</text>
</svg>

## 信令:被故意留白的一环

WebRTC **不规定信令**——两端怎么交换 SDP 和 ICE candidate，标准完全不管，由你用 WebSocket/HTTP/任意通道自己实现。这是有意为之：媒体路径要 P2P，但"两个还没连上的人怎么先碰头"本就需要一个双方都能到达的第三方。

<svg viewBox="0 0 720 300" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="信令服务器牵线协商,媒体与数据走 P2P 直连">
  <defs>
    <marker id="wrtcSigArrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
    <marker id="wrtcSigBidir" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto-start-reverse"><path d="M0,0 L8,3 L0,6 Z" fill="#0d9488"/></marker>
  </defs>
  <rect width="720" height="300" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">信令负责牵线,媒体/数据走 P2P 直连</text>

  <rect x="250" y="55" width="220" height="58" rx="8" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="360" y="76" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">信令服务器(自建,任意协议)</text>
  <text x="360" y="97" text-anchor="middle" font-size="10.5" fill="#475569">交换 SDP offer/answer + ICE candidate</text>

  <rect x="40" y="150" width="140" height="54" rx="8" fill="#4f46e5"/>
  <text x="110" y="182" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">Peer A</text>
  <rect x="540" y="150" width="140" height="54" rx="8" fill="#4f46e5"/>
  <text x="610" y="182" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">Peer B</text>

  <line x1="115" y1="150" x2="262" y2="115" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcSigArrow)"/>
  <line x1="458" y1="115" x2="605" y2="150" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcSigArrow)"/>

  <line x1="180" y1="177" x2="540" y2="177" stroke="#0d9488" stroke-width="1.8" marker-start="url(#wrtcSigBidir)" marker-end="url(#wrtcSigBidir)"/>
  <text x="360" y="197" text-anchor="middle" font-size="11" font-weight="700" fill="#0f766e">(协商完成后)SRTP 媒体 / SCTP 数据 —— P2P 直连</text>

  <rect x="40" y="225" width="640" height="52" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="56" y="246" font-size="12.5" fill="#115e59">信令只负责"两端怎么找到对方",协议不限定(WebSocket/HTTP 皆可、标准不管);</text>
  <text x="56" y="264" font-size="12.5" fill="#115e59">一旦 offer/answer 和 candidate 交换完成,媒体和数据就完全绕开服务器直连。</text>
</svg>

先有信令把 offer/answer 递过去，ICE 才有 candidate 可连——这就是 WebRTC 的"先有鸡"。

## SDP 协商

```
Caller → Callee (Offer):
  v=0
  o=- 1234567890 2 IN IP4 192.168.1.100
  s=-
  t=0 0
  a=group:BUNDLE audio video data     ← 同一传输链路上多路复用
  m=audio 49170 UDP/TLS/RTP/SAVPF 111 103
  c=IN IP4 192.168.1.100
  a=rtpmap:111 opus/48000/2
  a=rtcp-mux                           ← RTP 和 RTCP 同一个端口
  a=ice-ufrag:8sdf7g
  a=ice-pwd:asd88fg
  a=fingerprint:sha-256 AB:CD:...      ← DTLS certificate hash

Callee → Caller (Answer):
  m=audio 49180 UDP/TLS/RTP/SAVPF 111  ← 选定的端口 + codec
  ...

  → 双方协商完成 → ICE 开始连接检查
```

## ICE

每个 peer 收集三种 candidate:
1. **host**: 本地 IP:port
2. **srflx** (server reflexive): STUN server 返回的公网 IP:port
3. **relay** (relayed): TURN server 分配的中继地址

对每对 (local candidate + remote candidate) 做 connectivity check (STUN binding) → 选出最优可用 pair（优先级 host > srflx > relay，即"能直连就不走中继"）。

**STUN vs TURN 的分工**：STUN 只是"帮你问出自己的公网映射"，不转发媒体，几乎零成本；但当两端都在对称 NAT 后、打洞失败时，就必须退到 **TURN 中继**——所有媒体经它转发，带宽成本高，是部署里唯一绕不开的服务器成本。生产经验：约 10-20% 的连接最终落到 TURN。

**Trickle ICE**：不必等所有 candidate 收集完才发 offer。candidate 边发现边通过信令发给对端、边做连通性检查，把建连时间从"等最慢的 candidate"压缩到"第一个可用 pair 就绪"——这是 WebRTC 起连快的关键。

## DTLS-SRTP

DTLS handshake (类 TLS 1.2 over UDP) → 握手完成后，双方从 DTLS 派生 SRTP 密钥 (不额外交换):

<svg viewBox="0 0 720 400" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="DTLS 握手流程:从 ClientHello 到派生 SRTP 密钥">
  <defs><marker id="wrtcDtlsArrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="400" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">DTLS 握手:从 ClientHello 到派生 SRTP 密钥</text>

  <rect x="30" y="55" width="150" height="46" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="105" y="83" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">ClientHello</text>
  <line x1="180" y1="78" x2="200" y2="78" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcDtlsArrow)"/>

  <rect x="200" y="55" width="150" height="46" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="275" y="83" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">ServerHello</text>
  <line x1="350" y1="78" x2="370" y2="78" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcDtlsArrow)"/>

  <rect x="370" y="55" width="150" height="46" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="445" y="83" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">Certificate</text>
  <line x1="520" y1="78" x2="540" y2="78" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcDtlsArrow)"/>

  <rect x="540" y="55" width="150" height="46" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="615" y="76" text-anchor="middle" font-size="10.5" font-weight="700" fill="#3730a3">ServerKey-</text>
  <text x="615" y="90" text-anchor="middle" font-size="10.5" font-weight="700" fill="#3730a3">Exchange</text>

  <polyline points="615,101 615,116 190,116 190,131" fill="none" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcDtlsArrow)"/>

  <rect x="115" y="131" width="150" height="46" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="190" y="152" text-anchor="middle" font-size="10.5" font-weight="700" fill="#3730a3">ClientKey-</text>
  <text x="190" y="166" text-anchor="middle" font-size="10.5" font-weight="700" fill="#3730a3">Exchange</text>
  <line x1="265" y1="154" x2="285" y2="154" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcDtlsArrow)"/>

  <rect x="285" y="131" width="150" height="46" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="152" text-anchor="middle" font-size="10.5" font-weight="700" fill="#3730a3">ChangeCipher-</text>
  <text x="360" y="166" text-anchor="middle" font-size="10.5" font-weight="700" fill="#3730a3">Spec</text>
  <line x1="435" y1="154" x2="455" y2="154" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcDtlsArrow)"/>

  <rect x="455" y="131" width="150" height="46" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="530" y="159" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">Finished</text>

  <polyline points="530,177 530,192 360,192 360,207" fill="none" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcDtlsArrow)"/>

  <rect x="160" y="207" width="400" height="44" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="360" y="234" text-anchor="middle" font-size="12" font-weight="700" fill="#115e59">DTLS-SRTP 扩展:协商使用哪种 SRTP 密码套件</text>

  <line x1="360" y1="251" x2="360" y2="266" stroke="#475569" stroke-width="1.6" marker-end="url(#wrtcDtlsArrow)"/>

  <rect x="110" y="266" width="500" height="48" rx="6" fill="#dcfce7" stroke="#4ade80"/>
  <text x="360" y="287" text-anchor="middle" font-size="12" font-weight="700" fill="#166534">双方从 DTLS master_secret 派生 SRTP 密钥</text>
  <text x="360" y="304" text-anchor="middle" font-size="11" fill="#15803d">SRTP keys = SRTP_KDF(DTLS master_secret)</text>

  <rect x="40" y="330" width="640" height="54" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="56" y="351" font-size="12.5" fill="#3730a3">SRTP 密钥不再单独传输——直接由本次 DTLS 握手产生的 master secret 派生,</text>
  <text x="56" y="369" font-size="12.5" fill="#3730a3">天然与这次握手绑定,省掉一轮额外的密钥交换。</text>
</svg>

## Data Channel

覆盖在 DTLS 上的 SCTP (Stream Control Transmission Protocol):

```
与 TCP 相比:
  - 多 stream (无 HOL blocking)
  - 保留消息边界
  - 可配置: 可靠 (reliable) / 部分可靠 (timed reliability)

适合: 文件传输, 游戏状态, 聊天
```

## 拥塞控制与 Simulcast

实时媒体不能像 TCP 那样靠丢包+重传——一帧的截止时间只有十几毫秒，等重传必然错过。WebRTC 用 **GCC (Google Congestion Control)**：基于 RTP 包到达的**延迟梯度**预判拥塞（队列在涨 → 早降码率），而非等到真丢包，发送方据此动态调整编码码率。

**Simulcast / SVC** 解决多人会议的异构带宽：同一路摄像头**同时编出多档**（如 180p/360p/720p），由中间服务器按每个接收端的网络分别下发合适的一档——这正是下面 SFU 能"按人选档"的前提。

## SFU / MCU / Mesh — 多人拓扑

WebRTC 本是 P2P，但多人时 P2P 不可扩展，必须引入服务器。三种拓扑（与[流媒体协议](/networking/10-real-time-communication/02-streaming-protocols.md)里的分发权衡同源）：

<svg viewBox="0 0 720 355" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="多人拓扑三选一:Mesh、SFU、MCU 的取舍对比">
  <rect width="720" height="355" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">多人拓扑三选一:Mesh / SFU / MCU</text>

  <rect x="40" y="55" width="90" height="60" rx="6" fill="#94a3b8"/>
  <text x="85" y="90" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">Mesh</text>
  <rect x="145" y="55" width="535" height="60" rx="6" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="160" y="80" font-size="11.5" fill="#334155">N 人两两直连 → 每人上行 N-1 路</text>
  <text x="160" y="100" font-size="11.5" font-weight="700" fill="#475569">仅 3-4 人可用,无服务器</text>

  <rect x="40" y="127" width="90" height="64" rx="6" fill="#0d9488"/>
  <text x="85" y="163" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">SFU</text>
  <rect x="145" y="127" width="535" height="64" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="160" y="150" font-size="11" fill="#0f766e">选择性转发:服务器只转发不解码,每人上行 1 路、按需下发(配合 simulcast)</text>
  <text x="160" y="172" font-size="11.5" font-weight="700" fill="#115e59">→ 主流方案,可扩展,服务器 CPU 轻(不转码)</text>

  <rect x="40" y="203" width="90" height="60" rx="6" fill="#f97316"/>
  <text x="85" y="238" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">MCU</text>
  <rect x="145" y="203" width="535" height="60" rx="6" fill="#ffedd5" stroke="#fdba74"/>
  <text x="160" y="228" font-size="11.5" fill="#9a3412">混流:服务器解码所有流 + 合成一路再发</text>
  <text x="160" y="248" font-size="11.5" font-weight="700" fill="#c2410c">→ 省接收端下行/算力,但服务器 CPU 极贵</text>

  <rect x="40" y="277" width="640" height="54" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="56" y="298" font-size="12.5" fill="#115e59">SFU 只转发不解码,是可扩展性与服务器成本的平衡点,多人会议的主流方案;</text>
  <text x="56" y="316" font-size="12.5" fill="#115e59">Mesh 仅适合超小规模,MCU 用服务器算力换客户端轻量但成本最高。</text>
</svg>

**为何 WebRTC 难像 HLS 那样铺 CDN**：每条连接有状态（ICE/DTLS/SRTP 上下文 + 实时拥塞反馈），不是可缓存的文件。规模化只能靠 SFU 级联，而非边缘缓存——这是"亚秒实时"换来的根本代价。

## 参考

- **webrtc.org**: webrtc.googlesource.com
- **工具**: `chrome://webrtc-internals`

*Keywords: WebRTC, signaling, SDP, ICE, Trickle ICE, STUN, TURN, DTLS-SRTP, SCTP, Data Channel, GCC, simulcast, SVC, SFU, MCU, mesh, Opus*
