4、RTCPeerConnection基础:建立点对点连接、ICE框架与STUN/TURN服务器、SDP协议简介

好,咱们今天聊点硬核的。RTCPeerConnection,这是WebRTC里最核心的一个对象。说白了,它就是帮你完成“两个浏览器之间直接传数据”的那个关键角色。我刚开始接触WebRTC的时候,觉得这东西挺玄乎的——两个浏览器怎么就能直接连上呢?中间又没有服务器转发。后来深入一看,其实底层逻辑并不复杂,就是几个协议在配合工作。

今天这一章,我会带你拆解三个核心概念:点对点连接的建立流程ICE框架与STUN/TURN服务器、以及SDP协议。嗯,这三个东西你搞懂了,RTCPeerConnection就算入门了。

4.1 点对点连接:到底是怎么“点对点”的?

先问一个问题:两个浏览器在不同的局域网里,怎么找到对方?

你可能会说,用IP地址啊。但现实是,大多数设备都在NAT(网络地址转换)后面,你的电脑拿到的其实是一个内网IP,比如192.168.1.x。外网根本访问不到你。那怎么办?

这就是RTCPeerConnection要解决的核心问题。它不会直接去连,而是通过一个“信令服务器”先交换一些元数据,然后双方各自尝试找到一条可达的路径。

我个人习惯把整个过程分成三步:

  1. 交换SDP:双方先告诉对方“我支持什么编解码器、我的网络情况如何”。
  2. 收集ICE候选:双方各自尝试找出自己能用的IP和端口。
  3. 连通性检查:双方互相尝试连接,直到找到一条能用的路。

你看,其实不复杂。但每一步背后都有协议在支撑。

核心要点:RTCPeerConnection不负责传输信令数据(比如SDP),它只负责媒体数据的传输。信令的交换需要你自己实现,或者用第三方库。

4.2 ICE框架:让连接“穿墙而过”

ICE,全称Interactive Connectivity Establishment。翻译过来就是“交互式连接建立”。名字挺唬人,其实它的任务就一个:找到一条能让两个端点通信的路径

我在项目中遇到过一个问题:两个用户都在公司内网,按理说应该能直连,但就是连不上。后来发现是防火墙把某些端口封了。ICE这时候就派上用场了——它会尝试多种路径,直到找到一条通的。

ICE的候选类型有三种:

候选类型 说明 典型场景
host 本机IP地址(内网) 同一局域网内直连
srflx 经过STUN服务器反射后的公网IP 普通家庭网络
relay 通过TURN服务器中转的地址 对称NAT或防火墙严格的环境

ICE会按照优先级从高到低尝试:先试host,不行再试srflx,最后才用relay。为什么relay放最后?因为TURN服务器要消耗带宽和计算资源,说白了就是费钱。

避坑指南:我曾经在生产环境中只配了STUN服务器,结果有大约5%的用户连不上。后来加了TURN服务器,问题就解决了。所以,如果你的应用面向公网用户,建议至少配一个TURN服务器兜底。

4.3 STUN与TURN服务器:一个指路,一个带路

STUN和TURN,这两个名字容易搞混。我简单给你区分一下:

  • STUN(Session Traversal Utilities for NAT):帮你查出你的公网IP和端口。它不转发媒体数据,只做“地址查询”。
  • TURN(Traversal Using Relays around NAT):当直连走不通时,它作为中继服务器,帮你转发媒体数据。它既做地址查询,也做数据转发。

你想想看,STUN就像你问路人“我现在在哪儿?”,TURN就像你打不到车,路人直接开车送你过去。两者定位完全不同。

在实际开发中,STUN服务器通常是免费的(比如Google提供的 stun:stun.l.google.com:19302),而TURN服务器一般需要自己搭建或购买云服务。

注意:TURN服务器的带宽成本不低。如果你们的应用是音视频通话,每小时可能消耗几百MB甚至上GB的流量。建议只在必要时启用TURN,或者限制中继模式的使用。

4.4 SDP协议简介:连接双方的“自我介绍”

SDP,全称Session Description Protocol。它不是一个传输协议,而是一个描述格式。用来告诉对方:我支持哪些音视频编解码器、我的网络地址是什么、我想要用什么方式传输。

我刚开始看SDP的时候,觉得它像一堆乱码。后来发现,其实结构很清晰。一个典型的SDP长这样:

