WebRTC PeerConnection and DataChannel: data exchange between browsers
- Tutorial

Many have heard of the WebRTC project , so I won’t go into the description. The other day I wanted to try sending messages between browsers, and to figure this out, I decided to write a primitive P2P chat. The experiment was a success, and based on my motives I decided to write this post. On Habré there were already articles covering the issues of using WebRTC for video transmission, but first of all (and last) I was interested in the possibility of exchanging text or binary data.
For communication between clients, we will use RTCPeerConnection (to establish a connection) and RTCDataChannel (to transfer data). In the process, we will also need RTCIceCandidate and RTCSessionDescription, but more on that later.
Support for the DataChannel protocol appeared in browsers recently, so in order for all this to work, you need Firefox 19+ or Chrome 25+. However, in Firefox <22 WebRTC is disabled by default (you need to set the parameter media.peerconnection.enabled to true), and Chrome 25 needs to be started with the --enable-data-channels flag. I did not look back at them, and this post is focused on Firefox 22+ and Chrome 26+. Opera 15 does not have WebRTC support.
Go
Since all this is under development and the designers in Firefox and Chrome have the moz and webkit prefixes respectively, let's bring a little order:
window._RTCPeerConnection = window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection || window.PeerConnection;
window._RTCIceCandidate = window.webkitRTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
window._RTCSessionDescription = window.webkitRTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
The implementations in Firefox and Chrome are different today, so we need to define a browser.
var browser = {
mozilla: /firefox/i.test(navigator.userAgent),
chrome: /chrom(e|ium)/i.test(navigator.userAgent)
};
In order for customers to learn about each other, it is proposed to use a “signal” mechanism, the implementation of which WebRTC leaves to the developer. This is usually a server through which clients learn about each other and exchange information, after which they establish a direct connection. This diagram is well shown in the illustration I borrowed from here :

