WebRTC 核心概念详解

本文系统梳理 WebRTC 的五大核心概念:信令、SDP、ICE/STUN/TURN、DTLS+SRTP、GCC 拥塞控制,并深入讲解 NAT 穿透原理和 GCC 算法细节。


缩写全称速查

缩写 全称 中文
SDP Session Description Protocol 会话描述协议
ICE Interactive Connectivity Establishment 交互式连接建立
STUN Session Traversal Utilities for NAT NAT 会话穿越工具
TURN Traversal Using Relays around NAT 通过中继穿越 NAT
DTLS Datagram Transport Layer Security 数据报传输层安全协议
SRTP Secure Real-time Transport Protocol 安全实时传输协议
RTP Real-time Transport Protocol 实时传输协议
RTCP RTP Control Protocol RTP 控制协议
GCC Google Congestion Control 谷歌拥塞控制算法
NAT Network Address Translation 网络地址转换
TLS Transport Layer Security 传输层安全协议
UDP User Datagram Protocol 用户数据报协议
TCP Transmission Control Protocol 传输控制协议
NACK Negative Acknowledgement 否定确认(丢包重传请求)
FEC Forward Error Correction 前向纠错
REMB Receiver Estimated Maximum Bitrate 接收端估计最大码率
BWE Bandwidth Estimation 带宽估计

一、信令(Signaling)

一句话:WebRTC 两端想通话,但双方都不知道对方在哪、能用什么格式——信令就是用来交换这些「元信息」的通道。

比喻:你要和朋友打电话,但没有存他号码。你们先互发微信消息「我的号码是 xxx,你的是多少?」——这个过程就是信令。

关键点

  • WebRTC 不规定信令用什么协议,随便你用 WebSocket、HTTP、甚至短信
  • 信令只传两类内容:SDP(我能做什么)和 ICE Candidate(我在哪)
  • 信令服务器本身不碰媒体流,只是个「中间人」转发消息
1
2
3
4
5
A ──── Offer SDP ────→ 信令服务器 ──→ B
A ←─── Answer SDP ─── 信令服务器 ←── B
A ──── ICE Candidate → 信令服务器 ──→ B
A ←─── ICE Candidate ─ 信令服务器 ←── B
// 信令结束后,A 和 B 直连,信令服务器退出历史舞台

二、SDP(Session Description Protocol)

一句话:我把自己的「能力清单」写给你看——我支持哪些编解码器、用什么分辨率、监听哪个端口。

比喻:相亲前双方交换简历,写清楚「我身高 180、会做饭、有房」,对方看完说「OK 我接受,我的情况是 xxx」。

真实的 SDP 长这样(截取片段):

1
2
3
4
5
6
7
8
9
10
11
12
v=0
o=- 1234567890 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104
a=rtpmap:111 opus/48000/2 ← 支持 Opus 编码,48kHz,双声道
a=rtpmap:103 ISAC/16000
m=video 9 UDP/TLS/RTP/SAVPF 96 97
a=rtpmap:96 VP8/90000 ← 支持 VP8 视频编码
a=rtpmap:97 H264/90000 ← 也支持 H264
a=fmtp:97 profile-level-id=42e01f ← H264 具体 Profile

Offer / Answer 模型

1
2
3
A 发 Offer:「我支持 VP8 和 H264,你选一个」
B 回 Answer:「好,我们用 H264」
// 之后双方都只用 H264 通信

三、ICE / STUN / TURN

核心问题:互联网上大多数设备在 NAT 后面(路由器后),没有公网 IP,两端怎么直连?

3.1 STUN 和 TURN 的本质区别

一句话区别

  • STUN:照镜子,告诉你自己的公网地址是什么,不转发流量
  • TURN:快递柜,P2P 打不通时帮你中转所有流量
STUN TURN
作用 告诉你「你家门牌号是 xxx」 帮你把信收下,再转交给朋友
流量过服务器吗 ❌ 不过,只是查询 ✅ 所有流量都过
费用 极低(只有查询请求) 高(要承载所有音视频流量)
使用时机 每次连接都用 仅 P2P 打不通时兜底

3.2 STUN——「你的公网地址是什么」

比喻:你站在镜子前不知道自己长啥样,STUN 服务器就是那面镜子,照完告诉你「你的公网 IP 是 1.2.3.4:5678」。

1
2
3
你(192.168.1.100:12345)
──→ STUN Server(公网)
← 「你的公网地址是 1.2.3.4:5678」

3.3 ICE——「尝试所有路径,找最优的一条」

ICE 会枚举三类 Candidate(候选地址),按优先级依次尝试:

类型 是什么 优先级
host 本机局域网 IP 最高(同局域网直连)
srflx 通过 STUN 拿到的公网 IP 中(NAT 穿透)
relay TURN 服务器中继地址 最低(兜底)
1
2
3
① 先试 host candidate(局域网直连)→ 最快
② 打不通 → 试 srflx candidate(STUN 拿到的公网地址,P2P 穿透)
③ 还打不通 → 用 relay candidate(TURN 中继)→ 兜底,必通

3.4 TURN——「P2P 打不通时的中继」

