对等连接是 WebRTC 规范的一部分,用于处理将不同计算机上的两个应用连接起来以使用对等协议进行通信。对等方之间的通信可以是视频、音频或任意二进制数据(对于支持 RTCDataChannel API 的客户端)。为了发现两个对等互连如何连接,两个客户端都需要提供 ICE 服务器配置。这是 STUN 服务器或 TURN 服务器,其作用是向每个客户端提供 ICE 候选对象,然后将这些候选对象传输到远程对等方。这种 ICE 候选信息的传输通常称为信令。
信令
WebRTC 规范包含用于与 ICE(Internet 连接建立)服务器通信的 API,但信令组件不属于该规范。为了让两个对等方分享他们应如何连接,需要进行信令交换。通常,此问题可通过基于 HTTP 的常规 Web API(即 REST 服务或其他 RPC 机制)来解决,Web 应用可在启动对等连接之前中继必要的信息。
以下代码段展示了如何使用此虚构的信令服务来异步发送和接收消息。在本文档的其余示例中,我们将根据需要使用此变量。
// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
// New message from remote client received
});
// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');
信令可以通过多种不同的方式实现,WebRTC 规范并未偏好任何特定解决方案。
启动对等连接
每个对等连接都由一个 RTCPeerConnection 对象处理。此类的构造函数接受单个 RTCConfiguration 对象作为其参数。此对象定义了对等连接的设置方式,应包含有关要使用的 ICE 服务器的信息。
创建 RTCPeerConnection 后,我们需要创建 SDP 提议或应答,具体取决于我们是呼叫方对等互联方还是接收方对等互联方。创建 SDP offer 或 answer 后,必须通过其他渠道将其发送到远程对等方。将 SDP 对象传递给远程对等方称为信令,WebRTC 规范未涵盖此方面的内容。
为了从调用方发起对等连接设置,我们创建了一个 RTCPeerConnection 对象,然后调用 createOffer() 来创建 RTCSessionDescription 对象。此会话描述使用 setLocalDescription() 设置为本地描述,然后通过我们的信令通道发送到接收方。我们还为信号通道设置了一个监听器,用于在接收方收到我们提供的会话说明的回答时进行监听。
async function makeCall() {
const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.answer) {
const remoteDesc = new RTCSessionDescription(message.answer);
await peerConnection.setRemoteDescription(remoteDesc);
}
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
signalingChannel.send({'offer': offer});
}
在接收端,我们等待传入的 offer,然后创建 RTCPeerConnection 实例。完成该操作后,我们使用 setRemoteDescription() 设置收到的优惠。接下来,我们调用 createAnswer() 来创建对收到的 offer 的回答。此回答会使用 setLocalDescription() 设置为本地说明,然后通过我们的信令服务器发送给调用方。
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.offer) {
peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
signalingChannel.send({'answer': answer});
}
});
一旦两个对等方都设置了本地和远程会话说明,它们就会了解远程对等方的功能。但这并不意味着对等互连已准备就绪。为此,我们需要在每个对等方收集 ICE 候选对象,并通过信令通道将其传输到另一个对等方。
ICE 候选广告
在两个对等互连方可以使用 WebRTC 进行通信之前,他们需要交换连接信息。由于网络条件可能会因多种因素而异,因此通常使用外部服务来发现可连接到对等方的候选对象。此服务称为 ICE,使用 STUN 或 TURN 服务器。STUN 是 Session Traversal Utilities for NAT 的缩写,通常在大多数 WebRTC 应用中以间接方式使用。
TURN(Traversal Using Relay NAT)是一种更高级的解决方案,它整合了 STUN 协议,大多数基于 WebRTC 的商业服务都使用 TURN 服务器在对等方之间建立连接。WebRTC API 直接支持 STUN 和 TURN,并将其归类为更完整的术语“Internet 连接建立”。创建 WebRTC 连接时,我们通常会在 RTCPeerConnection 对象的配置中提供一个或多个 ICE 服务器。
Trickle ICE
创建 RTCPeerConnection 对象后,底层框架会使用提供的 ICE 服务器来收集用于建立连接的候选对象(ICE 候选对象)。RTCPeerConnection 上的事件 icegatheringstatechange 用于指示 ICE 收集处于何种状态(new、gathering 或 complete)。
虽然对等方可以等到 ICE 收集完成后再进行操作,但通常情况下,使用“trickle ice”技术并在发现每个 ICE 候选对象时将其传输到远程对等方会更高效。这将显著缩短对等连接的设置时间,并减少视频通话的启动延迟。
如需收集 ICE 候选,只需为 icecandidate 事件添加监听器即可。在该监听器上发出的 RTCPeerConnectionIceEvent 将包含 candidate 属性,该属性表示应发送到远程对等方的新候选对象(请参阅信令)。
// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
if (event.candidate) {
signalingChannel.send({'new-ice-candidate': event.candidate});
}
});
// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
if (message.iceCandidate) {
try {
await peerConnection.addIceCandidate(message.iceCandidate);
} catch (e) {
console.error('Error adding received ice candidate', e);
}
}
});
已建立连接
一旦开始接收 ICE 候选,我们应该会看到对等连接的状态最终变为已连接状态。为了检测这种情况,我们向 RTCPeerConnection 添加了一个监听器,用于监听 connectionstatechange 事件。
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});