Как использовать WebSockets в Node.js для создания приложений реального времени

В этом руководстве демонстрируется использование WebSockets в Node.js для двухстороннего, интерактивного общения между браузером и сервером. Эта техника является важной для быстрых, реально-временных приложений, таких как панели мониторинга, чат-приложения и многопользовательские игры.

Table of Contents

Веб основан на сообщениях HTTP запрос-ответ. Ваш браузер делает запрос по URL, и сервер отвечает данными. Это может привести к дальнейшим запросам браузера и ответам сервера для изображений, CSS, JavaScript и т.д., но сервер не может произвольно отправлять данные браузеру.

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

После начального подключения от браузера, серверно отправленные события являются стандартным (стримированным) HTTP ответом, который может отправлять сообщения с сервера в любое время. Однако канал односторонний, и браузер не может отправлять сообщения обратно. Для подлинно быстрой двусторонней коммуникации требуются WebSocket.

Обзор WebSocket

Термин WebSocket относится к протоколу связи TCP через ws:// или защищенный и зашифрованный wss://. Это отличается от HTTP, хотя он может работать через порты 80 или 443, чтобы обеспечить работу в местах, которые блокируют не-веб трафик. Большинство браузеров, выпущенных с 2012 года, поддерживают протокол WebSocket.

В типичном реальном веб-приложении вам нужно по крайней мере один веб-сервер для обслуживания веб-контента (HTML, CSS, JavaScript, изображения и т.д.) и один сервер WebSocket для обработки двусторонней коммуникации.

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

Общение с другими подключенными браузерами

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

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

Поддержка WebSocket Сервера

Node.js пока не имеет встроенной поддержки WebSocket, хотя ходят слухи, что она скоро появится! Для этой статьи я использую сторонний модуль ws, но существует множество других.

Встроенная поддержка WebSocket доступна в JavaScript средах выполнения Deno и Bun.

Библиотеки WebSocket доступны для сред выполнения, включая PHP, Python и Ruby. Также существуют сторонние SaaS-варианты, такие как Pusher и PubNub, которые предоставляют хостинг услуг WebSocket.

Быстрый старт демонстрации WebSockets

Приложения чата являются “Hello, World!” демонстраций WebSocket, поэтому прошу прощения за:

  1. Неоригинальность. Тем не менее, чат-приложения отлично подходят для объяснения концепций.

  2. Невозможность предоставить полностью размещенное онлайн-решение. Я предпочел бы не следить и не модерировать поток анонимных сообщений!

Клонируйте или загрузите репозиторий node-wschat с GitHub:

git clone https://github.com/craigbuckler/node-wschat

Установите зависимости Node.js:

cd node-wschat
npm install

Запустите чат-приложение:

npm start

Откройте http://localhost:3000/ в нескольких браузерах или вкладках (вы также можете определить свое имя чата в строке запроса — например, http://localhost:3000/?Craig). Введите что-нибудь в одном окне и нажмите SEND или нажмите Enter, вы увидите, что это появляется во всех подключенных браузерах.

Обзор кода Node.js

Входной файл Node.js приложения index.js запускает два сервера:

  1. Express-приложение, работающее по адресу http://localhost:3000/ с шаблоном EJS для обслуживания одной страницы с клиентским HTML, CSS и JavaScript. Браузер JavaScript использует WebSocket API для создания начального соединения, а затем для отправки и получения сообщений.

  2. A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:

    // Сервер WebSocket
    import WebSocket, { WebSocketServer } from 'ws';
    
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    // подключение клиента
    ws.on('connection', (socket, req) => {
    
      console.log(`connection from ${ req.socket.remoteAddress }`);
    
      // полученное сообщение
      socket.on('message', (msg, binary) => {
    
        // трансляция всем клиентам
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
    
      });
    
      // закрыто
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    
    });

The библиотека Node.js ws:

  • Возбуждает событие "connection" когда браузер хочет подключиться. Обработчик события получает объект socket для общения с этим устройством. Он должен сохраняться на протяжении всего времени подключения.

  • Возбуждает событие socket "message" когда браузер отправляет сообщение. Обработчик события транслирует сообщение обратно каждому подключенному браузеру (включая тот, который его отправил).

  • Возбуждает событие socket "close" когда браузер отключается — обычно при закрытии или обновлении вкладки.

Обзор клиентского JavaScript кода

Файл приложения static/main.js выполняет функцию wsInit() и передает адрес сервера WebSocket (домен страницы плюс значение порта, определенное в шаблоне HTML страницы):

wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);

