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