一个简单的 RTCDataChannel 示例

RTCDataChannel 接口是 WebRTC API 的一个特性,它允许您在两个对等端之间打开一个通道,用于发送和接收任意数据。该 API 的设计有意地与 WebSocket API 相似,以便两者可以使用相同的编程模型。

在此示例中,我们将打开一个 RTCDataChannel 连接,将同一页面上的两个元素链接起来。虽然这是一个显然的模拟场景,但它有助于演示连接两个对等端的流程。我们将涵盖完成连接以及传输和接收数据的机制,但将把与定位和连接到远程计算机相关的部分留给另一个示例。

HTML

首先,让我们快速看一下所需的 HTML。这里没有什么特别复杂的地方。首先,我们有几个用于建立和关闭连接的按钮。

html
<button id="connectButton" name="connectButton" class="buttonleft">
  Connect
</button>
<button
  id="disconnectButton"
  name="disconnectButton"
  class="buttonright"
  disabled>
  Disconnect
</button>

然后有一个包含文本输入框的框,用户可以在其中输入要发送的消息,还有一个发送已输入文本的按钮。这个 <div> 将是通道中的第一个对等端。

html
<div class="messagebox">
  <label for="message"
    >Enter a message:
    <input
      type="text"
      name="message"
      id="message"
      placeholder="Message text"
      inputmode="latin"
      size="60"
      maxlength="120"
      disabled />
  </label>
  <button id="sendButton" name="sendButton" class="buttonright" disabled>
    Send
  </button>
</div>

最后,有一个小框,我们将在此处插入消息。这个 <div> 块将是第二个对等端。

html
<div class="messagebox" id="receive-box">
  <p>Messages received:</p>
</div>

JavaScript 代码

虽然您可以直接 在 GitHub 上查看代码本身,但下面我们将回顾代码中完成繁重工作的部分。

启动

脚本运行时,我们设置一个 load 事件监听器,以便在页面完全加载后调用我们的 startup() 函数。

js
let connectButton = null;
let disconnectButton = null;
let sendButton = null;
let messageInputBox = null;
let receiveBox = null;

let localConnection = null; // RTCPeerConnection for our "local" connection
let remoteConnection = null; // RTCPeerConnection for the "remote"

let sendChannel = null; // RTCDataChannel for the local (sender)
let receiveChannel = null; // RTCDataChannel for the remote (receiver)

function startup() {
  connectButton = document.getElementById("connectButton");
  disconnectButton = document.getElementById("disconnectButton");
  sendButton = document.getElementById("sendButton");
  messageInputBox = document.getElementById("message");
  receiveBox = document.getElementById("receive-box");

  // Set event listeners for user interface widgets

  connectButton.addEventListener("click", connectPeers);
  disconnectButton.addEventListener("click", disconnectPeers);
  sendButton.addEventListener("click", sendMessage);
}

这非常直接。我们声明变量并获取对所有需要访问的页面元素的引用,然后为三个按钮设置 事件监听器

建立连接

当用户单击“连接”按钮时,将调用 connectPeers() 方法。为了清晰起见,我们将对其进行分解并逐一查看。

注意: 即使我们连接的两端都在同一页面上,我们将把发起连接的一端称为“本地”端,将另一端称为“远程”端。

设置本地对等端

js
localConnection = new RTCPeerConnection();

sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;

第一步是创建连接的“本地”端。这是将发送连接请求的对等端。下一步是通过调用 RTCPeerConnection.createDataChannel() 来创建 RTCDataChannel,并设置事件监听器来监控通道,以便我们知道何时打开和关闭它(即,当通道在该对等连接内连接或断开时)。

重要的是要记住,通道的每一端都有自己的 RTCDataChannel 对象。

设置远程对等端

js
remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;

远程端的设置方式类似,只是我们不需要显式创建 RTCDataChannel,因为我们将通过上面建立的通道进行连接。相反,我们设置一个 datachannel 事件处理程序;当数据通道打开时会调用此处理程序;此处理程序将接收一个 RTCDataChannel 对象;您将在下面看到它。

设置 ICE 候选

下一步是为每个连接设置 ICE 候选监听器;当有新的 ICE 候选需要告知对方时,将调用这些监听器。

