3、传输协议选型(中):WebRTC协议栈详解、ICE/STUN/TURN机制、SDP协商过程
上一节我们把传输层协议捋了一遍。这一节,咱们深入WebRTC的核心。
WebRTC这东西,说白了就是浏览器原生支持的点对点实时通信能力。不用装插件,不用额外SDK,打开浏览器就能推流拉流。我最早接触它是在2015年,那时候还在做Flash直播,后来发现WebRTC的延迟能做到几百毫秒,而Flash还在几秒徘徊。嗯,从那以后我就开始全面转向WebRTC了。
3.1 WebRTC协议栈全景
很多人以为WebRTC只是一个API,其实它背后是一整套协议栈。我习惯把它分成三层来看:
- 应用层:JavaScript API,包括getUserMedia、RTCPeerConnection、RTCDataChannel
- 会话层:SDP(Session Description Protocol)协商,负责交换媒体能力
- 传输层:ICE/STUN/TURN + SRTP/SCTP,负责打通网络和加密传输
你想想看,两个浏览器在不同的NAT后面,怎么找到对方?怎么知道对方支持什么编码?怎么保证数据不被窃听?这就是WebRTC协议栈要解决的问题。
核心要点:WebRTC不是单一协议,而是一个协议簇。它把P2P连接、NAT穿透、加密传输、自适应码率这些能力打包在一起,对外暴露简洁的API。
下面这张图是我自己整理的WebRTC协议栈分层结构,每次给团队新人讲都会拿出来用:
3.2 ICE机制:如何找到对方
ICE(Interactive Connectivity Establishment)是WebRTC最核心的机制之一。它的任务很简单:帮两个端点找到一条可以通信的路径。
但现实很残酷。大多数设备都在NAT后面,有的还是对称NAT。我记得有一次做跨国直播项目,北京和纽约的两个节点怎么都连不上,折腾了两天才发现是对称NAT导致STUN失效,最后靠TURN中继才搞定。
3.2.1 ICE的工作流程
ICE的流程可以概括为三步:
- 收集候选地址(Candidate):每个端点收集自己可能的通信地址,包括本地地址、STUN反射地址、TURN中继地址
- 交换候选地址:通过信令通道(通常是WebSocket)交换双方的候选地址列表
- 连通性检查:双方对每一对候选地址进行P2P连接测试,找到最优路径
我的经验:ICE的连通性检查是有优先级的。本地地址 > STUN反射地址 > TURN中继地址。因为本地延迟最低,TURN延迟最高。但实际项目中,很多场景直接走TURN,因为P2P打不通。这时候就要权衡延迟和服务器成本了。
3.2.2 候选地址类型
| 类型 | 优先级 | 来源 | 适用场景 |
|---|---|---|---|
| host | 最高 | 本机网卡IP | 局域网内通信 |
| srflx | 中 | STUN服务器反射 | 大部分NAT场景 |
| relay | 最低 | TURN服务器分配 | 对称NAT、防火墙限制 |
为什么会这样排序?因为host延迟最低,但只能在局域网用。srflx能穿透大部分NAT,但遇到对称NAT就失效。relay最稳定,但延迟高、带宽成本也高。
3.3 STUN与TURN:穿透与中继
STUN和TURN经常被放在一起说,但它们的角色完全不同。
3.3.1 STUN:帮我看看我的公网地址
STUN(Session Traversal Utilities for NAT)做的事情很简单:客户端向STUN服务器发请求,服务器返回客户端的公网IP和端口。这样客户端就知道自己在公网上的"身份"了。
但STUN有个致命弱点——它搞不定对称NAT。对称NAT会给每个目标分配不同的端口映射,STUN拿到的反射地址只对STUN服务器有效,换一个目标就不行了。
注意:STUN不是信令服务器,它只做地址发现。不要把两者搞混了。我见过有人把STUN当信令用,结果连不上还找不到原因。
3.3.2 TURN:最后的保底方案
TURN(Traversal Using Relays around NAT)是STUN的升级版。当P2P打不通时,TURN服务器作为中继转发数据。说白了,就是所有数据都经过服务器走一圈。
代价是什么?延迟增加、带宽成本翻倍。我做过一个估算:如果全部走TURN,服务器带宽成本是P2P模式的3-5倍。所以我的建议是:能用P2P尽量用P2P,TURN只做保底。
下面这张图展示了ICE的完整决策流程:
3.4 SDP协商:你们能聊什么
SDP(Session Description Protocol)不是传输协议,它是描述媒体会话的文本格式。WebRTC用它来交换双方的媒体能力——你支持什么编码?分辨率多少?要不要加密?
我习惯把SDP理解成"相亲简历"。双方各自写好自己的简历,通过信令通道交换,然后看看能不能"在一起"。
3.4.1 SDP的结构
一个典型的SDP长这样:
v=0
o=- 123456 2 IN IP4 192.168.1.100
s=-
t=0 0
m=audio 5004 RTP/AVP 96 97 98
a=rtpmap:96 OPUS/48000/2
a=rtpmap:97 PCMU/8000/1
a=rtpmap:98 telephone-event/8000
a=fmtp:96 minptime=10; useinbandfec=1
m=video 5006 RTP/AVP 100 101
a=rtpmap:100 H264/90000
a=rtpmap:101 VP8/90000
a=fmtp:100 profile-level-id=42e01f; packetization-mode=1
关键字段解释:
- m=:媒体行,声明媒体类型(audio/video)、端口、传输协议、payload类型
- a=rtpmap:把payload类型映射到具体的编解码器
- a=fmtp:编解码器的具体参数,比如H264的profile-level
3.4.2 Offer/Answer模型
SDP协商遵循Offer/Answer模型:
- Offer方:发起方生成SDP Offer,包含自己支持的媒体能力
- Answer方:接收方解析Offer,匹配双方都支持的能力,生成SDP Answer
- 最终:双方根据协商结果创建媒体通道
避坑指南:我曾经遇到过一个坑——SDP中的ICE候选地址和实际网络环境不匹配。原因是STUN服务器返回的反射地址过期了,但SDP里还用的是旧地址。解决办法是在ICE连接状态变化时重新生成SDP,而不是复用旧的。
3.5 实战:ICE连接状态机
ICE连接不是一蹴而就的,它有一个完整的状态机。理解这个状态机,对排查问题很有帮助。
| 状态 | 含义 | 常见原因 |
|---|---|---|
| new | 初始状态,还没开始收集候选地址 | 刚创建RTCPeerConnection |
| checking | 正在收集候选地址并进行连通性检查 | 正常流程 |
| connected | 至少找到一条可用路径 | P2P或TURN连接成功 |
| completed | 所有候选地址检查完毕 | 正常结束 |
| failed | 所有候选地址都连不上 | 防火墙、NAT限制、TURN服务器不可用 |
| disconnected | 曾经连上过,但现在断了 | 网络波动、NAT映射超时 |
| closed | 连接被主动关闭 | 调用close() |
我在项目中遇到过最典型的问题是:ICE状态一直在checking和disconnected之间来回跳。后来发现是STUN服务器响应太慢,导致ICE超时重试。解决办法是部署多个STUN服务器做冗余,同时缩短超时时间。
3.6 总结
WebRTC的传输层设计,说白了就是一套"先试P2P,不行就中继"的机制。ICE负责找路,STUN帮你发现公网地址,TURN做最后的保底,SDP负责双方"握手"确定能聊什么。
理解这些机制,你就能回答一个关键问题:为什么WebRTC能做到低延迟?因为它优先走P2P路径,数据不经过服务器中转,网络跳数最少。只有在P2P走不通时才退而求其次走TURN。