WebRTC 之通信 — PeerConnections 与信令服务
本文为 RTC 系列第三篇,上一篇介绍了 RTCPeerConnection 在本地的连接过程,如果是和远程用户连接,SDP 和 ICE 的交换是需要经过网络传输的,在 RTC 中,负责信息交换的服务器称为信令服务器,它所做的事情就是在连接前传输信息。由于 SDP 和 ICE 都是简单的字符串数据,因此信令服务只要能够传递字符串数据即可,任何满足要求的服务器都可以作为信令服务器,我们可以根据需要搭建自己的信令服务,可以使用 websocket,也可以使用 http、sip 等其他协议。
server 实现:
因为使用 socket.io 处理双向通信比较容易,因此本文以 socket.io 为例展示一个简单的 P2P 信令服务的实现,使用其他协议也同理。
用户想和其他用户进行通话首先需要知道对方是谁,因此信令服务器需要有一套类似房间的机制,具体形式取决于业务场景,可能是聊天室,可能是广场,总之需要把网络上的用户连起来,我们这里实现一个简单的 user-list 系统:
io.on("connection", async (socket) => { const userList = [...(await io.allSockets())]; io.emit("user-list", userList); }); 复制代码
当用户 A 向 B 发起连接时,A 只要把之前发给 B 的内容发给服务器,并告诉服务器发送目标是 B 即可,同样 B 发消息也一样,只需要把消息发给服务器,指定目标给 A。对于服务器而言只需要将内容转发出去即可,完全不需要理解内容:
socket.on("message", (msg) => { const target = io.sockets.sockets.get(msg.target); target?.emit("message", { from: socket.id, data: msg.data, type: `on-${msg.type}` }); }); 复制代码
这样就实现了一个 server,下面来看 client 端实现。
client 实现:
与之前的本地 demo 相比,客户端最大的改变就是发送端和接收端是分离的,因此我们需要把两分逻辑拆分开,现在再来看 SDP 交互流程:
A createOffer offerA
A setLocalDescription offerA
A 发送 offerA 给 B
B setRemoteDescription offerA
B createAnswer AnswerB
B setLocalDescription AnswerB
B 发送 AnswerB 给 A
A setRemoteDescription AnswerB
这里 A 与 B 在两个不同的页面上,它们不会直接通信,中间加入信令服务器,交互的流程就变成了这样:
发送方:
createOffer
setLocalDescription
发送 offer 给 server
接收 server 的 answer
setRemoteDescription
接收方:
接收 server 的 offer
setRemoteDescription
createAnswer
setLocalDescription
发送 answer 给 server
我们先实现一个发送客户端,由于发送端是主动发起方,因此需要知道发送目标,这里使用一个简单的 user-list 机制来作为演示,当点击用户列表中的某个用户时向其进行发送:
userWrapper.addEventListener('click', async (e) => { targetId = e.target.innerText; if (targetId !== socket.id) { if (targetId) { const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = localStream; createPeerConnection(); localStream.getTracks().forEach(track => pc.addTrack(track, localStream)); } } }); 复制代码
createPeerConnection 中创建一个 RTCPeerConnection,可以在 negotiationneeded 事件回调中创建 offer,在监听到 icecandidate 时发送出去:
function createPeerConnection() { pc = new RTCPeerConnection(pcConfig); pc.addEventListener('negotiationneeded', async () => { const offer = await pc.createOffer(); await pc.setLocalDescription(offer); socket.emit('message', { type: 'offer', target: targetId, data: { offer } }); }); } 复制代码
之后就是等待回复了,这里监听 server 的数据:
case 'on-answer': await pc.setRemoteDescription(data.answer); break; 复制代码
之后再看接收端的逻辑,接收端的数据源来自信令服务器,这里从监听 server 开始:
case 'on-offer': targetId = from; createPeerConnection(); await pc.setRemoteDescription(data.offer); const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = localStream; localStream.getTracks().forEach(track => { pc.addTrack(track, localStream); }); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); socket.emit('message', { type: 'answer', target: targetId, data: { answer }, }); break; 复制代码
其中 createPeerConnection 逻辑与发送端是一致的,这里不会触发 negotiationneeded,不需要创建 offer。之后数据转发回发送端正常处理,SDP 交换的流程就结束了。
SDP 之后进行 ICE 交换,ICE 逻辑很简单,不需要数据握手,只要在收到时发出去,收到时设置到 PeerConnection 上即可:
pc.addEventListener('icecandidate', e => { if (e.candidate) { socket.emit('message', { type: 'candidate', target: targetId, data: { candidate: e.candidate } }); } }); 复制代码
case 'on-candidate': await pc.addIceCandidate(data.candidate); break; 复制代码
这样就建立好连接了,想要获取远端流直接监听 track 就可以:
pc.addEventListener('track', e => { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; } }); 复制代码
以上就是一个简单的 WebRTC P2P 系统的实现,整个过程中,只有连接时需要信令服务器的参与,连接成功后,音视频的发送是通过 addTrack 完成的,这个过程不需要信令服务器参与。
但是上面的 P2P demo 有一个问题,它只能在同一内网环境工作,这就涉及到 P2P 中另一个知识点:打洞和穿越。
STUN/TURN
由于 NAT 的特性,外网主机无法和内网主机直接建立连接。这时需要在我自己的 NAT 设备上先打一个洞,外网主机穿越这个洞来与内网用户建立连接。
我们可以使用 STUN/TURN 服务来实现内网穿透的能力,在创建 RTCPeerConnection 时可以通过参数传入 STUN/TURN 信息:
const config = { 'iceServers': [ { 'urls': 'stun:stun.l.google.com:19302' }, { "urls": "turn:numb.viagenie.ca", "username": "webrtc@live.com", "credential": "muazkh" } ] }; new RTCPeerConnection(config); 复制代码
STUN/TURN 服务器的搭建过程在此不展开了,感兴趣可以阅读 coturn 项目。
多人场景架构
上面的是一对一场景下的实现方案,在实际应用中还有多人音视频的需求,处理多人场景有以下几种方案:
P2P:P2P(Point To Point)即点对点,在一对一会话场景我们已经见过了,多人会话和单人会话一样,每两个人都要建立 P2P 连接。
客户端:n 人的会话,推流 n - 1 拉流 n - 1
优点:不需要服务器感知视频流
缺点:人数越多客户端压力越大,无法处理太多人的场景
MCU:MCU(Multi-point Control Unit)中文为多点控制单元,有一个中心服务器,用户的所有流都直接推到这个服务器上,由服务器将多路数据流合成一路,客户端拉这一路流即可。
客户端:n 人的会话,推流 1 拉流 1
优点:节省客户端资源,适用于广播业务场景
缺点:流在服务端已经确定,不能定制,无法满足灵活定制的业务场景
SFU:SFU(Selective Forwarding Unit) 中文为选择性转发单元,中心服务器只负责流的转发,由客户端根据需要选择拉流。
客户端:n 人的会话,推流 1 拉流 n
优点:可以满足复杂多遍的需求场景
缺点:拉流过多时客户端性能有影响,需要根据实际业务情况进行平衡
多人音视频会话有不同的业务形态,实际应用中需要根据具体的场景选择合适的架构方案。
作者:丨隋堤倦客丨
链接:https://juejin.cn/post/7036035156787855396
伪原创工具 SEO网站优化 https://www.237it.com/