Статьи

Создание приложений реального времени с помощью веб-сокетов и событий, отправляемых на сервер

Эта статья была рецензирована Крейгом Билнером и Дэном Принсом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Важной частью написания многофункциональных интернет-приложений является реагирование на изменения данных. Рассмотрим следующую цитату Гильермо Рауха , взятую из его выступления на BrazilJS 2014 года «7 принципов многофункциональных веб-приложений» .

Когда данные изменятся на сервере, сообщите об этом клиентам, не спрашивая. Это форма повышения производительности, которая освобождает пользователя от действий по обновлению вручную (F5, pull to refresh). Новые задачи: (пере) управление подключениями, согласование состояния.

В этой статье мы рассмотрим примеры использования необработанного API WebSocket, а также менее известного EventSource для событий, отправляемых сервером (SSE), для создания «обновляемых» пользовательских интерфейсов в режиме реального времени. Если вы не уверены, что я имею в виду, я рекомендую посмотреть видео, указанное выше, или прочитать соответствующий пост в блоге .

Краткая история

В прошлом нам приходилось моделировать пересылку на сервер, наиболее заметным из которых был длинный опрос . Это подразумевало, что клиент делал длинный запрос, который оставался бы открытым, пока сервер не был готов отправить сообщение. После получения сообщения запрос будет закрыт и будет сделан новый запрос. Другие решения включали <iframe> и Flash. Это не было идеальным.

Затем в 2006 году Opera представила отправленные сервером события (SSE) из спецификации WHATWG Web Applications 1.0.
SSE позволял вам непрерывно передавать события с вашего веб-сервера в браузер посетителя. Другие браузеры последовали его примеру и начали внедрять SSE в 2011 году как часть спецификации HTML5.

Вещи продолжали становиться интересными в 2011 году, когда был стандартизирован протокол WebSocket . WebSockets позволяют вам открывать двустороннее постоянное соединение между клиентом и сервером, предоставляя вам возможность отправлять данные обратно клиентам при каждом изменении данных на сервере без запроса клиента. Это чрезвычайно важно для отзывчивости приложения с множеством одновременных подключений и быстро меняющимся контентом — например, многопользовательской онлайн-игрой. Тем не менее, до тех пор, пока socket.io — самая выдающаяся попытка представить WebSockets в массы — не была выпущена в 2014 году, мы увидели гораздо больше экспериментов с коммуникацией в реальном времени.

Достаточно сказать, что сегодня у нас есть намного более простые способы достижения серверной загрузки без выдачи новых запросов или использования нестандартных плагинов. Эти технологии дают вам возможность передавать данные обратно клиенту в тот момент, когда что-то происходит на сервере.

WebSockets

Самый простой способ понять, что позволяет вам сделать постоянное соединение, — это запустить работающую демонстрацию, мы рассмотрим код позже, а пока скачаем демонстрацию и поиграем.

демонстрация

 git clone https://github.com/sitepoint-editors/websocket-demo.git cd websocket-demo npm install npm start 

Откройте http: // localhost: 8080 / в нескольких окнах браузера и просмотрите журналы в браузере и на сервере, чтобы увидеть сообщения, идущие туда-сюда. Более важно отметить время, которое требуется для получения сообщения на сервере, и чтобы остальные подключенные клиенты были в курсе изменений.

Клиент

Конструктор WebSocket инициирует соединение с сервером по протоколам ws или wss (Secure). У него есть метод send для send данных на сервер, и вы можете предоставить обработчик onmessage для получения данных с сервера.

Вот аннотированный пример, показывающий все важные события:

 // Open a connection var socket = new WebSocket('ws://localhost:8081/'); // When a connection is made socket.onopen = function() { console.log('Opened connection '); // send data to the server var json = JSON.stringify({ message: 'Hello ' }); socket.send(json); } // When data is received socket.onmessage = function(event) { console.log(event.data); } // A connection could not be made socket.onerror = function(event) { console.log(event); } // A connection was closed socket.onclose = function(code, reason) { console.log(code, reason); } // Close the connection when the window is closed window.addEventListener('beforeunload', function() { socket.close(); }); 

Сервер

