Большинство приложений WebRTC не просто способны общаться через видео и аудио. Им нужно много других функций. В этой главе мы собираемся создать базовый сервер сигнализации.
Сигнализация и переговоры
Чтобы подключиться к другому пользователю, вы должны знать, где он находится в Интернете. IP-адрес вашего устройства позволяет подключенным к Интернету устройствам отправлять данные непосредственно между собой. За это отвечает объект RTCPeerConnection . Как только устройства узнают, как найти друг друга через Интернет, они начинают обмениваться данными о том, какие протоколы и кодеки поддерживает каждое устройство.
Для общения с другим пользователем вам просто необходимо обменяться контактной информацией, а остальное сделает WebRTC. Процесс подключения к другому пользователю также известен как сигнализация и согласование. Он состоит из нескольких шагов —
-
Создайте список потенциальных кандидатов для однорангового соединения.
-
Пользователь или приложение выбирает пользователя для подключения.
-
Слой сигнализации уведомляет другого пользователя, что кто-то хочет подключиться к нему. Он может принять или отклонить.
-
Первый пользователь уведомляется о принятии предложения.
-
Первый пользователь инициирует RTCPeerConnection с другим пользователем.
-
Оба пользователя обмениваются программной и аппаратной информацией через сервер сигнализации.
-
Оба пользователя обмениваются информацией о местоположении.
-
Соединение успешно или неудачно.
Создайте список потенциальных кандидатов для однорангового соединения.
Пользователь или приложение выбирает пользователя для подключения.
Слой сигнализации уведомляет другого пользователя, что кто-то хочет подключиться к нему. Он может принять или отклонить.
Первый пользователь уведомляется о принятии предложения.
Первый пользователь инициирует RTCPeerConnection с другим пользователем.
Оба пользователя обмениваются программной и аппаратной информацией через сервер сигнализации.
Оба пользователя обмениваются информацией о местоположении.
Соединение успешно или неудачно.
Спецификация WebRTC не содержит каких-либо стандартов обмена информацией. Так что имейте в виду, что вышеизложенное является лишь примером того, как может происходить передача сигналов. Вы можете использовать любой протокол или технологию, которая вам нравится.
Сборка сервера
Сервер, который мы собираемся построить, сможет соединить двух пользователей, которые не находятся на одном компьютере. Мы создадим наш собственный механизм сигнализации. Наш сигнальный сервер позволит одному пользователю звонить другому. Как только пользователь вызвал другого, сервер передает предложение, ответ, кандидатов ICE между ними и устанавливает соединение WebRTC.
Приведенная выше диаграмма представляет собой поток сообщений между пользователями при использовании сервера сигнализации. Прежде всего, каждый пользователь регистрируется на сервере. В нашем случае это будет простая строка имени пользователя. После регистрации пользователи могут звонить друг другу. Пользователь 1 делает предложение с идентификатором пользователя, которому он хочет позвонить. Другой пользователь должен ответить. Наконец, кандидаты ICE отправляются между пользователями, пока они не смогут установить соединение.
Чтобы создать соединение WebRTC, клиенты должны иметь возможность передавать сообщения без использования однорангового соединения WebRTC. Здесь мы будем использовать HTML5 WebSockets — двунаправленное сокетное соединение между двумя конечными точками — веб-сервером и веб-браузером. Теперь давайте начнем использовать библиотеку WebSocket. Создайте файл server.js и вставьте следующий код —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message){ console.log("Got message from a user:", message); }); connection.send("Hello from server"); });
Первая строка требует библиотеки WebSocket, которую мы уже установили. Затем мы создаем сервер сокетов на порту 9090. Затем мы прослушиваем событие подключения . Этот код будет выполнен, когда пользователь устанавливает соединение WebSocket с сервером. Затем мы слушаем любые сообщения, отправленные пользователем. Наконец, мы отправляем ответ подключенному пользователю, говоря «Привет с сервера».
Теперь запустите сервер узла, и сервер должен начать прислушиваться к сокетным соединениям.
Для тестирования нашего сервера мы будем использовать утилиту wscat, которую мы также уже установили. Этот инструмент помогает подключаться напрямую к серверу WebSocket и тестировать команды. Запустите наш сервер в одном окне терминала, затем откройте другое и выполните команду wscat -c ws: // localhost: 9090 . Вы должны увидеть следующее на стороне клиента —
Сервер также должен регистрировать подключенного пользователя —
Регистрация пользователя
На нашем сервере сигнализации мы будем использовать имя пользователя на основе строки для каждого соединения, чтобы мы знали, куда отправлять сообщения. Давайте немного изменим наш обработчик соединения —
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } });
Таким образом, мы принимаем только сообщения JSON. Далее нам нужно где-то хранить всех подключенных пользователей. Мы будем использовать простой объект Javascript для него. Изменить верх нашего файла —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {};
Мы собираемся добавить поле типа для каждого сообщения от клиента. Например, если пользователь хочет войти, он отправляет сообщение типа входа . Давайте определим это —
connection.on('message', function(message){ var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; default: sendTo(connection, { type: "error", message: "Command no found: " + data.type }); break; } });
Если пользователь отправляет сообщение с типом логина , мы —
-
Проверьте, если кто-то уже вошел в систему с этим именем пользователя
-
Если это так, то сообщите пользователю, что он не вошел в систему
-
Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
-
Если команда не распознана, мы отправляем ошибку.
Проверьте, если кто-то уже вошел в систему с этим именем пользователя
Если это так, то сообщите пользователю, что он не вошел в систему
Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
Если команда не распознана, мы отправляем ошибку.
Следующий код является вспомогательной функцией для отправки сообщений в соединение. Добавьте его в файл server.js —
function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Вышеуказанная функция гарантирует, что все наши сообщения отправляются в формате JSON.
Когда пользователь отключается, мы должны очистить его соединение. Мы можем удалить пользователя при возникновении события закрытия . Добавьте следующий код в обработчик соединения —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; } });
Теперь давайте проверим наш сервер командой login. Помните, что все сообщения должны быть закодированы в формате JSON. Запустите наш сервер и попробуйте войти. Вы должны увидеть что-то вроде этого —
Звонить
После успешного входа в систему пользователь хочет позвонить другому. Он должен сделать предложение другому пользователю для достижения этого. Добавить обработчик предложения —
case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null){ //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break;
Во-первых, мы получаем соединение с пользователем, которого пытаемся вызвать. Если он существует, мы отправляем ему детали предложения . Мы также добавляем otherName к объекту соединения . Это сделано для простоты поиска позже.
Ответ
Ответ на ответ имеет аналогичную схему, которую мы использовали в обработчике предложений . Наш сервер просто проходит через все сообщения как ответ другому пользователю. Добавьте следующий код после предложения Hander —
case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break;
Вы можете увидеть, как это похоже на обработчик предложений . Обратите внимание, что этот код следует за функциями createOffer и createAnswer объекта RTCPeerConnection .
Теперь мы можем проверить наш механизм предложения / ответа. Соедините двух клиентов одновременно и попробуйте сделать предложение и ответить. Вы должны увидеть следующее —
В этом примере предложение и ответ являются простыми строками, но в реальном приложении они будут заполнены данными SDP.
ЛЕД Кандидаты
Заключительная часть — обработка кандидата ICE между пользователями. Мы используем ту же технику, просто передавая сообщения между пользователями. Основное отличие состоит в том, что сообщения-кандидаты могут появляться несколько раз на пользователя в любом порядке. Добавьте обработчик кандидата —
case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break;
Он должен работать аналогично обработчикам предложений и ответов .
Оставляя связь
Чтобы позволить нашим пользователям отключаться от другого пользователя, мы должны реализовать функцию зависания. Он также скажет серверу удалить все пользовательские ссылки. Добавьте обработчик отпуска —
case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break;
Это также отправит другому пользователю событие ухода, чтобы он мог соответствующим образом отключить одноранговое соединение. Также следует разобраться со случаем, когда пользователь сбрасывает соединение с сервера сигнализации. Давайте изменим наш близкий обработчик —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } });
Теперь, если соединение прекращается, наши пользователи будут отключены. Событие close будет вызвано, когда пользователь закроет окно своего браузера, пока мы все еще в состоянии предложения , ответа или состояния кандидата .
Полный сигнальный сервер
Вот весь код нашего сигнального сервера —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {}; //when a user connects to our sever wss.on('connection', function(connection) { console.log("User connected"); //when server gets a message from a connected user connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break; case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break; case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break; case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break; default: sendTo(connection, { type: "error", message: "Command not found: " + data.type }); break; } }); //when user exits, for example closes a browser window //this may help if we are still in "offer","answer" or "candidate" state connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } }); connection.send("Hello world"); }); function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Итак, работа выполнена, и наш сервер сигнализации готов. Помните, что неправильная работа при подключении к WebRTC может вызвать проблемы.
Резюме
В этой главе мы создали простой и понятный сервер сигнализации. Мы прошли через процесс сигнализации, регистрации пользователей и механизма предложения / ответа. Мы также реализовали отправку кандидатов между пользователями.