Scaling WebSockets

Фреймворк Fresh на базе Deno предлагает лёгкий подход к работе с WebSocket. WebSocket в Fresh реализуется через стандартный API Deno, что позволяет устанавливать двунаправленное соединение между клиентом и сервером с минимальной задержкой. Основное преимущество такого подхода — нативная интеграция с серверной частью без необходимости в дополнительных библиотеках или сложной конфигурации.

Соединение WebSocket создаётся в обработчике запроса. Важно использовать метод Deno.upgradeWebSocket(req), который возвращает объект с потоками socket и response. Пример базового подключения:

import { serve } from "https://deno.land/std/http/server.ts";

serve(async (req) => {
  const { socket, response } = Deno.upgradeWebSocket(req);

  socket.ono pen = () => {
    console.log("WebSocket соединение установлено");
  };

  socket.onmess age = (event) => {
    socket.send(`Сервер получил: ${event.data}`);
  };

  socket.oncl ose = () => {
    console.log("Соединение закрыто");
  };

  return response;
});

socket.send() используется для отправки данных на клиент, а события onmessage, onopen и onclose позволяют управлять состоянием соединения.


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

При увеличении количества клиентов простое однопоточное решение быстро становится узким местом. Для масштабирования WebSocket важно учитывать несколько аспектов:

  1. Горизонтальное масштабирование — запуск нескольких инстансов приложения на разных серверах или контейнерах. Необходимо синхронизировать состояние соединений между экземплярами.

  2. Использование брокеров сообщений — Redis, NATS или Kafka позволяют обмениваться событиями между разными инстансами приложения. Каждый сервер обрабатывает свои соединения, но события реплицируются через брокер.

  3. Балансировка нагрузки — использование прокси типа Nginx или Envoy для распределения WebSocket-запросов между инстансами. Важно включать sticky sessions или идентификацию клиентов по токену, чтобы соединение оставалось на одном сервере.

Пример интеграции Redis Pub/Sub для синхронизации сообщений между инстансами:

import { connect } from "https://deno.land/x/redis/mod.ts";

const redis = await connect({ hostname: "127.0.0.1", port: 6379 });
const pub = redis.publish;
const sub = await redis.subscribe("messages");

sub.on("message", (channel, message) => {
  // Рассылка сообщения всем локальным клиентам
  for (const socket of connectedSockets) {
    socket.send(message);
  }
});

Управление состоянием соединений

Для масштабируемых приложений важно хранить активные соединения в структуре данных, которая позволяет быстро добавлять и удалять клиентов. В Fresh это обычно реализуется через Map:

const connectedSockets = new Map();

socket.ono pen = () => {
  connectedSockets.set(socket, { id: clientId });
};

socket.oncl ose = () => {
  connectedSockets.delete(socket);
};

Использование Map обеспечивает быструю итерацию и отправку сообщений группам клиентов.


Паттерны рассылки сообщений

Для эффективной работы с большим числом соединений применяются различные паттерны:

  • Broadcast — отправка сообщения всем клиентам.
  • Room / Channel — объединение клиентов в группы для целевой рассылки.
  • Direct message — адресная отправка конкретному пользователю.

Пример организации комнат:

const rooms = new Map();

function joinRoom(socket, room) {
  if (!rooms.has(room)) rooms.set(room, new Set());
  rooms.get(room).add(socket);
}

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

function broadcastRoom(room, message) {
  if (rooms.has(room)) {
    for (const socket of rooms.get(room)) {
      socket.send(message);
    }
  }
}

Обработка сбоев и переподключений

При масштабировании WebSocket критически важна устойчивость к сбоям:

  • Отслеживание статуса соединения через onclose и onerror.
  • Имплементация механизмов переподключения на клиенте с экспоненциальной задержкой.
  • Очереди сообщений на сервере для гарантированной доставки при временной недоступности клиента.

Пример простого переподключения на клиенте:

let ws;

function connect() {
  ws = new WebSocket("ws://localhost:8000");
  ws.ono pen = () => console.log("Соединение установлено");
  ws.oncl ose = () => setTimeout(connect, 1000);
}

connect();

Оптимизация производительности

  • Использование бинарных форматов (например, MessagePack) для передачи больших объёмов данных.
  • Сжатие сообщений через gzip или Brotli.
  • Пул соединений и лимитирование частоты сообщений для предотвращения перегрузки сервера.

Для крупномасштабных приложений сочетание этих методов обеспечивает надёжное и масштабируемое управление WebSocket-соединениями в Fresh.