Безусловно, самая популярная библиотека Node для работы с WebSockets на сервере — это ws , мы будем использовать ее для упрощения, так как написание серверов WebSocket не является тривиальной задачей.

 var WSS = require('ws').Server; // Start the server var wss = new WSS({ port: 8081 }); // When a connection is established wss.on('connection', function(socket) { console.log('Opened connection '); // Send data back to the client var json = JSON.stringify({ message: 'Gotcha' }); socket.send(json); // When data is received socket.on('message', function(message) { console.log('Received: ' + message); }); // The connection was closed socket.on('close', function() { console.log('Closed Connection '); }); }); // Every three seconds broadcast "{ message: 'Hello hello!' }" to all connected clients var broadcast = function() { var json = JSON.stringify({ message: 'Hello hello!' }); // wss.clients is an array of all connected clients wss.clients.forEach(function each(client) { client.send(json); console.log('Sent: ' + json); }); } setInterval(broadcast, 3000); 

Пакет ws упрощает создание сервера с поддержкой WebSocket, однако вам следует ознакомиться с WebSocket Security, если вы используете его в работе.

Совместимость браузера

Браузерная поддержка WebSockets безупречна, за исключением Opera Mini и IE9 и ниже, для старых IE есть полифилл , который использует Flash за кулисами.

Отладка

В Chrome вы можете просматривать сообщения, отправленные и полученные в разделе Сеть> WS> Кадры, отправленные сообщения отображаются зеленым цветом.

Отладка WebSocket в Firefox возможна с помощью дополнения Websocket Monitor для Firefox Dev Tools. Он разработан командой разработчиков Firebug.

Отправленные сервером события

Как и WebSockets, SSE открывает постоянное соединение, которое позволяет отправлять данные обратно подключенным клиентам, когда что-то меняется на сервере. Единственное предостережение в том, что оно не позволяет сообщениям идти в другом направлении. Это не проблема, хотя у нас все еще есть хорошие старомодные методы Ajax для этого.

демонстрация

 git clone https://github.com/sitepoint-editors/server-sent-events-demo.git cd server-sent-events-demo npm install npm start 

Как и раньше, откройте http: // localhost: 8080 / в нескольких окнах браузера и просмотрите журналы как в браузере, так и на сервере, чтобы увидеть сообщения, идущие туда-сюда.

Клиент

Функция EventSource инициирует соединение с сервером по старому доброму HTTP или HTTPS. Он имеет API, аналогичный WebSocket и вы можете предоставить обработчик onmessage для получения данных с сервера. Вот аннотированный пример, показывающий все важные события.

 // Open a connection var stream = new EventSource("/sse"); // When a connection is made stream.onopen = function() { console.log('Opened connection '); }; // A connection could not be made stream.onerror = function (event) { console.log(event); }; // When data is received stream.onmessage = function (event) { console.log(event.data); }; // A connection was closed stream.onclose = function(code, reason) { console.log(code, reason); } // Close the connection when the window is closed window.addEventListener('beforeunload', function() { stream.close(); }); 

Сервер