// обработка коммуникации WebSocket
function wsInit(wsServer) {

  const ws = new WebSocket(wsServer);

  // подключение к серверу
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

Событие open срабатывает, когда браузер подключается к серверу WebSocket. Обработчик функции отправляет сообщение вошел в чат-комнату путем вызова sendMessage():

// отправка сообщения
function sendMessage(setMsg) {

  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();

  name && msg && ws.send( JSON.stringify({ name, msg }) );

}

Функция sendMessage() извлекает имя пользователя и сообщение из HTML-формы, хотя сообщение может быть переопределено любым переданным аргументом setMsg. Значения преобразуются в объект JSON и отправляются на сервер WebSocket с помощью метода ws.send().

Сервер WebSocket получает входящее сообщение, что вызывает обработчик "message" (см. выше) и транслирует его обратно всем браузерам. Это вызывает событие "message" на каждом клиенте:

// получение сообщения
ws.addEventListener('message', e => {

  try {

    const
      chat = JSON.parse(e.data),
      name = document.createElement('div'),
      msg  = document.createElement('div');

    name.className = 'name';
    name.textContent = (chat.name || 'unknown');
    dom.chat.appendChild(name);

    msg.className = 'msg';
    msg.textContent = (chat.msg || 'said nothing');
    dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });

  }
  catch(err) {
    console.log('invalid JSON', err);
  }

});

Обработчик получает переданные данные JSON в свойстве .data объекта события. Функция парсит его в объект JavaScript и обновляет окно чата.

Наконец, новые сообщения отправляются с помощью функции sendMessage() каждый раз, когда срабатывает обработчик "submit" формы:

// form submit
dom.form.addEventListener('submit', e => {
  e.preventDefault();
  sendMessage();
  dom.message.value = '';
  dom.message.focus();
}, false);

Обработка ошибок

Событие "error" срабатывает, когда связь WebSocket терпит неудачу. Это можно обработать на сервере:

// обработка ошибки WebSocket на сервере
socket.on('error', e => {
  console.log('WebSocket error:', e);
});

и/или на клиенте:

// обработка ошибки WebSocket на клиенте
ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

Только клиент может восстановить соединение, выполнив конструктор new WebSocket() снова.

Закрытие соединений

Любое устройство может закрыть WebSocket в любое время, используя метод соединения .close(). Вы можете по желанию предоставить аргументы code целого числа и строку reason (максимум 123 байта), которые передаются другому устройству перед его отключением.

Расширенные WebSocket

Управление WebSocket легко в Node.js: одно устройство отправляет сообщение с помощью метода .send(), что вызывает событие "message" на другом устройстве. Как каждое устройство создает и реагирует на эти сообщения, может быть более сложным. В следующих разделах описываются вопросы, которые вам, возможно, потребуется учитывать.

Безопасность WebSocket

Протокол WebSocket не обрабатывает авторизацию или аутентификацию. Вы не можете гарантировать, что входящее запрос на связь поступает от браузера или от пользователя, вошедшего в ваше веб-приложение — особенно когда веб-сервер и сервер WebSocket могут быть на разных устройствах. Начальное соединение получает HTTP-заголовок, содержащий куки и сервер Origin, но возможно подделать эти значения.

Для ограничения WebSocket-коммуникаций только авторизованными пользователями следует использовать следующую технику:

  1. Перед отправкой первого запроса WebSocket, браузер связывается с веб-сервером HTTP (возможно, используя Ajax).

  2. Сервер проверяет учетные данные пользователя и возвращает новый авторизационный билет. Билет, как правило, ссылается на запись в базе данных, содержащую идентификатор пользователя, IP-адрес, время запроса, время окончания сессии и любые другие необходимые данные.

  3. Браузер передает билет серверу WebSocket в начальном соединении.

  4. Сервер WebSocket проверяет билет и проверяет такие факторы, как IP-адрес, время окончания срока действия и т. д., прежде чем разрешить соединение. Он выполняет метод WebSocket .close() при недействительном билете.

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

Важно, всегда проверять входящие данные:

  • Как и в случае с HTTP, сервер WebSocket подвержен атакам путем внедрения SQL и другим атакам.

  • Клиент никогда не должен вставлять необработанные значения в DOM или оценивать JavaScript-код.

Разделение vs множество экземпляров сервера WebSocket

Рассмотрим онлайн-игру с многопользовательским режимом. Игра имеет множество вселенных, в которых происходят отдельные игровые сессии: universeA, universeB и universeC. Игрок подключается к одной из вселенных:

  • universeA: присоединились player1, player2 и player3
  • universeB: присоединился player99

Можно реализовать следующее:

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. Использовать один сервер WebSocket для всех игровых вселенных.

    Это потребует меньше ресурсов и будет легче управлять, но серверу WebSocket придется отслеживать, к какой вселенной присоединяется каждый игрок. Когда player1 совершает действие, оно должно транслироваться player2 и player3, но не player99.

Несколько серверов WebSocket

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

Каждый сервер WebSocket может управлять только своими подключенными клиентами. Сообщение, отправленное из браузера на serverX, не может транслироваться тем, кто подключен к serverY. Возможно, потребуется реализовать систему публикации-подписки (pub-sub) на серверной стороне. Например:

  1. WebSocket serverX хочет отправить сообщение всем клиентам. Он публикует сообщение в системе pub-sub.

  2. Все серверы WebSocket, подписанные на систему pub-sub, получают событие нового сообщения (включая serverX). Каждый может обработать сообщение и транслировать его своим подключенным клиентам.

