# rtp 实时传输协议、RTCP 实时传输控制协议
rtp 是实时传输协议,定义于 rfc3550,是为了需要实时优先级的数据交换提供实时连接。
RTP(实时传输控制协议)负责传媒体,但媒体传的怎么样,就需要 RTCP 来进行反馈和协调,RTCP 专门用户传控制信息、不传音视频数据本身。
# webrtc 网页实时传输协议
webrtc 是网页实时传输协议,是用于客户端点对点建立连接、而不需要服务器中转数据的一种通信方式的协议 webrtc 实际上使用的是 SRTP 协议(安全实时传输协议),确保了交换数据的安全性和身份验证。
# 优势
- 跨平台:只要设备的浏览器上支持 webrtc,就可以使用同一套前端代码
- 直连成功后可以减少服务器中转和带宽的成本
- 低延迟、高吞吐:设备直连时比中转更加直接,但实际效果取决于网络环境
- 安全性:传输过程默认加密
- 支持 NAT 穿透:ICE 会尝试多种连接路径;直连失败时仍可能需要 TURN 服务器中继
# 浏览器集成
浏览器中已经集成了这样一套完整的 webrtc 的 api,直接使用即可,不过即使这样国内外一些大厂在使用 webrtc 时,常常也不是拿来即用的,而是在 webrtc 的基础之上抽取想要的部分,然后自定义了部分功能。
# webapis
建立 webrtc 连接需要先搭建一个信令服务器,用于连接双方找到彼此。
# RTCPeerConnection
RTCPeerConnection 是核心 api,负责在两个浏览器之间建立、维护、关闭点对点连接,需要传入对应配置,它处理的事情有:
- SDP 协商
- fICE 候选收集与交换
- fNAT 穿透
- DTLS/SRTPj 加密
- 音视频编解码协商
- RTP/RTCPs 媒体传输
- 带宽估计、连接状态管理
如果仅用于音视频的话,双方的 connection 实例中监听 ontrack 就能收到音视频流了,但是如果还要传输自定义数据的话,就需要使用 RTCDataChannel 了。
# RTCDataChannel
RTCDataChannel 的作用是在已建立的 RTCPeerConnection 之上,创建一个双向数据通道,用于传输文本、二进制、文件、游戏指令等任意的数据
- RTCDataChannel 类似于 websocket,但它是点对点的
- 可以进行配置:可靠 / 不可靠、有序 / 无序
- 适合聊天、文件传输、实时游戏状态同步、元数据交换 等
# 信令交换与 NAT 穿透
信令服务器用于进行 “信息交换和协调会话 “,NAT 穿透负责 “打洞直连、中继兜底 “,两者配合,WebRTC 才能即找到对方又真正直连。
# 领域知识
# 信令
信令是双方建立连接所需要的信息,由于通信过程中需要双方双向通信、且服务端要能主动推送,所以通常使用 websocket 实现。
# 信令服务器
# 为什么使用 websocket 协议
在连接建立和连接状态变化阶段,信令服务器需要服务端频繁推送消息给客户端,而客户端也得主动发送消息给信令服务器,这种全双工的通信协议非常适合使用 websocket 去实现。
# 信令为什么频繁交换
信令交换不是一次性的,WebRTC 使用 SDP 来描述:“我支持什么编解码、什么媒体、什么网络地址 “。连接流程是:
sequenceDiagram | |
autonumber | |
participant A as Peer A(主叫) | |
participant S as 信令服务器 | |
participant B as Peer B(被叫) | |
participant ICE as STUN/TURN 服务器 | |
Note over A: 创建 RTCPeerConnection<br/>添加媒体轨道或 DataChannel | |
A->>A: createOffer() | |
A->>A: setLocalDescription(offer) | |
A->>S: 发送 SDP Offer | |
S->>B: 转发 SDP Offer | |
B->>B: setRemoteDescription(offer) | |
B->>B: createAnswer() | |
B->>B: setLocalDescription(answer) | |
B->>S: 发送 SDP Answer | |
S->>A: 转发 SDP Answer | |
A->>A: setRemoteDescription(answer) | |
par Peer A 收集 ICE Candidate | |
A->>ICE: STUN Binding / TURN Allocate | |
ICE-->>A: 返回候选地址 | |
A->>S: 发送 ICE Candidate | |
S->>B: 转发 ICE Candidate | |
and Peer B 收集 ICE Candidate | |
B->>ICE: STUN Binding / TURN Allocate | |
ICE-->>B: 返回候选地址 | |
B->>S: 发送 ICE Candidate | |
S->>A: 转发 ICE Candidate | |
end | |
Note over A,B: ICE 连通性检查,选择最优候选对<br/>可能是直连,也可能经 TURN 中继 | |
A->>B: STUN Binding Request | |
B-->>A: STUN Binding Success | |
B->>A: STUN Binding Request | |
A-->>B: STUN Binding Success | |
Note over A,B: 在选定的候选对上执行 DTLS 握手 | |
A->>B: DTLS ClientHello | |
B-->>A: DTLS ServerHello / Certificate / KeyExchange | |
A->>B: DTLS Finished | |
B-->>A: DTLS Finished | |
alt 媒体通信 | |
Note over A,B: 从 DTLS 导出 SRTP 密钥 | |
A-->>B: SRTP/RTP 媒体流 | |
B-->>A: SRTP/RTP 媒体流 | |
else DataChannel 通信 | |
Note over A,B: 在 DTLS 上建立 SCTP 关联 | |
A-->>B: SCTP/DataChannel 数据 | |
B-->>A: SCTP/DataChannel 数据 | |
end | |
Note over A,B: ICE/DTLS 完成,连接状态 connected |
这种连接过程至少是一来一回的方式,但实际过程中还可能会发生:
- 新增 / 删除音视频轨道
- 屏幕共享的开始 / 结束
- 修改音视频编解码、带宽、方向等
- 网络变化导致 ICE 重新选举
- 连接失败时重新协商
这每一次的处理过程都需要重新 offer/answer、都需要信令服务器重新转发各种信令消息,所以信令不是一次性的、而是需要频繁交换。
# SDP 会话描述协议
SDP 不传媒体或者数据,只描述媒体 / 数据怎么传,是描述格式 / 协商载体,不是媒体传输协议。
# NAT 网络地址转换
NAT 网络地址转换的作用是:在 IP 数据包经过网络设备(路由器或防火墙)时,修改源 IP、目的 IP、源端口、目的端口的机制。
# 举例
- NAT 最常见的作用是让多个内网设备共享一个公网 IP 上网:
- 当内网设备访问外部服务器时,路由器会使用 NAT 将 IP 包的源 IP 地址修改为自己公网 IP 的地址
- 外部服务器看到的源 IP 地址是公网 ip 的,而不是内网 ip 地址
- 返回数据到达时,路由器再根据 ip 映射表将目标 ip 地址修改为对应内网 ip
# 作用
- 缓解 IPv4 地址枯竭
公网 IPv4 地址有限,NAT 让成百上千台内网设备共享少量公网 IP。 - 内网地址复用
不同内网可以重复使用 192.168.x.x、10.x.x.x 等私有地址,互不冲突。 - 隐藏内网结构
外网只能看到 NAT 设备的公网地址,看不到内网设备的真实 IP 和拓扑。 - 提供一定安全隔离
默认情况下,外网不能主动访问内网设备,除非 NAT 上配置了端口映射。
但 NAT 不等于防火墙,它只是附带了一定的隔离效果。 - 简化网络管理
企业、家庭网络不需要给每台设备分配公网 IP,出口统一做转换即可。
# 缺点
NAT 网络地址转换让多个内网设备共享了一个公网 IP,同时带来了后果:
- 外网不能主动访问内网设备,除非 NAT 上有明确的端口映射。
# 内网穿透与 NAT 穿透
NAT 穿透是内网穿透的一种,内网穿透的范围更广:
- 内网穿透的重点是让外部能够访问到内网服务,或者让内网之间互通
- NAT 穿透的重点是让两个都在 NAT 后面的设备直接互通、优先打洞直连
# NAT 穿透的具体实现
NAT 穿透常见的技术实现有:STUN、UDP 打洞、TCP 打洞、TURN、ICE 等。
# STUN(NAT 会话穿越工具)
STUN 作用是:帮助客户端发现自己的公网地址,尝试直连。因为 NAT 会将内网地址转换成公网地址,所以客户端不知道自己在公网上的地址是什么,而 STUN 的作用是将 NAT 分配的公网映射告诉自己。
# 工作方式
- 客户端向 STUN 服务器发送一个 Binding 请求。
- STUN 服务器收到后,把看到的源 IP 和端口返回给客户端。
- 客户端得到自己的公网映射地址。
- 这个地址作为 ICE 候选之一,参与连通性检查。
sequenceDiagram | |
participant C as 客户端<br/>内网 192.168.1.10:54321 | |
participant N as NAT | |
participant S as STUN 服务器<br/>公网 | |
C->>N: 1. 发送 STUN Binding Request<br/>源地址:192.168.1.10:54321 | |
N->>S: 2. 转发请求<br/>NAT 将源地址改为公网地址 xxxx:80304 | |
Note over S: 3. STUN 服务器看到源地址<br/>xxxx:80304 | |
S->>N: 4. 返回 Binding Response<br/>包含 XOR-MAPPED-ADDRESS = xxxx:80304 | |
N->>C: 5. 转发响应 | |
Note over C: 6. 客户端得到自己的公网映射地址<br/>作为 srflx 候选 |
# UDP 打洞
当使用 STUN(NAT 会话穿越工具)获取到在公网地址的映射时,就需要发送 STUN 请求去进行 UDP 打洞、在各自的 NAT 上打开临时通道、建立一条连接,流程如下:
sequenceDiagram | |
participant A as Peer A | |
participant NATA as NAT A | |
participant NATB as NAT B | |
participant B as Peer B | |
Note over A,B: 前置:通过信令交换 SDP 和 ICE 候选 | |
Note over A,B: 双方各自获得 host / srflx / relay 候选 | |
Note over A,B: 假设双方选择 srflx 候选进行直连尝试 | |
A->>NATA: 发送 STUN 请求到 B 的公网地址 | |
NATA->>NATB: 出站包,NAT A 创建映射 | |
NATB->>B: 转发到 B | |
B->>NATB: 发送 STUN 请求到 A 的公网地址 | |
NATB->>NATA: 出站包,NAT B 创建映射 | |
NATA->>A: 转发到 A | |
B-->>A: STUN 响应 | |
A-->>B: STUN 响应 | |
Note over A,B: 双方 NAT 都允许对方入站,打洞成功 | |
A->>B: DTLS 握手 | |
B->>A: DTLS 握手 | |
Note over A,B: 密钥协商完成 | |
A->>B: SRTP / SRTCP 媒体流 | |
B->>A: SRTP / SRTCP 媒体流 | |
Note over A,B: 直连建立,媒体不经过服务器 |
常见的打洞失败的原因:
- 对称 NAT
- 端口受限 NAT
- 运营商 NAt
- 防火墙阻止 UDP、阻止入站包
- IP 版本不匹配
- 浏览器权限或者系统权限不足 等
UDP 打洞依赖于 NAT 在分配和映射公网端口时,对端口映射的维护(一段时间没流量就会删除映射)。
打洞的过程是双向的,双方都会向对方的候选地址发送 STUN Binding 请求,因为内网主动向外发送发包,NAT 才会建立映射、之后来自那个目标地址的回包,NAT 才允许进入。
# TURN(使用中继穿越 NAT)
TURN 的作用是:当双方无法直连时,RURN 服务器作为中继,转发双方的音视频和数据,它充当一个 “中间人 “,所有媒体先发送给 TURN,再由 TURN 转发给对方。
# 工作方式
- 客户端向 TURN 服务器申请分配一个中继地址。
- TURN 服务器分配一个公网地址和端口,比如 198.51.100.7:60000。
- 客户端把这个地址作为 relay 候选,通过信令发给对方。
- 对方把媒体发到这个地址,TURN 服务器再转发给客户端。
- 反向同理。
sequenceDiagram | |
participant A as 客户端 A | |
participant T as TURN 服务器 | |
participant B as 客户端 B | |
Note over A,T: 阶段一:A 向 TURN 申请中继地址 | |
A->>T: 1. Allocate 请求(认证) | |
T-->>A: 2. 分配成功,返回 A 的中继地址<br/>Relay-A = 198.51.100.7:60000 | |
Note over B,T: 阶段二:B 向 TURN 申请中继地址 | |
B->>T: 3. Allocate 请求(认证) | |
T-->>B: 4. 分配成功,返回 B 的中继地址<br/>Relay-B = 198.51.100.7:60001 | |
Note over A,B: 阶段三:通过信令交换中继地址 | |
A->>B: 5. 信令:A 告诉 B 自己的中继地址 Relay-A | |
B->>A: 6. 信令:B 告诉 A 自己的中继地址 Relay-B | |
Note over A,T: 阶段四:A 向 B 发送数据(媒体或 DataChannel) | |
A->>T: 7. CreatePermission 请求<br/>允许 B 的地址 | |
T-->>A: 8. 权限创建成功 | |
A->>T: 9. Send Indication / ChannelData<br/>目标:Relay-B,负载:媒体数据 | |
T->>B: 10. 通过 B 与 TURN 的连接转发数据 | |
Note over B,T: 阶段五:B 向 A 发送数据 | |
B->>T: 11. CreatePermission 请求<br/>允许 A 的地址 | |
T-->>B: 12. 权限创建成功 | |
B->>T: 13. Send Indication / ChannelData<br/>目标:Relay-A,负载:媒体数据 | |
T->>A: 14. 通过 A 与 TURN 的连接转发数据 | |
Note over A,B: 后续媒体流持续通过 TURN 中继 |
# ICE 候选
连接 P2P 连接的条件是:双方需要知道对方、以及自己公网上的可达地址;ICE 候选机制就是一套 “收集所有可能地址、交换、逐一验证、选最优路径 “的机制。
ICE 候选的过程
- NAT(网络地址转换)穿透
- 防火墙穿透
- 中继兜底
- 多路径选择
- 适应网络变化
- 为 DTLS/SRTP 提供传输通道
# 什么是 ICE 候选
一个 ICE 候选就是一个 “可能可用的传输地址 “,通常包含:
- IP 地址
- 端口
- 传输协议
- 候选类型(host 本机网卡、srflx STUN 服务器的反射、prflx 连通性检查中发现、relay TURN 服务器分配)
- 优先级
- 基础地址 等
在 ICE 候选过程中,因为网络路径不稳定,所以 A 和 B 之间各自收集到多个候选:
- 本地局域网地址
- 公网反射地址
- TURN 中继地址
这些地址两两组合,会形成很多个 “候选对 “,ICE 候选要找出最优的网络联通方式
flowchart TD | |
A[开始收集] --> B[host 候选<br/>本机网卡地址] | |
A --> C[向 STUN 请求] | |
C --> D[srflx 候选<br/>NAT 公网映射] | |
A --> E[向 TURN 申请] | |
E --> F[relay 候选<br/>TURN 中继地址] | |
B & D & F --> G[通过信令交换] | |
G --> H[形成候选对] | |
H --> I[连通性检查] | |
I --> J{是否成功?} | |
J -- 是 --> K[提名并选中] | |
J -- 否 --> L[尝试下一候选对] | |
L --> I | |
K --> M[媒体传输] |
# 直连遇到的问题
假设 A 和 B 想直连。最理想是 A 知道 B 的 IP: 端口,B 知道 A 的 IP: 端口,然后直接发 UDP。
但现实是:
- A 可能在 NAT 后面,本地地址是 192.168.1.2:5000,公网映射可能是 203.0.113.5:62000。
- B 也在另一个 NAT 后面。
- 双方都不知道自己的公网映射,更不知道对方的。
- 防火墙可能只允许某些方向、某些端口。
- 设备可能有 Wi-Fi、有线、4G 多个网卡,有多个本地地址。
- 对称 NAT 下,映射还会随目标变化。
- 所以只交换一个地址几乎必然失败。ICE 候选机制就是为了解决 “到底用哪个地址才能连通” 的问题。
# DTLS/SRTP 加密
DTLS(数据报传输层安全)可以看成是跑在 UDP 协议上的 TLS 安全层协议;SRTP(安全实时传输协议)是 RTP 的加密版本