Как поднять свой WebSocket сервер на Node.js: основы

e5ef2e0e2c60a142432765290aa1b2aa.jpg

Привет, Хабр!

Сегодня создадим свой WebSocket сервер на Node.js. Это тот самый протокол, который позволяет отправлять и принимать данные в реальном времени без перекладывания на HTTP. Для этого мы будем использовать библиотеку ws.

Подготовка окружения

Первый шаг — установить Node.js, если его еще нет. Ну, а если есть, проверим версии:

node -v
npm -v

Все на месте? Отлично. Теперь создадим папку для проекта:

mkdir websocket-server
cd websocket-server
npm init -y

Тут сразу заложим фундамент проекта. npm init -y создает стандартный package.json. Но не забудь туда добавить "type": "module", чтобы сразу подключать все по человески, без всяких require:

{
  "type": "module",
  "dependencies": {
    "ws": "^8.12.0"
  }
}

Теперь пришло время установить библиотеку для работы с WebSocket — ws:

npm install ws

Это некий золотой стандарт среди библиотек для WebSocket на Node.js.

Создание базового WebSocket сервера

Окей, есть инструменты, пора за работу! В файле server.js создадим сервер WebSocket, который уже готов принять соединение. Минимум кода:

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('Новый клиент подключился!');

  ws.on('message', (message) => {
    console.log(`Получено сообщение: ${message}`);
    ws.send('Сообщение получено!');
  });

  ws.on('close', () => {
    console.log('Клиент отключился');
  });
});

Разберем. Мы создали сервер на порту 8080. Как только клиент подключается, мы сообщаем об этом в консоль. Когда он присылает сообщение, сервер отвечает:»Сообщение получено! ». А когда клиент отключается, сервер говорит об этом. Запустим его:

node server.js

Теперь можно подключиться к серверу через браузер, Postman или вообще любой клиент WebSocket.

Обработка сообщений и работа с несколькими клиентами

Теперь научим сервер работать с несколькими клиентами одновременно и обрабатывать не только текст, но и бинарные данные.

Обработка бинарных данных

Чтобы обрабатывать бинарные данные, нужно убедиться, что сервер может различать текст и двоичные сообщения:

ws.on('message', (data, isBinary) => {
  if (isBinary) {
    console.log('Получены бинарные данные');
  } else {
    console.log(`Получено сообщение: ${data}`);
  }
  ws.send(data); // Отправляем обратно клиенту то, что получили
});

Теперь сервер умеет отличать текстовые сообщения от бинарных и логирует их соответственно.

Множество клиентов

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

const clients = [];

wss.on('connection', (ws) => {
  clients.push(ws);
  
  ws.on('message', (message) => {
    clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });

  ws.on('close', () => {
    clients.splice(clients.indexOf(ws), 1); // Убираем клиента из списка при отключении
  });
});

Теперь, когда один клиент отправляет сообщение, оно транслируется всем остальным подключенным клиентам, кроме самого отправителя.

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

Нельзя говорить о серверной архитектуре, не затронув безопасность. WebSocket не исключение. Самый простой способ защиты — ограничить число подключений с одного IP-адреса и установить таймауты.

Ограничение числа подключений

Если хочешь избежать атак типа DoS, придется ограничить количество соединений с одного IP. Пример, как это сделать:

const ipConnections = new Map();

wss.on('connection', (ws, req) => {
  const ip = req.socket.remoteAddress;
  
  if (ipConnections.get(ip) >= 5) {
    ws.close();
    return;
  }
  
  ipConnections.set(ip, (ipConnections.get(ip) || 0) + 1);

  ws.on('close', () => {
    ipConnections.set(ip, ipConnections.get(ip) - 1);
  });
});

Теперь сервер будет автоматически разрывать соединение, если с одного IP слишком много подключений.

Зашифрованные соединения

Да, да — WebSocket через SSL. Важно, чтобы данные между сервером и клиентом были зашифрованы, особенно если это чувствительная информация. Чтобы перейти на wss://, нужно настроить SSL:

import { createServer } from 'https';
import { readFileSync } from 'fs';
import { WebSocketServer } from 'ws';

const server = createServer({
  cert: readFileSync('path/to/cert.pem'),
  key: readFileSync('path/to/key.pem')
});

const wss = new WebSocketServer({ server });

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    console.log(`Received: ${message}`);
  });
  ws.send('Secure connection established!');
});

server.listen(8080);

Теперь сервер использует защищенные соединения.

Оптимизация и масштабирование

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

Широковещательные сообщения

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

wss.on('message', (message) => {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
});

Масштабирование с Redis

Можно синхронизировать несколько серверов через Redis Pub/Sub, чтобы сообщения отправлялись на все экземпляры WebSocket сервера:

import Redis from 'ioredis';

const redis = new Redis();
const sub = new Redis();

sub.subscribe('messages');
sub.on('message', (channel, message) => {
  wss.clients.forEach((client) => {
    client.send(message);
  });
});

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    redis.publish('messages', message);
  });
});

Таким образом, клиенты, подключенные к разным серверам, могут обмениваться сообщениями через Redis.

Заключение

WebSocket — это невероятно мощный инструмент для создания real-time приложений, но, как и в любом деле, чтобы стать мастером, нужно идти дальше. Так что, если вам показалось, что этого мало (а это точно так), вот вам несколько идей, чем можно заняться дальше:

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

  2. Работа с WebSocket в продакшене — изучи, как лучше мониторить и перезагружать WebSocket серверы, например, с помощью PM2 или Kubernetes.

  3. Веб-сокеты и микросервисы — попробуй организовать обмен данными через WebSocket в архитектуре микросервисов, когда один сервис передает информацию другим.

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

А еще приходите на бесплатный захватывающий вебинар посвященный изучению мощного фреймворка разработки веб-приложений Nest.Js. Зарегистрироваться.

© Habrahabr.ru