Real-Time Backend

ws (Node.js)

120 миллионов загрузок в неделю. ws - это не просто популярная библиотека, это инфраструктурный элемент Node.js экосистемы. Socket.IO, Jest, Storybook, тысячи инструментов используют её как фундамент. Понять ws - значит понять как работает WebSocket на уровне протокола.

  • **Socket.IO:** использует ws как WebSocket транспорт. Понимание ws объясняет поведение Socket.IO при отладке низкоуровневых проблем с соединением.
  • **Chatbot платформы:** Slack, Discord используют WebSocket для real-time доставки сообщений. ws - стандартный выбор для Node.js backend этих систем.
  • **Dev tools:** webpack-dev-server, Vite, Next.js используют ws для Hot Module Replacement. Каждый раз, когда файл сохраняется и браузер перезагружает модуль - ws доставляет обновление.

ws: де-факто стандарт WebSocket для Node.js

Библиотека `ws` - самый скачиваемый WebSocket пакет для Node.js: более 120 миллионов загрузок в неделю на npm. Socket.IO использует её как транспорт. Jest использует её для WebSocket-тестов. Это pure JavaScript реализация RFC 6455 без нативных зависимостей - главное преимущество перед uWebSockets.js при работе в средах без стабильной glibc. API максимально близок к браузерному WebSocket API: `new WebSocket(url)` на клиенте, `new WebSocketServer()` на сервере. EventEmitter интерфейс делает ws знакомым любому Node.js разработчику.

ws не включает HTTP сервер - его нужно подключать к существующему. Это гибкость: ws можно встроить в Express, Fastify, http.createServer или использовать standalone. WebSocketServer можно передать `noServer: true` для ручного управления upgrade, или `server: httpServer` для автоматического.

Что нужно сделать для интеграции ws в существующий Express сервер?

WebSocket как Node.js Stream

ws поддерживает интерфейс Node.js Streams: `ws.createWebSocketStream(socket)` возвращает Duplex stream. Это даёт доступ к stream composition: pipe(), pipeline(), Transform streams. Можно подключить парсер прямо к WebSocket соединению без промежуточных буферов. Это особенно ценно при работе с бинарными протоколами поверх WebSocket: msgpack, protobuf, custom binary framing.

По умолчанию ws получает сообщения как Buffer для бинарных данных и string для текстовых. Это отличается от браузера, где всё - это либо string, либо Blob/ArrayBuffer. В Node.js backend лучше работать с Buffer: это позволяет использовать pool'd buffers и избегать лишних преобразований. Опция `binary: true` в ws.send() явно указывает тип фрейма.

Какое преимущество даёт createWebSocketStream() по сравнению с прямым использованием событий ws?

permessage-deflate: сжатие на уровне протокола

permessage-deflate (RFC 7692) - расширение WebSocket для сжатия каждого сообщения через DEFLATE. ws поддерживает его из коробки. Для JSON payload с повторяющимися ключами сжатие даёт 60-80% экономии трафика. Но у сжатия есть цена: CPU на каждое сообщение, память для LZ77 sliding window (до 32KB на соединение), и latency на сжатие/распаковку. При миллионе соединений 32KB на окно - это 32GB RAM только под deflate контексты.

Опция `perMessageDeflate.serverNoContextTakeover: true` заставляет сервер сбрасывать контекст DEFLATE после каждого сообщения. Это снижает степень сжатия, но устраняет проблему с памятью: окно не накапливается. `clientNoContextTakeover: true` то же для клиента. Компромисс: память vs эффективность сжатия.

Почему опция serverNoContextTakeover важна при большом числе WebSocket соединений?

Бинарные данные и ping/pong keepalive

WebSocket поддерживает бинарные фреймы наравне с текстовыми. В ws бинарные данные приходят как Buffer. Для production систем важно два момента: правильный выбор между бинарным и текстовым форматом (msgpack vs JSON), и keepalive через ping/pong. Многие NAT и load balancer закрывают idle WebSocket соединения через 30-60 секунд. Ping/pong - стандартный механизм keepalive: сервер отправляет ping, браузер автоматически отвечает pong. ws требует реализовать это вручную.

Heartbeat - стандартный паттерн в ws. Каждые N секунд сервер проверяет, что клиент живой. Если pong не пришёл за следующий цикл - соединение считается мёртвым и закрывается. Это предотвращает накопление zombie-соединений, которые потребляют filehandles и память без реальных клиентов на другом конце.

ws автоматически поддерживает соединение живым и обнаруживает разрывы без дополнительного кода

ws не реализует keepalive по умолчанию. Ping/pong heartbeat и обнаружение zombie-соединений - ответственность разработчика

RFC 6455 определяет ping/pong фреймы как опциональный механизм. ws реализует их на уровне протокола (ws.ping(), событие pong), но не запускает heartbeat таймер автоматически. Это осознанный выбор: разные приложения имеют разные требования к частоте и логике keepalive.

Зачем в heartbeat паттерне устанавливать ws.isAlive = false перед ws.ping()?

Ключевые идеи

  • **Гибкая интеграция:** ws подключается к любому HTTP серверу через { server } или noServer + ручной upgrade. Это позволяет добавить WebSocket в Express/Fastify без изменения архитектуры.
  • **Stream API:** createWebSocketStream() превращает WebSocket в Duplex stream для pipe/pipeline композиции с Transform, zlib, криптографическими потоками.
  • **Heartbeat обязателен:** ws не управляет keepalive автоматически. Паттерн isAlive + ping/pong + terminate() - стандартное решение для обнаружения и очистки zombie-соединений.

Связанные темы

ws - базовый уровень для понимания более сложных WebSocket решений:

  • uWebSockets.js — Высокопроизводительная альтернатива ws на C++ для сценариев с требованиями 100k+ соединений или >1M msg/s
  • Socket.IO — Надстройка над ws с автопереподключением, комнатами и fallback на long polling. Использует ws как транспорт

Вопросы для размышления

  • ws позволяет использовать noServer: true для ручного upgrade. В каких сценариях это предпочтительнее автоматического upgrade через { server }, и что можно делать в обработчике upgrade до handleUpgrade?
  • permessage-deflate с serverNoContextTakeover снижает эффективность сжатия. Для каких типов payload (чат, котировки, IoT sensor data) это trade-off оправдан, а для каких нет?
  • Heartbeat использует ws.terminate() для принудительного закрытия. Чем terminate() отличается от ws.close(), и почему для мёртвых соединений нужен именно terminate()?

Связанные уроки

  • rt-05 — ws is the canonical Node.js WebSocket implementation; protocol knowledge from rt-05 is prerequisite
  • rt-11 — uWebSockets.js is the high-performance C++ alternative to ws; knowing both defines the JS WebSocket landscape
  • rt-09 — Socket.IO vs ws: high-level framework vs low-level library
  • rt-07 — The first real-time app lesson likely uses ws under the hood; this lesson deepens that foundation
  • net-36-websocket
ws (Node.js)

0

1

Войти