一个简单的 RTCDataChannel 示例

The 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="receivebox">
  <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("receivebox");

  // Set event listeners for user interface widgets

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

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

建立连接

当用户点击“连接”按钮时,将调用 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(会话描述协议) Blob,描述我们想要建立的连接。此方法可选地接受一个包含要满足的连接约束的对象,例如连接是否应支持音频、视频或两者兼而有之。在我们的简单示例中,我们没有任何约束。
  2. 如果成功创建了请求,我们将把 Blob 传递给本地连接的 RTCPeerConnection.setLocalDescription() 方法。这将配置连接的本地端。
  3. 下一步是通过告知远程对等体来将本地对等体连接到远程对等体。这是通过调用 remoteConnection.setRemoteDescription() 来完成的。现在 remoteConnection 知道正在建立的连接。在实际应用中,这将需要一个信令服务器来交换描述对象。
  4. 这意味着远程对等体需要进行回复。它通过调用其 createAnswer() 方法来做到这一点。这将生成一个 SDP Blob,描述远程对等体愿意且能够建立的连接。此配置位于两个对等体都支持的选项的并集中。
  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;
    }
  }
}

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

如果状态已更改为“closed”,则会发生相反的操作:输入框和“发送”按钮被禁用,“连接”按钮被启用,以便用户可以根据需要打开新的连接,“断开连接”按钮被禁用,因为在没有连接的情况下它没有用处。

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

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()将其发送到远程对等体。就是这样!此方法的其余部分只是一些用户体验优化——输入框被清空并重新聚焦,以便用户可以立即开始输入另一条消息。

接收消息

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

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

  el.appendChild(txtNode);
  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源代码。

另请参阅