[Из песочницы] Многопользовательский чат с изпользованием WebRTC
WebRTC — это API, предоставляемое браузером и позволяющее организовать P2P соединение и передачу данных напрямую между браузерами. В Интернете довольно много руководств по написанию собственного видео-чата при помощи WebRTC. Например, вот статья на Хабре. Однако, все они ограничиваются соединением двух клиентов. В этой статье я постараюсь рассказать о том, как при помощи WebRTC организовать подключение и обмен сообщениями между тремя и более пользователями.Интерфейс RTCPeerConnection представляет собой peer-to-peer подключение между двумя браузерами. Чтобы соединить трех и более пользователей, нам придется организовать mesh-сеть (сеть, в которой каждый узел подключен ко всем остальным узлам).Будем использовать следующую схему:
При открытии страницы проверяем наличие ID комнаты в location.hash Если ID комнаты не указано, генерируем новый Отправляем signalling server’у сообщение о том, что мы хотим присоединиться к указанной комнате Signalling server разсылает остальным клиентам в этой комнате оповещение о новом пользователе Клиенты, уже находящиеся к комнате, отправляют новичку SDP offer Новичок отвечает на offer’ы 0. Signalling serverКак известно, хоть WebRTC и предоставляет возможность P2P соединения между браузерами, для его работы всё равно требуется дополнительный транспорт для обмена сервисными сообщениями. В этом примере в качестве такого транспорта выступает WebSocket сервер, написанный на Node.JS с использованием socket.io: var socket_io = require («socket.io»);
module.exports = function (server) { var users = {}; var io = socket_io (server); io.on («connection», function (socket) {
// Желание нового пользователя присоединиться к комнате socket.on («room», function (message) { var json = JSON.parse (message); // Добавляем сокет в список пользователей users[json.id] = socket; if (socket.room!== undefined) { // Если сокет уже находится в какой-то комнате, выходим из нее socket.leave (socket.room); } // Входим в запрошенную комнату socket.room = json.room; socket.join (socket.room); socket.user_id = json.id; // Отправялем остальным клиентам в этой комнате сообщение о присоединении нового участника socket.broadcast.to (socket.room).emit («new», json.id); });
// Сообщение, связанное с WebRTC (SDP offer, SDP answer или ICE candidate) socket.on («webrtc», function (message) { var json = JSON.parse (message); if (json.to!== undefined && users[json.to] !== undefined) { // Если в сообщении указан получатель и этот получатель известен серверу, отправляем сообщение только ему… users[json.to].emit («webrtc», message); } else { // …иначе считаем сообщение широковещательным socket.broadcast.to (socket.room).emit («webrtc», message); } });
// Кто-то отсоединился socket.on («disconnect», function () { // При отсоединении клиента, оповещаем об этом остальных socket.broadcast.to (socket.room).emit («leave», socket.user_id); delete users[socket.user_id]; }); }); }; 1. index.html Исходный код самой страницы довольно простой. Я сознательно не стал уделять внимание верстке и прочим красивостям, так как это статья не об этом. Если кому-то захочется, сделать ее красивой, особого труда не составит.
if (! ROOM) { ROOM = uuid (); } room_link.innerHTML = »Link to the room»;
var ME = uuid (); 2.2. WebSocket Сразу при открытии страницы подключимся к нашему signalling server’у, отправим запрос на вход в комнату и укажем обработчики сообщений. // Указываем, что при закрытии сообщения нужно отправить серверу оповещение об этом var socket = io.connect (», {«sync disconnect on unload»: true}); socket.on («webrtc», socketReceived); socket.on («new», socketNewPeer); // Сразу отправляем запрос на вход в комнату socket.emit («room», JSON.stringify ({id: ME, room: ROOM}));
// Вспомогательная функция для отправки адресных сообщений, связанных с WebRTC function sendViaSocket (type, message, to) { socket.emit («webrtc», JSON.stringify ({id: ME, to: to, type: type, data: message})); } 2.3. Настройки PeerConnection Большинство провайдеров предоставляем подключение к Интернету через NAT. Из-за этого прямое подключение становится не таким уж тривиальным делом. При создании соединения нам нужно указать список STUN и TURN серверов, которые браузер будет пытаться использовать для обхода NAT. Так же укажем пару дополнительных опций для подключения. var server = { iceServers: [ {url: «stun:23.21.150.121»}, {url: «stun: stun.l.google.com:19302»}, {url: «turn: numb.viagenie.ca», credential: «your password goes here», username: «example@example.com»} ] }; var options = { optional: [ {DtlsSrtpKeyAgreement: true}, // требуется для соединения между Chrome и Firefox {RtpDataChannels: true} // требуется в Firefox для использования DataChannels API ] } 2.4. Подключение нового пользователя Когда в комнату добавляется новый пир, сервер отправляет нам сообщение new. Согласно обработчикам сообщений, указанным выше, вызовется функция socketNewPeer. var peers = {};
function socketNewPeer (data) { peers[data] = { candidateCache: [] };
// Создаем новое подключение var pc = new PeerConnection (server, options); // Инициализирууем его initConnection (pc, data, «offer»);
// Сохраняем пира в списке пиров peers[data].connection = pc;
// Создаем DataChannel по которому и будет происходить обмен сообщениями var channel = pc.createDataChannel («mychannel», {}); channel.owner = data; peers[data].channel = channel;
// Устанавливаем обработчики событий канала bindEvents (channel);
// Создаем SDP offer pc.createOffer (function (offer) { pc.setLocalDescription (offer); }); }
function initConnection (pc, id, sdpType) { pc.onicecandidate = function (event) { if (event.candidate) { // При обнаружении нового ICE кандидата добавляем его в список для дальнейшей отправки peers[id].candidateCache.push (event.candidate); } else { // Когда обнаружение кандидатов завершено, обработчик будет вызван еще раз, но без кандидата // В этом случае мы отправялем пиру сначала SDP offer или SDP answer (в зависимости от параметра функции)… sendViaSocket (sdpType, pc.localDescription, id); // …а затем все найденные ранее ICE кандидаты for (var i = 0; i < peers[id].candidateCache.length; i++) { sendViaSocket("candidate", peers[id].candidateCache[i], id); } } } pc.oniceconnectionstatechange = function (event) { if (pc.iceConnectionState == "disconnected") { connection_num.innerText = parseInt(connection_num.innerText) - 1; delete peers[id]; } } }
function bindEvents (channel) { channel.onopen = function () { connection_num.innerText = parseInt (connection_num.innerText) + 1; }; channel.onmessage = function (e) { chatlog.innerHTML += »
pc.setRemoteDescription (new SessionDescription (data)); pc.createAnswer (function (answer) { pc.setLocalDescription (answer); }); } function createConnection (id) { if (peers[id] === undefined) { peers[id] = { candidateCache: [] }; var pc = new PeerConnection (server, options); initConnection (pc, id, «answer»);
peers[id].connection = pc; pc.ondatachannel = function (e) { peers[id].channel = e.channel; peers[id].channel.owner = id; bindEvents (peers[id].channel); } } } 2.5.1 SDP answer function remoteAnswerReceived (id, data) { var pc = peers[id].connection; pc.setRemoteDescription (new SessionDescription (data)); } 2.5.2 ICE candidate function remoteCandidateReceived (id, data) { createConnection (id); var pc = peers[id].connection; pc.addIceCandidate (new IceCandidate (data)); } 2.6. Отправка сообщения При нажатии на кнопку Send вызывается функция sendMessage. Всё, что она делает, это проходится по списку пиров, и пытается отправить всем указанное сообщение. function sendMessage () { var msg = message.value; for (var peer in peers) { if (peers.hasOwnProperty (peer)) { if (peers[peer].channel!== undefined) { try { peers[peer].channel.send (msg); } catch (e) {} } } } chatlog.innerHTML += »
function onBeforeUnload (e) { for (var peer in peers) { if (peers.hasOwnProperty (peer)) { if (peers[peer].channel!== undefined) { try { peers[peer].channel.close (); } catch (e) {} } } } } 3. Список источников http://www.html5rocks.com/en/tutorials/webrtc/basics/ https://www.webrtc-experiment.com/docs/WebRTC-PeerConnection.html https://developer.mozilla.org/en-US/docs/Web/Guide/API/WebRTC/WebRTC_basics