注意: 在实际场景中,当两个对等端不在同一上下文中运行时,过程会更复杂;每一方一次提供一种建议的连接方式(例如,UDP、通过中继的 UDP、TCP 等),通过调用 RTCPeerConnection.addIceCandidate(),它们会来回协商,直到达成一致。但在这里,由于没有实际的网络通信,我们只需接受每一方的第一个提议。

js
localConnection.onicecandidate = (e) =>
  !e.candidate ||
  remoteConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);

remoteConnection.onicecandidate = (e) =>
  !e.candidate ||
  localConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);

我们将每个 RTCPeerConnection 配置为拥有一个 icecandidate 事件的事件处理程序。

开始连接尝试

为了开始连接我们的对等端,我们需要做的最后一件事是创建一个连接提议。

js
localConnection
  .createOffer()
  .then((offer) => localConnection.setLocalDescription(offer))
  .then(() =>
    remoteConnection.setRemoteDescription(localConnection.localDescription),
  )
  .then(() => remoteConnection.createAnswer())
  .then((answer) => remoteConnection.setLocalDescription(answer))
  .then(() =>
    localConnection.setRemoteDescription(remoteConnection.localDescription),
  )
  .catch(handleCreateDescriptionError);

让我们逐行查看并弄清楚它的含义。

  1. 首先,我们调用 RTCPeerConnection.createOffer() 方法来创建一个描述我们想要建立的连接的 SDP(会话描述协议)块。此方法可选地接受一个对象,其中包含为满足您的需求而必须满足的约束,例如连接是否应支持音频、视频或两者都支持。在我们简单的示例中,我们没有任何约束。
  2. 如果提议成功创建,我们将该块传递给本地连接的 RTCPeerConnection.setLocalDescription() 方法。这配置了连接的本地端。
  3. 下一步是通过告知远程对等端来将本地对等端连接到远程端。这是通过调用 remoteConnection.setRemoteDescription() 来完成的。现在 remoteConnection 了解正在建立的连接。在实际应用程序中,这需要一个信令服务器来交换描述对象。
  4. 这意味着远程对等端是时候做出回应了。它通过调用其 createAnswer() 方法来做到这一点。这会生成一个 SDP 块,描述远程对等端愿意并且能够建立的连接。此配置位于两个对等端都可以支持的选项的联合中。
  5. 一旦创建了应答,就通过调用 RTCPeerConnection.setLocalDescription() 将其传递给 remoteConnection。这建立了连接的远程端(对于远程对等端来说,这是其本地端。这些东西可能会令人困惑,但您会习惯的)。同样,这通常会通过信令服务器进行交换。
  6. 最后,通过调用本地连接的 RTCPeerConnection.setRemoteDescription() 来设置本地连接的远程描述,以引用远程对等端。
  7. catch() 调用一个例程来处理发生的任何错误。

注意: 再次强调,这个过程不是一个实际的实现;在正常使用中,有两段代码在两台计算机上运行,进行交互和协商连接。通常使用一个称为“信令服务器”的侧信道来在两个对等端之间交换描述(以 application/sdp 格式)。

处理成功的对等端连接

随着对等端连接的每一端成功连接,将触发相应的 RTCPeerConnectionicecandidate 事件。这些处理程序可以执行任何需要的操作,但在本示例中,我们只需要更新用户界面。

js
function handleCreateDescriptionError(error) {
  console.log(`Unable to create an offer: ${error.toString()}`);
}

function handleLocalAddCandidateSuccess() {
  connectButton.disabled = true;
}

function handleRemoteAddCandidateSuccess() {
  disconnectButton.disabled = false;
}

function handleAddCandidateError() {
  console.log("Oh noes! addICECandidate failed!");
}

我们在这里唯一要做的就是当本地对等端连接时禁用“连接”按钮,并在远程对等端连接时启用“断开连接”按钮。

连接数据通道

一旦 RTCPeerConnection 打开,就会向远程端发送 datachannel 事件以完成打开数据通道的过程;这会调用我们的 receiveChannelCallback() 方法,该方法如下所示。

js
function receiveChannelCallback(event) {
  receiveChannel = event.channel;
  receiveChannel.onmessage = handleReceiveMessage;
  receiveChannel.onopen = handleReceiveChannelStatusChange;
  receiveChannel.onclose = handleReceiveChannelStatusChange;
}

