--- title: RTCDataChannel 简单示例 slug: Web/API/WebRTC_API/Simple_RTCDataChannel_sample tags: - WebRTC - 建立 - 数据通道 translation_of: Web/API/WebRTC_API/Simple_RTCDataChannel_sample ---
{{WebRTCSidebar}}
{{domxref("RTCDataChannel")}} 接口是WebRTC API的一个功能,可以让您在两个对等体之间打开一个通道,您可以通过该通道发送和接收任意数据。 API有意地类似于WebSocket API,因此可以为每个API使用相同的编程模型。
在本示例中, 我们会在一个页面内建立 一条{{domxref("RTCDataChannel")}}链接 . 这个场景是为了演示如何链接两个Peer,实际场景并不常见。在本示例中解释了协商和建立链接的过程,定位和链接另外一台主机的场景在另外的一个示例中。
首先让我们看看我们需要的HTML代码HTML that's needed. 其实很简单,我们先有两个按钮用来链接和断开链接。
<button id="connectButton" name="connectButton" class="buttonleft"> Connect </button> <button id="disconnectButton" name="disconnectButton" class="buttonright" disabled> Disconnect </button>
然后我们还有一个输入框,用来输入消息。一个按钮,来触发发送事件。这个 {{HTMLElement("div")}} 是给channel中第一个节点使用的。
<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用来显示收到的内容. 这个 {{HTMLElement("div")}} 是给channel中第二个peer使用的。
<div class="messagebox" id="receivebox"> <p>Messages received:</p> </div>
你可以直接到look at the code itself on GitHub来看代码, 下面我们也会一步一步的解释。
WebRTC API 大量使用了{{jsxref("Promise")}}. 这样会让建立链接的过程变得简单;如果你还没有到ECMAScript 2015了解过Promise, 你应该先去看看. 另外本示例还使用了箭头语法arrow functions。
当脚本开始运行时, 我们对load事件挂接 {{event("load")}} 事件侦听, 因此一旦页面完全加载, startup()
函数将被调用。
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); }
上述逻辑一目了然. 我们拿到所有需要操作的页面元素引用, 之后对三个按钮设置事件侦听 {{domxref("EventListener", "event listeners")}} 。
当用户点击 "Connect" 按钮, connectPeers()
方法被调用。下面将逐一分析该方法中的细节。
注意: 尽管参与连接的两端都在同一页面,我们将启动连接的一端称为 "local" 端,另一端称为 "remote" 端。
localConnection = new RTCPeerConnection(); sendChannel = localConnection.createDataChannel("sendChannel"); sendChannel.onopen = handleSendChannelStatusChange; sendChannel.onclose = handleSendChannelStatusChange;
第一步是建立该连接的 "local" 端,它是发起连接请求的一方。 下一步是通过调用{{domxref("RTCPeerConnection.createDataChannel()")}} 来创建 {{domxref("RTCDataChannel")}} 并设置事件侦听以监视该数据通道, 从而获知该通道的打开或关闭 (即获得该对等连接的通道打开或者关闭的时机)。
请务必记住该通道的每一端都拥有自己的 {{domxref("RTCDataChannel")}} 对象。
remoteConnection = new RTCPeerConnection(); remoteConnection.ondatachannel = receiveChannelCallback;
远程端的建立过程类似“local”端, 但它无需自己创建 {{domxref("RTCDataChannel")}} , 因为我们将通过上面建立的渠道进行连接。 我们创建对 {{event("datachannel")}} 的事件处理回调;数据通道打开时该逻辑将被执行, 该回调处理将接收到一个 RTCDataChannel
对象,此过程将在文章后面部分描述。
下一步为每个连接建立 ICE 候选侦听处理, 当连接的一方出现新的 ICE 候选时该侦听逻辑将被调用以告知连接的另一方此消息。
注意: 在现实场景,当参与连接的两节点运行于不同的上下文,建立连接的过程或稍微复杂些,每一次双方通过调用{{domxref("RTCPeerConnection.addIceCandidate()")}},提出连接方式的建议 (例如: UDP,、中继UDP 、 TCP之类的) , 双方来回往复直到达成一致。本文既然不涉及现实网络环境,因此我们假定双方接受首次连接建议。
localConnection.onicecandidate = e => !e.candidate || remoteConnection.addIceCandidate(e.candidate) .catch(handleAddCandidateError); remoteConnection.onicecandidate = e => !e.candidate || localConnection.addIceCandidate(e.candidate) .catch(handleAddCandidateError);
我们配置每个 {{domxref("RTCPeerConnection")}} 对于事件 {{event("icecandidate")}} 建立事件处理。
建立节点连接的最后一项是创建一个连接offer.
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);
逐行解读上面的代码:
remoteConnection.
{{domxref("RTCPeerConnection.setRemoteDescription()")}},告知remote节点上述描述,将local 节点连接到到远程 。 现在 remoteConnection
了解正在建立的连接。catch()
调用一个用于处理任何异常的逻辑。注意: 再次申明,上述处理过程并非针对现实世界的实现,在正常环境下,建立连接的两端的机器,运行两块不同的代码,用于交互和协商连接过程。
当peer-to-peer连接的任何一方成功连接, 相应的 {{domxref("RTCPeerConnection")}}的{{event("icecandidate")}} 事件将被触发。 在事件的处理中可以执行任何需要的操作, 但在本例中,我们所需要做的只是更新用户界面。
function handleLocalAddCandidateSuccess() { connectButton.disabled = true; } function handleRemoteAddCandidateSuccess() { disconnectButton.disabled = false; }
当local节点连接成功时,禁用 "Connect" 按钮, 当remote节点连接时许用 "Disconnect" 按钮。
{{domxref("RTCPeerConnection")}} 一旦open, 事件{{event("datachannel")}} 被发送到远端以完成打开数据通道的处理, 该事件触发 receiveChannelCallback()
方法,如下所示:
function receiveChannelCallback(event) { receiveChannel = event.channel; receiveChannel.onmessage = handleReceiveMessage; receiveChannel.onopen = handleReceiveChannelStatusChange; receiveChannel.onclose = handleReceiveChannelStatusChange; }
事件{{event("datachannel")}} 在它的channel属性中包括了: 对代表remote节点的 channel的{{domxref("RTCDataChannel")}} 的指向, 它保存了我们用以在该channel上对我们希望处理的事件建立的事件监听。 一旦侦听建立, 每当remote节点接收到数据 handleReceiveMessage()
方法将被调用, 每当通道的连接状态发生改变 handleReceiveChannelStatusChange()
方法将被调用, 因此通道完全打开或者关闭时我们都可以作出相应的相应。
local节点和remote节点采用同样的方法处理表示通道连接状态变更的事件。
当local节点遭遇open 或者 close 事件, handleSendChannelStatusChange()
方法被调用:
function handleSendChannelStatusChange(event) { if (sendChannel) { var 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", 意味着我们已经完成了在两对等节点之间建立连接。 相应地用户界面根据状态更新,许用并将输入光标聚焦在text 输入框,以便用户可以立即输入要发送给对方的文本消息, 同时界面许用 "Send" 和 "Disconnect" 按钮(既然它们已经准备好了),禁用"Connect"按钮,既然在已经建立连接的情况下用不着它。
当连接状态变更为 "closed"时,界面执行相反的操作: 禁用文本输入框和 "Send" 按钮 , 许用"Connect" 按钮, 以便用户在需要时可以打开新的连接,禁用"Disconnect" 按钮,既然没有连接时用不着它。
另一方面,作为我们例子的remote 节点, 则无视这些状态改变事件, 仅仅是在控制台输出它们:
function handleReceiveChannelStatusChange(event) { if (receiveChannel) { console.log("Receive channel's status has changed to " + receiveChannel.readyState); } }
handleReceiveChannelStatusChange()
方法接收到发生的事件,事件类型为 {{domxref("RTCDataChannelEvent")}}.
当用户按下 "Send" 按钮,触发我们已建立的该按钮的 {{event("click")}} 事件处理逻辑,在处理逻辑中调用sendMessage() 方法。 该方法也足够简单:
function sendMessage() { var message = messageInputBox.value; sendChannel.send(message); messageInputBox.value = ""; messageInputBox.focus(); }
首先,待发送的消息文本从文本输入框的 {{htmlattrxref("value", "input")}}属性获得,之后该文本通过调用 {{domxref("RTCDataChannel.send", "sendChannel.send()")}}发送到remote节点。 都搞定了! 余下的只是些用户体验糖 ——清空并聚焦文本输入框,以便用户可以立即开始下一条消息的输入。
当远程通道发生“message”事件时,我们的handleReceiveMessage()方法被调用来处理事件。
function handleReceiveMessage(event) { var el = document.createElement("p"); var txtNode = document.createTextNode(event.data); el.appendChild(txtNode); receiveBox.appendChild(el); }
该方法只是简单地注入了一些 {{Glossary("DOM")}}, 它创建了 {{HTMLElement("p")}} (paragraph) 元素, 然后创建了 {{domxref("Text")}} 用于显示从事件的data
属性拿到的消息文本。该text node作为子节点附加到receiveBox
block,显示在浏览器窗口内容区。
当用户点击"Disconnect" 按钮。在之前我们设置的按钮事件处理逻辑中disconnectPeers()
方法被调用。
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; }
该方法首先关闭每个节点的{{domxref("RTCDataChannel")}},之后类似地关闭每个节点的 {{domxref("RTCPeerConnection")}}。将所有对它们的指向置为null
以避免意外的复用。 之后更新界面状态以符合目前已经不存在连接的事实。
You should try out this example and take a look at the webrtc-simple-datachannel source code, available on GitHub.