一個簡單的 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 原始碼。

另見