1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
|
---
title: Простой пример RTCDataChannel
slug: Web/API/WebRTC_API/Simple_RTCDataChannel_sample
translation_of: Web/API/WebRTC_API/Simple_RTCDataChannel_sample
---
<p>{{WebRTCSidebar}}</p>
<p>Интерфейс {{domxref("RTCDataChannel")}} является функциональностью <a href="/en-US/docs/Web/API/WebRTC_API">WebRTC API</a> , который позволяет открыть канал между узлами соединения, по которому можно отправлять и получать произвольные данные. Эти API намеренно сходны с <a href="/en-US/docs/Web/API/WebSocket_API">WebSocket API</a>, для использования единой программной модели.</p>
<p>В этом примере мы откроем соединение {{domxref ("RTCDataChannel")}}, связывающее два элемента на одной странице. Хотя это явно надуманный сценарий, он полезен для демонстрации последовательности соединения двух узлов. Мы расскажем о механизме выполнения соединения, передачи и получения данных, но оставим немного информации о поиске и подключении к удаленному компьютеру для другого примера.</p>
<h2 id="Разметка_HTML">Разметка HTML</h2>
<p>Сначала быстро посмотрим на <a class="external" href="https://github.com/mdn/samples-server/tree/master/s/webrtc-simple-datachannel/index.html" rel="noopener">необходимую разметку HTML </a>. В ней нет ничего сложного. В начале мы определяем пару кнопок, создающих и закрывающих соединение:</p>
<pre class="brush: html"><button id="connectButton" name="connectButton" class="buttonleft">
Connect
</button>
<button id="disconnectButton" name="disconnectButton" class="buttonright" disabled>
Disconnect
</button></pre>
<p>Затем, определяем блок, который содержит элемент управления ввода текста, в который пользователь печатает текст своего сообщения, предназначенного для отправки, по нажатию кнопки. Элемент {{HTMLElement("div")}} будет представлять первый узел в канале передачи (сторона отправителя).</p>
<pre class="brush: 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></pre>
<p>И наконец, небольшой блок, в который будем помещать получаемое сообщение. Элемент {{HTMLElement("div")}} будет представлять второй узел соединения (сторона получателя).</p>
<pre class="brush: html"><div class="messagebox" id="receivebox">
<p>Messages received:</p>
</div></pre>
<h2 id="Код_JavaScript">Код JavaScript</h2>
<p>While you can just <a class="external" href="https://github.com/mdn/samples-server/tree/master/s/webrtc-simple-datachannel/main.js" rel="noopener">look at the code itself on GitHub</a>, below we'll review the parts of the code that do the heavy lifting.</p>
<p>The WebRTC API makes heavy use of {{jsxref("Promise")}}s. They make it very easy to chain the steps of the connection process together; if you haven't already read up on this functionality of <a href="/en-US/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_6_support_in_Mozilla">ECMAScript 2015</a>, you should read up on them. Similarly, this example uses <a href="/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions">arrow functions</a> to simplify syntax.</p>
<h3 id="Starting_up">Starting up</h3>
<p>When the script is run, we set up an {{event("load")}} event listener, so that once the page is fully loaded, our <code>startup()</code> function is called.</p>
<pre class="brush: js">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);
}</pre>
<p>This is quite straightforward. We grab references to all the page elements we'll need to access, then set {{domxref("EventListener", "event listeners")}} on the three buttons.</p>
<h3 id="Establishing_a_connection">Establishing a connection</h3>
<p>When the user clicks the "Connect" button, the <code>connectPeers()</code> method is called. We're going to break this up and look at it a bit at a time, for clarity.</p>
<div class="note">
<p><strong>Note:</strong> Even though both ends of our connection will be on the same page, we're going to refer to the one that starts the connection as the "local" one, and to the other as the "remote" end.</p>
</div>
<h4 id="Set_up_the_local_peer">Set up the local peer</h4>
<pre class="brush: js">localConnection = new RTCPeerConnection();
sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;
</pre>
<p>The first step is to create the "local" end of the connection. This is the peer that will send out the connection request. The next step is to create the {{domxref("RTCDataChannel")}} by calling {{domxref("RTCPeerConnection.createDataChannel()")}} and set up event listeners to monitor the channel so that we know when it's opened and closed (that is, when the channel is connected or disconnected within that peer connection).</p>
<p>It's important to keep in mind that each end of the channel has its own {{domxref("RTCDataChannel")}} object.</p>
<h4 id="Set_up_the_remote_peer">Set up the remote peer</h4>
<pre class="brush: js">remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;</pre>
<p>The remote end is set up similarly, except that we don't need to explicitly create an {{domxref("RTCDataChannel")}} ourselves, since we're going to be connected through the channel established above. Instead, we set up a {{event("datachannel")}} event handler; this will be called when the data channel is opened; this handler will receive an <code>RTCDataChannel</code> object; you'll see this below.</p>
<h4 id="Set_up_the_ICE_candidates">Set up the ICE candidates</h4>
<p>The next step is to set up each connection with ICE candidate listeners; these will be called when there's a new ICE candidate to tell the other side about.</p>
<div class="note">
<p><strong>Note:</strong> In a real-world scenario in which the two peers aren't running in the same context, the process is a bit more involved; each side provides, one at a time, a suggested way to connect (for example, UDP, UDP with a relay, TCP, etc.) by calling {{domxref("RTCPeerConnection.addIceCandidate()")}}, and they go back and forth until agreement is reached. But here, we just accept the first offer on each side, since there's no actual networking involved.</p>
</div>
<pre class="brush: js"> localConnection.onicecandidate = e => !e.candidate
|| remoteConnection.addIceCandidate(e.candidate)
.catch(handleAddCandidateError);
remoteConnection.onicecandidate = e => !e.candidate
|| localConnection.addIceCandidate(e.candidate)
.catch(handleAddCandidateError);</pre>
<p>We configure each {{domxref("RTCPeerConnection")}} to have an event handler for the {{event("icecandidate")}} event.</p>
<h4 id="Start_the_connection_attempt">Start the connection attempt</h4>
<p>The last thing we need to do in order to begin connecting our peers is to create a connection offer.</p>
<pre class="brush: 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);</pre>
<p>Let's go through this line by line and decipher what it means.</p>
<ol>
<li>First, we call {{domxref("RTCPeerConnection.createOffer()")}} method to create an {{Glossary("SDP")}} (Session Description Protocol) blob describing the connection we want to make. This method accepts, optionally, an object with constraints to be met for the connection to meet your needs, such as whether the connection should support audio, video, or both. In our simple example, we don't have any constraints.</li>
<li>If the offer is created successfully, we pass the blob along to the local connection's {{domxref("RTCPeerConnection.setLocalDescription()")}} method. This configures the local end of the connection.</li>
<li>The next step is to connect the local peer to the remote by telling the remote peer about it. This is done by calling <code>remoteConnection.</code>{{domxref("RTCPeerConnection.setRemoteDescription()")}}. Now the <code>remoteConnection</code> knows about the connection that's being built. In a real application, this would require a signaling server to exchange the description object.</li>
<li>That means it's time for the remote peer to reply. It does so by calling its {{domxref("RTCPeerConnection.createAnswer", "createAnswer()")}} method. This generates a blob of SDP which describes the connection the remote peer is willing and able to establish. This configuration lies somewhere in the union of options that both peers can support.</li>
<li>Once the answer has been created, it's passed into the remoteConnection by calling {{domxref("RTCPeerConnection.setLocalDescription()")}}. That establishes the remote's end of the connection (which, to the remote peer, is its local end. This stuff can be confusing, but you get used to it). Again, this would normally be exchanged through a signalling server.</li>
<li>Finally, the local connection's remote description is set to refer to the remote peer by calling localConnection's {{domxref("RTCPeerConnection.setRemoteDescription()")}}.</li>
<li>The <code>catch()</code> calls a routine that handles any errors that occur.</li>
</ol>
<div class="note">
<p><strong>Note:</strong> Once again, this process is not a real-world implementation; in normal usage, there's two chunks of code running on two machines, interacting and negotiating the connection. A side channel, commonly called a “signalling server,” is usually used to exchange the description (which is in <strong>application/sdp</strong> form) between the two peers.</p>
</div>
<h4 id="Handling_successful_peer_connection">Handling successful peer connection</h4>
<p>As each side of the peer-to-peer connection is successfully linked up, the corresponding {{domxref("RTCPeerConnection")}}'s {{event("icecandidate")}} event is fired. These handlers can do whatever's needed, but in this example, all we need to do is update the user interface:</p>
<pre class="brush: js"> function handleLocalAddCandidateSuccess() {
connectButton.disabled = true;
}
function handleRemoteAddCandidateSuccess() {
disconnectButton.disabled = false;
}</pre>
<p>The only thing we do here is disable the "Connect" button when the local peer is connected and enable the "Disconnect" button when the remote peer connects.</p>
<h4 id="Connecting_the_data_channel">Connecting the data channel</h4>
<p>Once the {{domxref("RTCPeerConnection")}} is open, the {{event("datachannel")}} event is sent to the remote to complete the process of opening the data channel; this invokes our <code>receiveChannelCallback()</code> method, which looks like this:</p>
<pre class="brush: js"> function receiveChannelCallback(event) {
receiveChannel = event.channel;
receiveChannel.onmessage = handleReceiveMessage;
receiveChannel.onopen = handleReceiveChannelStatusChange;
receiveChannel.onclose = handleReceiveChannelStatusChange;
}</pre>
<p>The {{event("datachannel")}} event includes, in its channel property, a reference to a {{domxref("RTCDataChannel")}} representing the remote peer's end of the channel. This is saved, and we set up, on the channel, event listeners for the events we want to handle. Once this is done, our <code>handleReceiveMessage()</code> method will be called each time data is received by the remote peer, and the <code>handleReceiveChannelStatusChange()</code> method will be called any time the channel's connection state changes, so we can react when the channel is fully opened and when it's closed.</p>
<h3 id="Handling_channel_status_changes">Handling channel status changes</h3>
<p>Both our local and remote peers use a single method to handle events indicating a change in the status of the channel's connection.</p>
<p>When the local peer experiences an open or close event, the <code>handleSendChannelStatusChange()</code> method is called:</p>
<pre class="brush: js"> 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;
}
}
}</pre>
<p>If the channel's state has changed to "open", that indicates that we have finished establishing the link between the two peers. The user interface is updated correspondingly by enabling the text input box for the message to send, focusing the input box so that the user can immediately begin to type, enabling the "Send" and "Disconnect" buttons, now that they're usable, and disabling the "Connect" button, since it is not needed when the conneciton is open.</p>
<p>If the state has changed to "closed", the opposite set of actions occurs: the input box and "Send" button are disabled, the "Connect" button is enabled so that the user can open a new connection if they wish to do so, and the "Disconnect" button is disabled, since it's not useful when no connection exists.</p>
<p>Our example's remote peer, on the other hand, ignores the status change events, except for logging the event to the console:</p>
<pre class="brush: js"> function handleReceiveChannelStatusChange(event) {
if (receiveChannel) {
console.log("Receive channel's status has changed to " +
receiveChannel.readyState);
}
}</pre>
<p>The <code>handleReceiveChannelStatusChange()</code> method receives as an input parameter the event which occurred; this will be an {{domxref("RTCDataChannelEvent")}}.</p>
<h3 id="Sending_messages">Sending messages</h3>
<p>When the user presses the "Send" button, the sendMessage() method we've established as the handler for the button's {{event("click")}} event is called. That method is simple enough:</p>
<pre class="brush: js"> function sendMessage() {
var message = messageInputBox.value;
sendChannel.send(message);
messageInputBox.value = "";
messageInputBox.focus();
}</pre>
<p>First, the text of the message is fetched from the input box's {{htmlattrxref("value", "input")}} attribute. This is then sent to the remote peer by calling {{domxref("RTCDataChannel.send", "sendChannel.send()")}}. That's all there is to it! The rest of this method is just some user experience sugar -- the input box is emptied and re-focused so the user can immediately begin typing another message.</p>
<h3 id="Receiving_messages">Receiving messages</h3>
<p>When a "message" event occurs on the remote channel, our <code>handleReceiveMessage()</code> method is called as the event handler.</p>
<pre class="brush: js"> function handleReceiveMessage(event) {
var el = document.createElement("p");
var txtNode = document.createTextNode(event.data);
el.appendChild(txtNode);
receiveBox.appendChild(el);
}</pre>
<p>This method simply performs some basic {{Glossary("DOM")}} injection; it creates a new {{HTMLElement("p")}} (paragraph) element, then creates a new {{domxref("Text")}} node containing the message text, which is received in the event's <code>data</code> property. This text node is appended as a child of the new element, which is then inserted into the <code>receiveBox</code> block, thereby causing it to draw in the browser window.</p>
<h3 id="Disconnecting_the_peers">Disconnecting the peers</h3>
<p>When the user clicks the "Disconnect" button, the <code>disconnectPeers()</code> method previously set as that button's handler is called.</p>
<pre class="brush: 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;
}
</pre>
<p>This starts by closing each peer's {{domxref("RTCDataChannel")}}, then, similarly, each {{domxref("RTCPeerConnection")}}. Then all the saved references to these objects are set to <code>null</code> to avoid accidental reuse, and the user interface is updated to reflect the fact that the connection has been closed.</p>
<h2 id="Следующие_шаги">Следующие шаги</h2>
<p><a href="https://mdn-samples.mozilla.org/s/webrtc-simple-datachannel">Попробуйте пример в деле</a> и посмотрите на <a href="https://github.com/mdn/samples-server/tree/master/s/webrtc-simple-datachannel">исходный код простого примера</a>, доступный на GitHub.</p>
|