Есть небольшая аккуратная оболочка для создания отправленных сервером событий. Сначала мы воспользуемся этим для упрощения, но отправка событий с сервера достаточно проста, поэтому мы объясним, как работает SSE на сервере позже.

 var SSE = require('sse'); var http = require('http'); var server = http.createServer(); var clients = []; server.listen(8080, '127.0.0.1', function() { // initialize the /sse route var sse = new SSE(server); // When a connection is made sse.on('connection', function(stream) { console.log('Opened connection '); clients.push(stream); // Send data back to the client var json = JSON.stringify({ message: 'Gotcha' }); stream.send(json); console.log('Sent: ' + json); // The connection was closed stream.on('close', function() { clients.splice(clients.indexOf(stream), 1); console.log('Closed connection '); }); }); }); // Every three seconds broadcast "{ message: 'Hello hello!' }" to all connected clients var broadcast = function() { var json = JSON.stringify({ message: 'Hello hello!' }); clients.forEach(function(stream) { stream.send(json); console.log('Sent: ' + json); }); } setInterval(broadcast, 3000) 

Отправка событий с сервера

Как уже упоминалось выше, отправка событий с сервера достаточно проста, чтобы сделать сами. Вот как:

Когда HTTP-запрос приходит из EventSource он будет иметь заголовок Accept text/event-stream , нам нужно ответить заголовками, которые поддерживают HTTP-соединение, а затем, когда мы будем готовы отправить данные обратно клиенту, которому мы записываем данные Response объект в специальном формате data: <data>\n\n .

 http.createServer(function(req, res) { // Open a long held http connection res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // Send data to the client var json = JSON.stringify({ message: 'Hello ' }); res.write("data: " + json + "\n\n"); }).listen(8000); 

В дополнение к полю data вы также можете отправить поля событий, идентификаторов и повторов, если они вам нужны, например,

 event: SOMETHING_HAPPENED data: The thing id: 123 retry: 300 event: SOMETHING_ELSE_HAPPENED data: The thing id: 124 retry: 300 

Хотя SSE удивительно просто реализовать как на клиенте, так и на сервере, как уже упоминалось выше, его единственное предостережение заключается в том, что он не обеспечивает способ отправки данных с клиента на сервер. К счастью, мы уже можем сделать это с помощью XMLHttpRequest или fetch . Наша новая найденная сверхдержава должна быть способна продвигаться от сервера к клиенту.

В целях безопасности, поскольку это HTTP, применяются стандартные правила перекрестного происхождения, поэтому всегда следует создавать источники белого списка как на сервере, так и на клиенте:

 stream.onmessage = function(event) { if (e.origin != 'http://example.com') return; } 

Тогда мы все еще можем нажать на сервер, как обычно, со старым добрым Ajax:

 document.querySelector('#send').addEventListener('click', function(event) { var json = JSON.stringify({ message: 'Hey there' }); var xhr = new XMLHttpRequest(); xhr.open('POST', '/api', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(json); log('Sent: ' + json); }); 

Совместимость браузера

Поддержка браузера для SSE ниже, чем для WebSocket, поскольку Microsoft никогда не поставляла браузер, который его поддерживает, для него существует сообщение об ошибке, и вам следует проголосовать за SSE, чтобы сделать его приоритетом для следующего выпуска.

Если вам нужно, чтобы SSE работал в IE и Edge сегодня, вы можете использовать Polyfill для EventSource .

Отладка

В Chrome вы можете просматривать сообщения, полученные в разделе Сеть> XHR> EventStream

проблемы

В статье Гильермо Рауха, процитированной в начале (повторном) управлении соединением и согласовании состояния, упоминаются как новые проблемы, которые эти постоянные соединения создали. Он прав, вам нужно подумать о том, что должно произойти, когда соединение потеряно и когда оно снова подключено.

EventSource имеет встроенный механизм переподключения, он будет пытаться переподключаться каждые 3 секунды, если соединение теряется автоматически. Вы можете проверить это в демоверсии SSE, установив соединение в браузере и остановив сервер с помощью Ctrl + C , вы увидите ошибки, регистрируемые до тех пор, пока вы не запустите сервер снова с помощью npm start , он сохранит спокойствие и продолжит работу ,

WebSocket не имеет этой возможности, если соединение потеряно, вам нужно создать новое и снова подключить события, если вы хотите того же поведения.

Согласование состояний — это практика синхронизации клиента с сервером при повторном подключении. Один из способов сделать это — отслеживать время, когда произошло отключение, и при повторном подключении отправлять все события, которые пропустил конкретный клиент во время отключения.

Решения этих проблем варьируются в зависимости от типа приложения, которое вы создаете:

  • Если вы строите многопользовательскую онлайн-игру, вам может потребоваться остановить игру, пока не произойдет переподключение.
  • В одностраничном приложении вы можете начать сохранять изменения локально, а затем отправлять массовые обновления на сервер при переподключении.
  • Если у вас есть традиционное приложение, содержащее всего пару страниц в режиме реального времени, вам может быть все равно, если соединение потеряно, поскольку в конечном итоге все будет согласованно.

Каркасы

Справедливо сказать, что эра WebSockets настала. Независимо от того, на каком языке программирования вы работаете на сервере, будет существовать инфраструктура, включающая методы для обработки постоянных подключений и трансляции для подключенных клиентов.

На стороне клиента эти платформы предоставляют вам методы для решения проблем (пере) управления соединением и согласования состояний и предоставляют простой способ подписки на разные «каналы». На стороне сервера они предлагают вам пул открытых соединений и механизмы вещания.

При реализации функции реального времени в вашем приложении нет необходимости отбрасывать то, что вы знаете о HTTP, и начинать заново. Вы можете начать, добавив один дополнительный маршрут (или канал), на который клиенты могут подписаться, что будет полезно при обновлении в режиме реального времени. Рассматривайте это как улучшение производительности как для клиента, так и для сервера, клиент мгновенно обновляется в тот момент, когда что-то происходит, и серверу не нужно отвечать на утомительный опрос:

Мы уже на месте? Мы уже на месте?

Теперь сервер может ответить при запуске.

Я скажу тебе, когда мы будем там

Используете ли вы WebSockets или отправленные сервером события в производстве? Есть ли рамки, которые я пропустил, которые заслуживают упоминания? Обязательно дайте мне знать в комментариях.