RTCPeerConnection 连接建立:SDP 与 ICE
RTCPeerConnection 连接建立:SDP 与 ICE
RTCPeerConnection 是 WebRTC 的核心,负责建立点对点连接、传输音视频。这篇讲连接建立的完整流程:SDP 协商、ICE 候选、Offer/Answer 交换,一步步把连接建起来。
大家好,我是黒漂技术佬。
前面讲了音视频采集,有了本地媒体流,接下来要传给对方——这就靠 RTCPeerConnection。
RTCPeerConnection 是 WebRTC 最核心的 API,也是最复杂的。SDP 是什么?ICE 是什么?Offer 和 Answer 怎么交换?为什么需要信令服务器?
这篇把连接建立的完整流程讲清楚,从创建 PeerConnection 到打通 P2P 通道。
一、RTCPeerConnection 是什么?
作用
RTCPeerConnection 负责两端之间的连接建立和媒体数据传输:
- 媒体协商:两边商量用什么编解码器、分辨率、码率
- 网络穿透:NAT 打洞,找到能连通的网络路径
- 安全传输:加密传输音视频数据(SRTP)
- 质量控制:拥塞控制、丢包恢复、自适应码率
创建
constpc=newRTCPeerConnection({iceServers:[{urls:'stun:stun.l.google.com:19302'}// STUN 服务器]});参数里配置 ICE 服务器(STUN/TURN),用来 NAT 穿透。
二、SDP 是什么?
全称
Session Description Protocol,会话描述协议。
是什么
一段文本,描述媒体会话的所有信息:
- 有哪些媒体流(视频、音频)
- 用什么编解码器(VP8、H.264、Opus…)
- 媒体的传输地址(IP+端口,ICE 候选填进来)
- 加密信息
- 带宽信息
长什么样
v=0 o=- 1234567890 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE audio video m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 c=IN IP4 0.0.0.0 a=rtpmap:111 opus/48000/2 ... m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 c=IN IP4 0.0.0.0 a=rtpmap:100 VP8/90000 ...不用深究每一行,知道它是描述媒体配置的文本就行。
Offer / Answer 模型
连接建立用的是 Offer-Answer 模式:
- 发起方(A)生成 Offer SDP(我这边的媒体配置)
- A 把 Offer 发给接收方(B)
- B 收到 Offer,生成 Answer SDP(我这边的配置,和你协商好)
- B 把 Answer 发回给 A
- 两边都有了对方的 SDP,媒体协商完成
三、ICE 是什么?
全称
Interactive Connectivity Establishment,交互式连接建立。
作用
解决 NAT 穿透问题,找到两端之间能连通的网络路径。
ICE Candidate(ICE 候选)
每个候选是一个可能的连接地址:
candidate:1234567890 1 udp 2122260223 192.168.1.100 55555 typ host包含:IP、端口、协议、类型(host/srflx/relay)、优先级。
三种候选类型
| 类型 | 说明 | 优先级 |
|---|---|---|
| host | 本机内网地址 | 最高(延迟最低) |
| srflx | 服务器反射地址(STUN 拿到的公网地址) | 中 |
| relay | TURN 中继地址 | 最低(延迟高、走服务器) |
ICE 会按优先级从高到低尝试,找到第一个能连通的就用。
ICE 收集过程
创建 PeerConnection 后,开始收集候选:
- 先收集 host 候选(本机所有网卡的地址)
- 然后用 STUN 服务器获取 srflx 候选(公网映射地址)
- 最后收集 relay 候选(TURN 中继地址)
收集到一个就触发一次onicecandidate事件,通过信令发给对方。
四、完整的连接建立流程
前提
- A 和 B 都连上了信令服务器
- 可以互相发消息
步骤 1:创建 PeerConnection,添加媒体轨
// A 和 B 各自创建constpc=newRTCPeerConnection({iceServers:[...]});// 把本地媒体流加进去stream.getTracks().forEach(track=>{pc.addTrack(track,stream);});步骤 2:A 创建 Offer
// A 端:发起方constoffer=awaitpc.createOffer();awaitpc.setLocalDescription(offer);// 通过信令把 offer 发给 Bsignaling.send({type:'offer',sdp:offer.sdp});setLocalDescription 后,ICE 开始收集候选。
步骤 3:B 收到 Offer,创建 Answer
// B 端:接收方asyncfunctiononOffer(offer){awaitpc.setRemoteDescription(newRTCSessionDescription(offer));constanswer=awaitpc.createAnswer();awaitpc.setLocalDescription(answer);// 通过信令把 answer 发回给 Asignaling.send({type:'answer',sdp:answer.sdp});}步骤 4:A 收到 Answer
// A 端asyncfunctiononAnswer(answer){awaitpc.setRemoteDescription(newRTCSessionDescription(answer));}到这里,SDP 交换完成,媒体协商好了。
步骤 5:交换 ICE 候选
两边各自收集候选,收集到一个就发给对方:
// A 和 B 都要处理pc.onicecandidate=(event)=>{if(event.candidate){// 发给对方signaling.send({type:'ice-candidate',candidate:event.candidate});}};收到对方的候选,加进去:
functiononIceCandidate(candidate){pc.addIceCandidate(newRTCIceCandidate(candidate));}步骤 6:ICE 连接成功
两边候选交换完,开始配对尝试连接。成功后触发oniceconnectionstatechange:
pc.oniceconnectionstatechange=()=>{console.log('ICE 状态:',pc.iceConnectionState);// checking → connected/completed};状态变成connected或completed就说明连接通了。
步骤 7:接收远端媒体流
pc.ontrack=(event)=>{constremoteStream=event.streams[0];remoteVideo.srcObject=remoteStream;};对方的媒体流过来了,显示到页面上。
五、状态机
ICE 连接状态
| 状态 | 说明 |
|---|---|
| new | 初始状态 |
| checking | 正在检查候选配对 |
| connected | 找到可用连接(可能还在找更好的) |
| completed | 检查完了,找到了最好的连接 |
| failed | 所有候选都试过了,连不上 |
| disconnected | 连接断了(可能暂时的) |
| closed | 关闭了 |
信令状态
| 状态 | 说明 |
|---|---|
| stable | 稳定状态,没有待处理的 offer |
| have-local-offer | 本地设置了 offer,等对方 answer |
| have-remote-offer | 收到了对方的 offer,要生成 answer |
| have-local-pranswer | 本地发了暂定 answer |
| have-remote-pranswer | 收到了对方的暂定 answer |
| closed | 关闭 |
状态机很重要,顺序错了会报错。比如还没收到 offer 就 createAnswer 会失败。
六、信令服务器
为什么需要信令服务器?
WebRTC 标准只定义了媒体传输,不管怎么交换联系方式。SDP 和 ICE 候选怎么交换,自己想办法。
信令要做什么
- 房间管理:用户加入、离开房间
- 转发消息:Offer、Answer、ICE Candidate
- 成员管理:谁在房间里
怎么实现
随便什么都可以,只要能双向通信:
- WebSocket(最常用)
- Socket.IO
- HTTP 长轮询
- 甚至用信鸽(开玩笑的)
最简单的是 Node.js + Socket.IO,几十行代码就能搭一个。
信令消息类型
// 加入房间{type:'join',roomId:'123',userId:'userA'}// 转发 Offer{type:'offer',from:'userA',to:'userB',sdp:'...'}// 转发 Answer{type:'answer',from:'userB',to:'userA',sdp:'...'}// 转发 ICE 候选{type:'ice-candidate',from:'userA',to:'userB',candidate:{...}}// 离开{type:'leave',userId:'userA'}七、最简一对一通话代码框架
前端(发起方 A)
constpc=newRTCPeerConnection({iceServers:[...]});constlocalStream=awaitgetUserMedia({video:true,audio:true});// 添加本地轨道localStream.getTracks().forEach(t=>pc.addTrack(t,localStream));localVideo.srcObject=localStream;// 收到远端流pc.ontrack=e=>remoteVideo.srcObject=e.streams[0];// ICE 候选pc.onicecandidate=e=>{if(e.candidate)signaling.send({type:'candidate',candidate:e.candidate});};// 发起呼叫asyncfunctioncall(){constoffer=awaitpc.createOffer();awaitpc.setLocalDescription(offer);signaling.send({type:'offer',sdp:offer.sdp});}// 收到 Answersignaling.on('answer',async(data)=>{awaitpc.setRemoteDescription(newRTCSessionDescription(data));});// 收到 ICE 候选signaling.on('candidate',async(candidate)=>{awaitpc.addIceCandidate(newRTCIceCandidate(candidate));});前端(接收方 B)
constpc=newRTCPeerConnection({iceServers:[...]});constlocalStream=awaitgetUserMedia({video:true,audio:true});localStream.getTracks().forEach(t=>pc.addTrack(t,localStream));localVideo.srcObject=localStream;pc.ontrack=e=>remoteVideo.srcObject=e.streams[0];pc.onicecandidate=e=>{if(e.candidate)signaling.send({type:'candidate',candidate:e.candidate});};// 收到 Offersignaling.on('offer',async(data)=>{awaitpc.setRemoteDescription(newRTCSessionDescription(data));constanswer=awaitpc.createAnswer();awaitpc.setLocalDescription(answer);signaling.send({type:'answer',sdp:answer.sdp});});signaling.on('candidate',async(candidate)=>{awaitpc.addIceCandidate(newRTCIceCandidate(candidate));});八、常见问题排查
连接不上怎么办?
1. 看 ICE 状态
console.log(pc.iceConnectionState);- 一直 checking:候选没交换完,或者网络不通
- failed:所有候选都不行,可能需要 TURN 服务器
2. 检查 ICE 候选有没有交换
- 两边都收到对方的 candidate 了吗?
- 信令服务器有没有转发成功?
3. 检查 STUN/TURN 服务器配置
- 地址对不对?
- 端口有没有被防火墙挡?
- TURN 的用户名密码对不对?
4. 用 trickle-ice 工具测试
网上有在线的 WebRTC 测试工具,可以测 STUN/TURN 能不能用。
视频出不来怎么办?
1. 轨道加了吗?
有没有调用 addTrack?
2. SDP 交换成功了吗?
两边都 setRemoteDescription 了吗?
3. ICE 连接成功了吗?
iceConnectionState 是 connected 吗?
4. video 标签有没有 autoplay?
移动端需要用户手势触发播放。
九、常见坑
坑 1:setLocalDescription 之后才开始收 ICE 候选
没设本地描述前不会收集候选。顺序别搞反。
坑 2:信令顺序错了
Offer/Answer 有严格顺序,状态机不对会报错。比如收到两个 offer 没处理好。
坑 3:ICE 候选要及时转发
收集到一个就发一个(Trickle ICE),别等全部收集完再发,慢很多。
坑 4:没有 STUN 服务器内网可以通,外网不行
同局域网内不用 STUN 也能连,跨网就不行了。生产环境一定要配 STUN/TURN。
坑 5:TURN 服务器带宽成本高
走 TURN 中继的话,所有流量都过服务器,人多了带宽成本很高。尽量让 P2P 打通。
坑 6:移动端后台连接断了
App 切后台,WebSocket 和 WebRTC 连接可能断,要处理重连。
十、本篇小结
- RTCPeerConnection 是 WebRTC 核心,负责连接建立和媒体传输
- SDP:会话描述,协商媒体配置(编解码、加密等)
- ICE:NAT 穿透,找连通路径,有 host/srflx/relay 三种候选
- 连接流程:创建 PC → 加轨道 → Offer → Answer → 交换 ICE → 连接成功
- 信令服务器:交换 SDP 和 ICE 候选,自己实现,WebSocket 最常用
- 状态机很重要,Offer/Answer 顺序不能乱
- 连接不上先查 ICE 状态、候选交换、STUN/TURN 配置
下一篇深入讲NAT 穿透:STUN、TURN、ICE 的原理和配置。
我是黒漂技术佬。