走 TURN 就是走公网服务器代理所有媒体流

1
2
3
4
5
6
7
P2P 直连(host/srflx):
手机A ──────────────────────────→ 手机B
直接传,不过任何服务器

走 TURN:
手机A ──→ TURN服务器(公网)──→ 手机B
所有音视频都经过这里

走 TURN 的影响:

  • 延迟增加:多绕一跳,增加 20-100ms(取决于 TURN 服务器离你多远)
  • 带宽成本高:服务器要承载完整双向媒体流,1路 720p 约 1-2Mbps
  • 内容仍然安全:媒体流是 SRTP 加密的,TURN 服务器只转发密文,看不到内容

生产环境策略:

  • 能 P2P 直连的绝不走 TURN(省成本)
  • TURN 服务器尽量部署在离用户近的地区(降延迟)
  • 监控 TURN 使用率,超过 30% 说明 P2P 打洞成功率偏低,需排查

它们不是路由器实现,代码里只需配置地址:

1
2
3
4
5
6
7
8
9
10
11
12
val iceServers = listOf(
// STUN(Google 免费公共服务器)
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
// TURN(需要账号密码,一般自己部署)
PeerConnection.IceServer.builder("turn:your-turn-server.com:3478")
.setUsername("user")
.setPassword("password")
.createIceServer()
)
val config = PeerConnection.RTCConfiguration(iceServers)
// ICE 打洞、STUN 查询、TURN 中继全部自动处理,无需额外代码
val peerConnection = factory.createPeerConnection(config, observer)

四、NAT 深挖

4.1 NAT 是什么,为什么存在

IPv4 地址只有 43 亿个,全球设备远不止这个数。NAT 的本质是「地址复用」——一个公网 IP 后面可以藏几百台设备,路由器负责做地址映射。

1
2
3
你的手机(192.168.1.100:12345)
↓ 路由器做 NAT
公网(1.2.3.4:54321)←── 外网只看到这个地址

4.2 NAT 的四种类型(从容易到难穿透排序)

① 完全锥型 NAT(Full Cone NAT)

1
2
规则:只要内网 A 发过包,任何人都能从 1.2.3.4:54321 发包进来找到 A
穿透难度:★☆☆☆ 最容易

② 地址限制锥型 NAT(Address Restricted Cone)

1
2
规则:只有 A 主动发过包的目标 IP,才能回包进来(端口不限)
穿透难度:★★☆☆

③ 端口限制锥型 NAT(Port Restricted Cone)

1
2
规则:只有 A 主动发过包的目标 IP + Port,才能回包进来
穿透难度:★★★☆

④ 对称型 NAT(Symmetric NAT)

1
2
3
4
规则:每次发往不同目标,路由器分配不同的公网端口
发给 B → 公网端口 54321
发给 C → 公网端口 54322(换了!)
穿透难度:★★★★ 最难

对称型 NAT 是 P2P 打洞最大的敌人——你通过 STUN 拿到的公网端口,只对 STUN 服务器有效,发给对端时路由器又换了个新端口,对端根本找不到你。

4.3 P2P 打洞原理(以端口限制锥型为例)

两端同时向对方「假装」发包,让路由器在 NAT 表里留下记录,之后对方的包就能进来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
A(192.168.1.100)  NAT-A(1.2.3.4)    NAT-B(5.6.7.8)  B(192.168.1.200)

① A 和 B 都把自己的 srflx Candidate 通过信令发给对方
A 知道:B 的公网地址是 5.6.7.8:9012
B 知道:A 的公网地址是 1.2.3.4:5678

② A 向 5.6.7.8:9012 发 STUN Binding Request
→ NAT-A 记录:内网 192.168.1.100:X ↔ 公网 1.2.3.4:5678(允许 5.6.7.8 回包)
→ 包到达 NAT-B,但 NAT-B 还没有 A 的记录,丢弃

③ B 同时向 1.2.3.4:5678 发 STUN Binding Request
→ NAT-B 记录:内网 192.168.1.200:Y ↔ 公网 5.6.7.8:9012(允许 1.2.3.4 回包)
→ 包到达 NAT-A,此时 NAT-A 已有记录,放行!A 收到了

④ A 回复 B,B 的 NAT 也放行
✅ 洞打通,双方直连

关键在于「同时」——两端必须几乎同时发包,这就是为什么信令服务器要协调双方同步开始。

4.4 对称型 NAT 为什么打不通

1
2
3
A 通过 STUN 拿到公网地址 1.2.3.4:5678(STUN 服务器看到的端口)
A 向 B 发包 → NAT-A 分配新端口 1.2.3.4:5679(换了!)
B 拿着 5678 去敲门,但 NAT-A 只认 5679 → 永远打不通

此时只能走 TURN 中继。现实中企业内网、运营商级 NAT(CGNAT)大量使用对称型 NAT,P2P 打洞成功率约 70-80%,剩下 20-30% 必须靠 TURN 兜底,所以生产环境不能省 TURN。


五、DTLS + SRTP

一句话:WebRTC 的所有媒体流强制加密,不能关,不存在明文传输。

为什么需要两层?

