WebSocket на JavaScript: продвинутые real-time приложения

Онлайн-тренажер Python для начинающих

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

Начать курс

Введение: за пределами базового WebSocket

Базовое понимание WebSocket позволяет создать простой чат или уведомления. Однако для построения production-grade real-time приложений — многопользовательских игр, финансовых терминалов, систем совместного редактирования — необходимо освоить продвинутые техники. В этой статье мы выйдем за рамки "Hello, World" и разберем, как создавать надежные, масштабируемые и безопасные WebSocket-приложения на Node.js.

Мы рассмотрим четыре ключевые области: масштабирование с помощью Redis, управление состояниями и комнатами, безопасность и авторизацию, а также кластеризацию для работы на нескольких процессорах. Все примеры кода написаны на JavaScript с использованием библиотеки ws и Node.js.

1. Масштабирование с помощью Pub/Sub (Redis)

Одно из главных ограничений WebSocket-сервера — он не может "общаться" с другими экземплярами. Если у вас запущено два сервера за балансировщиком нагрузки, клиент, подключенный к серверу A, не получит сообщение, отправленное через сервер B. Решение — использование паттерна Pub/Sub (издатель-подписчик) на базе Redis.

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

const WebSocket = require('ws');
const redis = require('redis');

const wss = new WebSocket.Server({ port: 8080 });
const publisher = redis.createClient();
const subscriber = redis.createClient();

// Подписываемся на канал 'broadcast'
subscriber.subscribe('broadcast');
subscriber.on('message', (channel, message) => {
  // Рассылаем сообщение всем подключенным клиентам
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
});

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    // Публикуем сообщение в Redis
    publisher.publish('broadcast', data.toString());
  });
});

Этот подход позволяет горизонтально масштабировать ваше приложение практически бесконечно. При добавлении нового сервера он просто создает свою подписку на Redis.

2. Управление комнатами и состояниями

В большинстве real-time приложений необходимо группировать клиентов по "комнатам" (чат-комнаты, игровые лобби) и управлять их состоянием. Базовый wss.clients не предоставляет такой функциональности. Реализуем простую, но эффективную систему комнат.

Мы создадим объект rooms, где ключ — имя комнаты, а значение — Set из WebSocket-клиентов. Добавим методы для входа, выхода и рассылки внутри комнаты.

const rooms = new Map();

function joinRoom(ws, roomName) {
  if (!rooms.has(roomName)) {
    rooms.set(roomName, new Set());
  }
  rooms.get(roomName).add(ws);
  // Сохраняем комнату на объекте ws для быстрого доступа
  ws.room = roomName;
}

function leaveRoom(ws) {
  if (ws.room && rooms.has(ws.room)) {
    rooms.get(ws.room).delete(ws);
    if (rooms.get(ws.room).size === 0) {
      rooms.delete(ws.room);
    }
    delete ws.room;
  }
}

function broadcastToRoom(roomName, message, exclude = null) {
  if (!rooms.has(roomName)) return;
  rooms.get(roomName).forEach(client => {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    switch (msg.type) {
      case 'join':
        joinRoom(ws, msg.room);
        break;
      case 'message':
        broadcastToRoom(ws.room, msg.text, ws);
        break;
    }
  });
  ws.on('close', () => leaveRoom(ws));
});

Для управления состоянием (например, позиция игрока, курсор) можно хранить данные в Map по ключу ws или по userId.

3. Безопасность и авторизация

WebSocket-соединения не должны быть открыты для всех. Критически важно проверять подлинность клиента при установке соединения и в процессе работы. Рассмотрим два подхода: токен при подключении и верификация сообщений.

Подход 1: Токен в URL. Клиент передает JWT-токен через query-параметр при установке соединения.

const jwt = require('jsonwebtoken');
const wss = new WebSocket.Server({ 
  port: 8080,
  verifyClient: (info, cb) => {
    const token = new URL(info.req.url, 'http://localhost').searchParams.get('token');
    if (!token) {
      cb(false, 401, 'Unauthorized');
      return;
    }
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      info.req.user = decoded; // Передаем данные пользователя дальше
      cb(true);
    } catch (err) {
      cb(false, 401, 'Invalid token');
    }
  }
});

wss.on('connection', (ws, req) => {
  console.log('User connected:', req.user.id);
});

Подход 2: Верификация внутри сообщений. Если токен не может быть передан в URL (например, из-за прокси), используйте первое сообщение для аутентификации.

wss.on('connection', (ws) => {
  let authenticated = false;
  ws.on('message', (data) => {
    if (!authenticated) {
      const msg = JSON.parse(data);
      if (msg.type === 'auth') {
        try {
          const decoded = jwt.verify(msg.token, process.env.JWT_SECRET);
          ws.userId = decoded.id;
          authenticated = true;
          ws.send(JSON.stringify({ type: 'auth_ok' }));
        } catch {
          ws.close(4001, 'Auth failed');
        }
      }
      return;
    }
    // Обработка обычных сообщений
  });
});

Не забывайте про rate limiting и валидацию входящих данных — это защитит от спама и DoS-атак.

4. Кластеризация и использование всех ядер CPU

Node.js работает в одном потоке. Для полной утилизации многоядерных процессоров используйте модуль cluster. Каждый воркер запускает свой WebSocket-сервер на одном порту (через общий мастер-процесс). Сочетание кластеризации с Redis Pub/Sub (из раздела 1) дает максимальную производительность.

const cluster = require('cluster');
const os = require('os');

if (cluster.isMaster) {
  const numWorkers = os.cpus().length;
  console.log                

Блоги

Рекомендаций по книгам