datachannel 事件在其 channel 属性中包含对 RTCDataChannel 的引用,该引用代表通道的远程对等端。将其保存,然后我们为通道设置事件监听器,以处理我们想要响应的事件。完成此操作后,每次远程对等端接收到数据时,都会调用我们的 handleReceiveMessage() 方法,而通道的连接状态发生任何变化时,都会调用 handleReceiveChannelStatusChange() 方法,以便我们可以在通道完全打开和关闭时做出响应。

处理通道状态更改

我们的本地和远程对等端都使用一个方法来处理指示通道连接状态更改的事件。

当本地对等端遇到打开或关闭事件时,将调用 handleSendChannelStatusChange() 方法。

js
function handleSendChannelStatusChange(event) {
  if (sendChannel) {
    const state = sendChannel.readyState;

    if (state === "open") {
      messageInputBox.disabled = false;
      messageInputBox.focus();
      sendButton.disabled = false;
      disconnectButton.disabled = false;
      connectButton.disabled = true;
    } else {
      messageInputBox.disabled = true;
      sendButton.disabled = true;
      connectButton.disabled = false;
      disconnectButton.disabled = true;
    }
  }
}

如果通道的状态已更改为“打开”,则表示我们已完成两个对等端之间的链接建立。用户界面会相应地更新,方法是启用用于发送消息的文本输入框,将输入框聚焦以便用户可以立即开始键入,启用“发送”和“断开连接”按钮(因为它们现在可用),并禁用“连接”按钮(因为在连接打开时不需要它)。

如果状态已更改为“关闭”,则会发生相反的一系列操作:输入框和“发送”按钮被禁用,启用“连接”按钮以便用户可以选择打开新连接,并禁用“断开连接”按钮(因为它在不存在连接时无效)。

另一方面,我们示例中的远程对等端会忽略状态更改事件,除了将事件记录到控制台。

js
function handleReceiveChannelStatusChange(event) {
  if (receiveChannel) {
    console.log(
      `Receive channel's status has changed to ${receiveChannel.readyState}`,
    );
  }
}

handleReceiveChannelStatusChange() 方法将发生的事件作为输入参数接收;这将是一个 RTCDataChannelEvent

发送消息

当用户按下“发送”按钮时,将调用我们已经设置为该按钮 click 事件处理程序的 sendMessage() 方法。该方法很简单。

js
function sendMessage() {
  const message = messageInputBox.value;
  sendChannel.send(message);

  messageInputBox.value = "";
  messageInputBox.focus();
}

首先,从输入框的 value 属性获取消息文本。然后,通过调用 sendChannel.send() 将其发送到远程对等端。仅此而已!该方法的其余部分只是为了改善用户体验——输入框会被清空并重新聚焦,以便用户可以立即开始键入另一条消息。

接收消息

当远程通道上发生“消息”事件时,将调用我们的 handleReceiveMessage() 方法作为事件处理程序。

js
function handleReceiveMessage(event) {
  const el = document.createElement("p");
  const textNode = document.createTextNode(event.data);

  el.appendChild(textNode);
  receiveBox.appendChild(el);
}

此方法执行一些基本的 DOM 注入;它创建一个新的 <p>(段落)元素,然后创建一个包含消息文本的新 Text 节点,该文本在事件的 data 属性中接收。此文本节点将被追加为新元素的子节点,然后该新元素将被插入到 receiveBox 块中,从而使其在浏览器窗口中显示。

断开对等端连接

当用户单击“断开连接”按钮时,将调用先前设置为该按钮处理程序的 disconnectPeers() 方法。

js
function disconnectPeers() {
  // Close the RTCDataChannels if they're open.

  sendChannel.close();
  receiveChannel.close();

  // Close the RTCPeerConnections

  localConnection.close();
  remoteConnection.close();

  sendChannel = null;
  receiveChannel = null;
  localConnection = null;
  remoteConnection = null;

  // Update user interface elements

  connectButton.disabled = false;
  disconnectButton.disabled = true;
  sendButton.disabled = true;

  messageInputBox.value = "";
  messageInputBox.disabled = true;
}

首先,关闭每个对等端的 RTCDataChannel,然后同样关闭每个 RTCPeerConnection。然后将所有对这些对象的引用设置为 null 以避免意外重用,并更新用户界面以反映连接已关闭的事实。

后续步骤

查看在 GitHub 上可用的 webrtc-simple-datachannel 源代码。

另见