协议 作用
密钥交换 DTLS 基于 UDP 的 TLS,握手阶段交换加密密钥
媒体加密 SRTP 用 DTLS 协商出来的密钥,加密每一帧音视频

比喻

  • DTLS = 见面时互换暗语(密钥)
  • SRTP = 之后说话都用暗语加密

流程

1
2
3
1. ICE 建立 UDP 连接
2. DTLS 握手(交换证书 + 密钥)
3. 之后所有 RTP 包用协商好的密钥加密 → SRTP

为什么用 DTLS 不用 TLS?

因为 RTP 跑在 UDP 上,UDP 没有 TCP 的可靠传输,TLS 依赖 TCP,所以需要能在 UDP 上工作的 DTLS。


六、GCC 拥塞控制深挖

GCC 本质是两个并行的带宽估计器,结果取最小值:

1
2
3
              ┌── 基于延迟的估计器(Trendline Filter)──┐
输入:网络包 → → min() → 目标码率
└── 基于丢包的估计器(Loss-based)─────────┘

6.1 估计器一:基于延迟梯度(Trendline Filter)

核心思想:网络队列开始积压时,包的到达间隔会变长,比丢包更早暴露拥塞。

第一步:计算每个包组的延迟梯度

WebRTC 把包分组(每组若干个包),测量「发送间隔」和「到达间隔」的差值:

1
2
3
4
发送端:包组1在 t=0 发出,包组2在 t=20ms 发出  → 发送间隔 20ms
接收端:包组1在 t=50ms 到达,包组2在 t=75ms 到达 → 到达间隔 25ms

延迟梯度 δ = 到达间隔 - 发送间隔 = 25 - 20 = +5ms

δ > 0 说明队列在积压(拥塞信号);δ < 0 说明队列在消散(网络变好)。

第二步:Trendline 滤波,过滤抖动噪声

单个包的 δ 抖动很大(网络天然不稳定),用线性回归拟合最近 N 个 δ 的趋势线:

1
2
δ序列:+2, -1, +3, +4, +5, +6  → 趋势向上 → 拥塞信号
δ序列:+2, -1, +3, -2, +1, -1 → 趋势平稳 → 正常

第三步:状态机判断

1
2
3
NORMAL   ──(trend持续上升)──→  OVERUSE(拥塞)
OVERUSE ──(trend下降)──────→ UNDERUSE(空闲)
UNDERUSE ──(trend平稳)──────→ NORMAL

只有连续多个包组都显示 OVERUSE,才触发降码率(避免误判)。

第四步:AIMD 调整码率

1
2
3
UNDERUSE → 加性增加(Additive Increase):每秒增加 8% 码率
OVERUSE → 乘性减少(Multiplicative Decrease):乘以 0.85,立刻降 15%
NORMAL → 保持

为什么「加慢减快」:降码率是应急响应,必须快;加码率如果太猛会重新触发拥塞,所以慢慢探测上限在哪。

6.2 估计器二:基于丢包率(Loss-based)

接收端通过 RTCP 报告丢包情况(每秒一次),发送端根据丢包率调整:

1
2
3
4
丢包率 < 2%   → 可以提升码率(乘以 1.08)
丢包率 2~10% → 保持当前码率
丢包率 > 10% → 降码率(乘以 1 - 0.5 × 丢包率)
比如丢包 20% → 乘以 0.9 → 降 10%

6.3 两个估计器为什么都需要

延迟梯度 丢包率
响应速度 快(队列刚积压就感知到) 慢(必须等到包真的丢了)
准确性 受网络抖动影响,有误判 准确,丢了就是丢了
适用场景 提前预防拥塞 确认拥塞已发生

取两者最小值 = 保守策略,哪个说要降就降,不冒险。

6.4 完整 GCC 工作闭环

1
2
3
4
5
6
7
8
9
10
11
发送端                              接收端
│ │
│── 发 RTP 包(带时间戳)──────────→ │
│ │ 计算到达间隔,算 δ
│ │ 统计丢包率
│ ←── RTCP Transport-CC Feedback ── │ (每包都反馈到达时间)
│ ←── RTCP REMB(估计最大码率)───── │ (旧版,逐渐被 Transport-CC 替代)
│ │
│ 发送端本地运行 Trendline Filter
│ 计算目标码率 → 通知编码器调整码率
│ → 编码器输出更小/更大的帧

现代 WebRTC 主要用 Transport-CC(接收端把每个包的到达时间戳都反馈给发送端,发送端自己算延迟梯度),比老的 REMB 更精准。


七、串起来看整个流程

1
2
3
4
5
6
7
8
1. A 和 B 各自通过 STUN 拿到自己的公网地址
2. A 创建 Offer SDP(我支持 VP8/H264/Opus)发给信令服务器 → 转给 B
3. B 创建 Answer SDP(我选 H264)发回给 A
4. 双方交换 ICE Candidate,开始打洞
5. ICE 找到最优路径(优先直连,打不通走 TURN)
6. DTLS 握手,协商加密密钥
7. 开始传 SRTP 加密的音视频流
8. GCC 持续监测网络,动态调整发送码率

最后更新:2026-06