Эффективность сообщений WebSocket

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

  • Как синхронизировать действия игрока на всех клиентских устройствах?

  • Если player1 находится в другом месте, чем player2, нужно ли отправлять player2 информацию о действиях, которые он не видит?

  • Как справиться с сетевым лагом — или задержкой в коммуникации? Будет ли у кого-то с быстрым компьютером и подключением нечестное преимущество?

Быстрые игры должны идти на компромиссы. Представьте, что играете в игру на локальном устройстве, но некоторые объекты подвержены влиянию действий других. Вместо отправки точного положения каждого объекта постоянно, игры часто отправляют более простые, менее частые сообщения. Например:

  • objectX появился в точкеX
  • objectY изменил направление и скорость
  • objectZ уничтожен

Каждый клиентский игровой процесс заполняет пробелы. Когда objectZ взрывается, не имеет значения, будет ли взрыв выглядеть по-разному на каждом устройстве.

Вывод

Node.js облегчает работу с WebSockets. Это не обязательно упрощает проектирование или кодирование реальных приложений, но технология не будет вас сдерживать!

Основные недостатки:

  • WebSockets требуют отдельного экземпляра сервера. Запросы Ajax Fetch() и серверно-отправленные события могут быть обработаны веб-сервером, который вы уже используете.

  • Серверы WebSocket требуют собственных проверок безопасности и авторизации.

  • Упавшие соединения WebSocket должны быть восстановлены вручную.

Но не позволяйте этому отпугнуть вас!

Часто задаваемые вопросы (FAQ) о реальных приложениях с WebSockets и серверно-отправленными событиями

Как WebSockets отличаются от HTTP с точки зрения производительности и функциональности?

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

Можете ли вы объяснить жизненный цикл соединения WebSocket?

Жизненный цикл соединения WebSocket начинается с рукопожатия, которое обновляет HTTP-соединение до соединения WebSocket. После установления соединения данные могут передаваться вперед и назад между клиентом и сервером до тех пор, пока одна из сторон не решит закрыть соединение. Соединение может быть закрыто либо клиентом, либо сервером, отправляющим кадр закрытия, за которым следует подтверждение другой стороны о закрытии кадра.

Как я могу реализовать WebSockets в приложении для Android?

Реализация WebSockets в приложении для Android включает создание клиента WebSocket, который может подключаться к серверу WebSocket. Это можно сделать с помощью библиотек, таких как OkHttp или Scarlet. После настройки клиента вы можете открыть соединение с сервером, отправлять и принимать сообщения и обрабатывать различные события, такие как открытие соединения, получение сообщений и закрытие соединения.

Что такое Server-Sent Events и как они сравниваются с WebSockets?

Серверно-отправляемые события (SSE) — это стандарт, позволяющий серверу отправлять обновления клиенту через HTTP. В отличие от WebSockets, SSE являются однонаправленными, что означает, что они позволяют отправлять данные только от сервера к клиенту. Это делает их менее подходящими для приложений, требующих двустороннего общения, но они могут быть более простым и эффективным решением для приложений, которым нужно только получать обновления от сервера.

Каковы некоторые общие сценарии использования WebSockets и Server-Sent Events?

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

Как обработать соединения WebSocket в приложении Spring Boot?

Spring Boot предоставляет поддержку коммуникации через WebSocket с помощью модуля Spring WebSocket. Вы можете использовать аннотацию @EnableWebSocket для включения поддержки WebSocket, а затем определить WebSocketHandler для управления жизненным циклом соединения и обработки сообщений. Также можно использовать SimpMessagingTemplate для отправки сообщений подключенным клиентам.

Какие аспекты безопасности следует учитывать при использовании WebSockets?

Как и любая другая веб-технология, WebSockets могут быть уязвимы для различных угроз безопасности, таких как Cross-Site WebSocket Hijacking (CSWSH) и атаки Denial of Service (DoS). Чтобы минимизировать эти риски, вы должны всегда использовать безопасные соединения WebSocket (wss://) и проверять и очищать все входящие данные. Также рекомендуется использовать механизмы аутентификации и авторизации для контроля доступа к вашему серверу WebSocket.

Могу ли я использовать WebSockets с REST API?

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

Как я могу протестировать сервер WebSocket?

Существует несколько инструментов для тестирования серверов WebSocket, таких как Echo Test от WebSocket.org или Postman. Эти инструменты позволяют открыть соединение WebSocket с сервером, отправить сообщения и получить ответы. Вы также можете написать автоматические тесты для вашего сервера WebSocket, используя библиотеки, такие как Jest или Mocha.

Каковы ограничения WebSockets и Server-Sent Events?

Хотя WebSockets и Server-Sent Events предоставляют мощные возможности для реального времени коммуникации, у них также есть свои ограничения. Например, не все браузеры и сети поддерживают эти технологии, и они могут потреблять значительное количество ресурсов, если не управлять ими должным образом. Кроме того, их может быть сложнее реализовать и управлять по сравнению с традиционным обменом данными по HTTP.

Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/