v=0
o=- 1234567890 1234567890 IN IP4 192.168.1.10
s=-
t=0 0
m=audio 49170 RTP/AVP 96
a=rtpmap:96 OPUS/48000/2
a=fmtp:96 minptime=10; useinbandfec=1
c=IN IP4 192.168.1.10
a=candidate:1 1 UDP 2122252543 192.168.1.10 49170 typ host

别怕,我带你拆一下:

  • v=0:SDP版本号,目前就是0。
  • o=:会话的发起者信息,包括ID、版本、地址类型等。
  • m=:媒体描述,比如音频(audio)或视频(video),以及端口和传输协议。
  • a=:属性行,用来描述编解码器、参数等。
  • c=:连接信息,通常是IP地址。
  • a=candidate:ICE候选地址,就是前面说的host/srflx/relay。

在WebRTC中,SDP的交换是通过信令服务器完成的。一端调用 createOffer() 生成SDP,另一端调用 createAnswer() 生成应答SDP。然后双方通过 setLocalDescription()setRemoteDescription() 把对方的SDP设置进去。

关键点:SDP里包含了ICE候选信息。但ICE候选的收集是异步的,所以通常会在 onicecandidate 事件中逐步发送候选,而不是一次性发完。

4.5 整体流程:一张图看懂

说了这么多,咱们用一张图把整个流程串起来。下面这张SVG图展示了从信令交换到连接建立的全过程:

RTCPeerConnection 连接建立流程 发起方 (Offerer) 信令服务器 应答方 (Answerer) ① createOffer() 生成SDP Offer SDP Offer 转发Offer ② createAnswer() 生成SDP Answer SDP Answer 转发Answer ③ 收集ICE候选 (host/srflx/relay) ③ 收集ICE候选 (host/srflx/relay) ICE候选 ICE候选 ⑤ 连通性检查 (STUN Binding Request/Response) ✅ P2P 连接建立成功

这张图里,我特意把信令服务器放在中间。你注意看,SDP和ICE候选的交换都经过信令服务器,但最终的媒体流是直接点对点的(图中紫色箭头)。这就是WebRTC的精髓:信令走服务器,媒体走P2P

4.6 代码示例:一个最简单的RTCPeerConnection

光说不练假把式。咱们写一个最简版的RTCPeerConnection创建流程。假设你已经有了信令服务器(比如WebSocket),这里只展示核心逻辑:

// 发起方
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'turn:your-turn-server.com:3478', username: 'user', credential: 'pass' }
  ]
});

// 创建Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

// 通过信令服务器发送offer
signalServer.send({ type: 'offer', sdp: offer.sdp });

// 监听ICE候选
pc.onicecandidate = (event) => {
  if (event.candidate) {
    signalServer.send({ type: 'candidate', candidate: event.candidate });
  }
};

// 应答方
const pc2 = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'turn:your-turn-server.com:3478', username: 'user', credential: 'pass' }
  ]
});

// 收到Offer后创建Answer
signalServer.onmessage = async (msg) => {
  if (msg.type === 'offer') {
    await pc2.setRemoteDescription(new RTCSessionDescription(msg));
    const answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    signalServer.send({ type: 'answer', sdp: answer.sdp });
  }
};

这段代码虽然简单,但已经涵盖了核心流程。你注意看 iceServers 的配置,我习惯把STUN和TURN都配上去,这样兼容性最好。

小技巧:在实际项目中,ICE候选的发送不要用 onicecandidate 一个个发,可以用 onicecandidate 收集完所有候选后一次性发送,减少信令交互次数。不过要注意,收集过程可能比较慢,需要设置超时机制。

4.7 常见问题与避坑

最后,分享几个我踩过的坑:

  • SDP交换顺序搞反了:必须先 setLocalDescription,再发送SDP。否则ICE候选可能不完整。
  • 忘记处理ICE连接状态变化:建议监听 oniceconnectionstatechange,当状态变成 disconnectedfailed 时做重连处理。
  • TURN服务器配置错误:我曾经把TURN的端口写成了3478(默认),但实际用的是5349(TLS)。结果一直连不上,排查了半天。

嗯,这一章的内容就到这里。RTCPeerConnection的基础你已经有数了。下一章我们会深入SDP的细节,看看里面那些参数到底是什么意思。


公众号:蓝海资料掘金营,微信deep3321