- the first client sends the Offer to the second client through the server;
- the second client sends the Answer to the first through the server;
- customers now know about each other and can establish a connection.
In my case, I used WebSocket to communicate with the server on node.js. If this is a chat, then our server remembers each connected client, is able to transfer data from client to client and return a list of connected clients (for new arrivals).
Here I will not give the server code, because this is beyond the scope of the article. Let the client-side communication interface with our server be like this:
var observer = {
// ...
send: function(evt, data) {
// используется для отправки данных другому участнику
this._send(evt, data);
},
on: function(evt, callback) {
// устанавливает обработчик события ("ICE" или "SDP", речь о них пойдет ниже)
// ...
}
// ...
}
Create Connection
Imagine that there are two users - Alice and Bob. Bob went into the chat when there was no one there, and Alice came in a minute later and from the signal server she found out that Bob was online and was waiting for her. In this case, Alice will send a connection request to Bob, and Bob will answer her.
To start the connection, Alice creates an RTCPeerConnection object (as you remember, we made a cross-browser _RTCPeerConnection a little higher). The constructor needs to pass two arguments with parameters, which I will discuss below.
var pc, channel;
var config = {
iceServers: [{ url: !browser.mozilla ? "stun:stun.l.google.com:19302" : "stun:23.21.150.121" }]
};
var constrains = {
options: [{ DtlsSrtpKeyAgreement: true }, { RtpDataChannels: true }]
};
function createPC(isOffer) {
pc = new _RTCPeerConnection(config, constrains);
// Сразу установим обработчики событий
pc.onicecandidate = function(evt) {
if(evt.candidate) {
// Каждый ICE-кандидат мы будем отправлять другому участнику через сигнальный сервер
observer.send('ICE', evt.candidate);
}
};
pc.onconnection = function() {
// Пока это срабатывает только в Firefox
console.log('Connection established');
};
pc.onclosedconnection = function() {
// И это тоже. В Chrome о разрыве соединения придется узнавать другим способом
console.log('Disconnected');
};
if(isOffer) {
openOfferChannel();
createOffer();
} else {
openAnswerChannel();
}
}
// Алиса создает соединение
createPC(true);
Since in the real world many users are behind provider NAT , WebRTC provides ways to bypass it ( ICE , STUN , TURN ). The first config parameter passes an object with an array of STUN and / or TURN servers. You can use public, you can raise your own. I used the STUN server from Google. By the way, if I understood correctly, today in Firefox there are problems with the use of domain STUN-servers, therefore, it is recommended to use others in it.
The constrains parameter is optional; the connection settings are passed in it. You can read about the DtlsSrtpKeyAgreement option here , and the RtpDataChannels option, Apparently , need for Chrome 25 (and maybe even some versions). At 28, I worked without her.
To establish a connection, participants need to exchange ICE candidates through a signal server (they contain data about the network interface, address, etc.). When each candidate appears, the pc.onicecandidate event will fire (it will start after setting the local session using the setLocalDescription method, which will be discussed below).
Getting ready to accept candidates from another participant:
observer.on('ICE', function(ice) {
// добавляем пришедший ICE-кандидат
pc.addIceCandidate(new _RTCIceCandidate(ice));
});
Next, Alice creates a channel. This channel will be used for data transfer:
function openOfferChannel() {
// Первый параметр – имя канала, второй - настройки. В настоящий момент Chrome поддерживает только UDP-соединения (non-reliable), а Firefox – и UDP, и TCP (reliable)
channel = pc.createDataChannel('RTCDataChannel', browser.chrome ? {reliable: false} : {});
// Согласно спецификации, после создания канала клиент должен установить binaryType в "blob", но пока это поддерживает только Firefox (Chrome выбрасывает ошибку)
if(browser.mozilla) channel.binaryType = 'blob';
setChannelEvents();
}
function setChannelEvents() {
channel.onopen = function() {
console.log('Channel opened');
};
channel.onclose = function() {
console.log('Channel closed');
};
channel.onerror = function(err) {
console.log('Channel error:', err);
};
channel.onmessage = function(e) {
console.log('Incoming message:', e.data);
};
}
The next step, Alice creates and sends Bob an “Offer” (description of the session with various service information, SDP ).
function createOffer() {
pc.createOffer(function(offer) {
pc.setLocalDescription(offer, function() {
// Отправляем другому участнику через сигнальный сервер
observer.send('SDP', offer);
// После завершения этой функции начнет срабатывать событие pc.onicecandidate
}, function(err) {
console.log('Failed to setLocalDescription():', err);
});
}, function(err) {
console.log('Failed to createOffer():', err);
});
}
Now Alice is waiting for Bob's session from the signal server. When this happens, the setRemoteSDP function is called.
function setRemoteSDP(sdp) {
pc.setRemoteDescription(new _RTCSessionDescription(sdp), function() {
if(pc.remoteDescription.type == 'offer') {
// Это выполнится у Боба
createAnswer();
}
}, function(err) {
console.log('Failed to setRemoteDescription():', err);
});
}
observer.on('SDP', function(sdp) {
if(!pc) {
// Пришел Offer от другого участника
// Боб создает соединение
createPC(false);
}
setRemoteSDP(sdp);
});
Meanwhile, Bob receives Alice's session from the signal server, for his part, creates an RTCPeerConnection object and prepares to accept the channel (this is called from the createPC function).
function openAnswerChannel() {
pc.ondatachannel = function(e) {
channel = e.channel;
if(browser.mozilla) channel.binaryType = 'blob';
setChannelEvents();
};
}
Finally, Bob saves Alice's session, creates his own and sends it to Alice.
function createAnswer() {
pc.createAnswer(function(offer) {
pc.setLocalDescription(offer, function() {
// Отправляем другому участнику через сигнальный сервер
observer.send('SDP', offer);
}, function(err) {
console.log('Failed to setLocalDescription():', err);
});
}, function(err) {
console.log('Failed to createAnswer():', err);
});
}
Message exchange
After successfully completing setRemoteDescription () for both participants and exchanging ICE candidates, the connection between Alice and Bob should be established. In this case, the channel.onopen event will fire in Chrome and Firefox, and pc.onconnection in Firefox.
Alice and Bob can now exchange messages using the channel.send () method:
channel.send("Hi there!");
When a message is received, the channel.onmessage event will fire.
Disconnect Definition
When another participant completes the connection, two events are triggered immediately in Firefox: pc.onclosedconnection and channel.onclose.
But nothing works in Chrome, however, the pc object has the iceConnectionState property value changed to “disconnected” (according to my observations, it changes not immediately, but after a few seconds). Therefore, you will have to make a small crutch until the developers have fixed the event call.
if(browser.chrome) {
setInterval(function() {
if(pc.iceConnectionState == "disconnected") {
console.log("Disconnected");
}
}, 1000);
}
Current issues
- I want to note that today Chrome can send data with a length of no more than ~ 1100 bytes. Therefore, to send something more, you will have to divide the message and send in parts. Firefox already knows how to send large messages, it has no such problems.
- Another serious drawback is that while Chrome and Firefox are incompatible with each other (setRemoteDescription () with a session of another browser will throw an error, the connection will not be established).
- Theoretically, in this way you can send both text and binary data. Firefox has no problems with this, but the situation with Chrome is incomprehensible: they write on the Internet that binary data is not sent and wait for the developers to fix it, but in Chrome 28 I managed to send and receive it successfully. Maybe I don’t understand something.
Conclusion
The technology seems very promising to me, and now you can begin to experiment with its implementation, albeit with significant limitations.
And here is a link to a simple chat, the creation process of which inspired me to this article. I wrote it solely for training and learning WebRTC, and in Chrome it will not be able to send more than ~ 1100 bytes (I did not breakdown).
Sources of information:
- draft specification (there are a couple of examples in the same place)
- informative article on HTML5 Rocks
- The source code for this project also helped .