- PVSM.RU - https://www.pvsm.ru -

Многие слышали о проекте WebRTC [1], поэтому я не буду углубляться в описание. На днях мне захотелось попробовать отправлять сообщения между браузерами, и чтобы разобраться в этом, я решил написать примитивный P2P-чат. Эксперимент удалался, и по мотивам я решил написать этот пост. На Хабре уже были статьи, освещающие вопросы использования WebRTC для передачи видео, однако меня в первую (и последнюю) очередь интересовала возможность обмена текстовыми или бинарными данными.
Для общения между клиентами мы будем использовать RTCPeerConnection (для установления соединения) и RTCDataChannel (для передачи данных). В процессе нам также понадобятся RTCIceCandidate и RTCSessionDescription, но об этом позже.
Поддержка протокола DataChannel появилась в браузерах совсем недавно, поэтому для того, чтобы все это работало, нужен Firefox 19+ или Chrome 25+. Однако в Firefox < 22 WebRTC по умолчанию отключен (нужно установить параметр media.peerconnection.enabled в true), а Chrome 25 нужно запускать с флагом --enable-data-channels. Я не стал оглядываться на них, и этот пост ориентирован на Firefox 22+ и Chrome 26+. Opera 15 поддержки WebRTC не имеет.
Так как все это находится в разработке и конструкторы в Firefox и Chrome имеют префиксы moz и webkit соотвественно, давайте наведем небольшой порядок:
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;
Реализации в Firefox и Chrome на сегодняшний день отличаются, поэтому нам потребуется определять браузер.
var browser = {
mozilla: /firefox/i.test(navigator.userAgent),
chrome: /chrom(e|ium)/i.test(navigator.userAgent)
};
Для того, чтобы клиенты узнали друг о друге, предлагается использовать «сигнальный» механизм, реализиацию которого WebRTC оставляет разработчику. Обычно это сервер, через который клиенты узнают обмениваются информацией, после чего устанавливают прямое соединение. Эта схема хорошо показана на иллюстрации, которую я позаимствовал отсюда [2]:

В моем случае я использовал WebSocket для общения с сервером на node.js. Если это чат, то наш сервер помнит о каждом подключенном клиенте, умеет передавать данные от клиента клиенту и возвращать список подключенных клиентов (для новоприбывших пользователей).
Здесь я не буду приводить код сервера, т.к. это выходит за рамки статьи. Пусть интерфейс общения с нашим сервером на стороне клиента будет таким:
var observer = {
// ...
send: function(evt, data) {
// используется для отправки данных другому участнику
this._send(evt, data);
},
on: function(evt, callback) {
// устанавливает обработчик события ("ICE" или "SDP", речь о них пойдет ниже)
// ...
}
// ...
}
Представим, что есть два пользователя – Алиса и Боб. Боб зашел в чат, когда там никого не было, а Алиса зашла через минуту и с от сигнального сервера узнала, что Боб онлайн и ждет ее. В данном случае Алиса будет будет отправлять запрос на подключение Бобу, а Боб – отвечать ей.
Для начала соединения Алиса создает объект RTCPeerConnection (как вы помните, чуть выше мы сделали кроссбраузерный _RTCPeerConnection). Конструктору нужно передать два аргумента с параметрами, о которых я расскажу ниже.
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);
Так как в реальном мире многие пользователи находятся за провайдерским NAT [3], в WebRTC предусмотрены способы его обхода (ICE [4], STUN [5], TURN [6]). Первым параметром config передается объект с массивом STUN и/или TURN серверов. Можно использовать публичные, можно поднять свои. Я использовал STUN-сервер от Google. Кстати, если я правильно понял, сегодня в Firefox имеются проблемы с использованием доменных STUN-серверов, поэтому в нем рекомендуют использовать другие.
Параметр constrains необязательный, в нем передаются настройки соединения. Про опцию DtlsSrtpKeyAgreement можно почитать здесь [7], а опция RtpDataChannels, судя по всему [8], нужна для Chrome 25 (и, быть может, еще каких-то версий). В 28 у меня работало и без нее.
Для установки соединения участникам необходимо обменяться ICE-кандидатами через сигнальный сервер (в них содержатся данные о сетевом интерфейсе, адрес и др.). При появлении каждого кандидата будет срабатывать событие pc.onicecandidate (оно начнет срабатывать после установки локальной сессии методом setLocalDescription, о чем речь пойдет ниже).
Готовимся принимать кандидаты другого участника:
observer.on('ICE', function(ice) {
// добавляем пришедший ICE-кандидат
pc.addIceCandidate(new _RTCIceCandidate(ice));
});
Дальше Алиса создает канал. Именно этот канал и будет использоваться для передачи данных:
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);
};
}
Следующим шагом Алиса создает и отправляет Бобу «Offer» (описание сессии с различной служебной информацией, SDP [9]).
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);
});
}
Теперь Алиса ждет сессию Боба от сигнального сервера. Когда это случится, вызовется функция setRemoteSDP.
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);
});
Тем временем Боб получает от сигнального сервера сессию Алисы, со своей стороны создает объект RTCPeerConnection и готовится принять канал (это вызывается из функции createPC).
function openAnswerChannel() {
pc.ondatachannel = function(e) {
channel = e.channel;
if(browser.mozilla) channel.binaryType = 'blob';
setChannelEvents();
};
}
Наконец, Боб сохраняет сессию Алисы, создает свою и отправляет ее Алисе.
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);
});
}
После успешного выполнения setRemoteDescription() у обоих участников и обмена ICE-кандидатами соединение между Алисой и Бобом должно установиться. В этом случае в Chrome и Firefox сработает событие channel.onopen, а в Firefox – еще и pc.onconnection.
Теперь Алиса и Боб могут обмениваться сообщениями с помощью метода channel.send():
channel.send("Hi there!");
При получении сообщения сработает событие channel.onmessage.
Когда другой участник завершает соедниение, в Firefox срабатывает сразу два события: pc.onclosedconnection и channel.onclose.
А вот в Chrome не срабатывает ничего, однако у объекта pc значение свойства iceConnectionState меняется на «disconnected» (по моим наблюдениям, меняется не сразу, а через несколько секунд). Поэтому придется сделать небольшой костыль, пока разработчики не исправили вызов события.
if(browser.chrome) {
setInterval(function() {
if(pc.iceConnectionState == "disconnected") {
console.log("Disconnected");
}
}, 1000);
}
Технология кажется мне очень перспективной, и уже сейчас можно начинать экспериментировать с ее внедрением, хоть и с существенными ограничениями.
А вот и ссылка [10] на простенький чат, процесс создания которого и вдохновил меня на эту статью. Я писал его исключительно для тренировки и изучения WebRTC, и в Chrome он не сможет отправлять более ~1100 байт (разбивку я не делал).
Источники информации:
Автор: luethus
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/39126
Ссылки в тексте:
[1] WebRTC: http://webrtc.org
[2] отсюда: http://habrahabr.ru/post/163527/
[3] NAT: http://ru.wikipedia.org/wiki/NAT
[4] ICE: http://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment
[5] STUN: http://en.wikipedia.org/wiki/STUN
[6] TURN: http://en.wikipedia.org/wiki/TURN
[7] здесь: http://www.webrtc.org/interop
[8] судя по всему: http://www.webrtc.org/chrome
[9] SDP: http://tools.ietf.org/html/rfc3264
[10] ссылка: http://luethus.org/chat/
[11] черновик спецификации: http://dev.w3.org/2011/webrtc/editor/webrtc.html
[12] познавательная статья на HTML5 Rocks: http://www.html5rocks.com/en/tutorials/webrtc/basics/
[13] этого проекта: https://github.com/muaz-khan/WebRTC-Experiment
[14] Источник: http://habrahabr.ru/post/187270/
Нажмите здесь для печати.