Real-Time Backend
uWebSockets.js
15 миллионов WebSocket сообщений в секунду на одном ядре против 300 тысяч у стандартного ws. Разница в 50 раз - не маркетинговый слайд, а benchmark на одном CPU. uWebSockets.js достигает этого не хитрым алгоритмом, а выходом за пределы JavaScript.
- **Онлайн-игры:** realtime мультиплеер с 10000+ игроками требует рассылки состояния 60 раз в секунду. На ws это ~18M сообщений/сек, что невозможно на одном процессе. uWS справляется благодаря C++ pub/sub и cork.
- **Финтех:** trading платформы отправляют котировки тысячам клиентов с задержкой менее 1ms. Zero-copy и минимальные аллокации снижают tail latency - именно то, что критично для трейдинга.
- **Умные дома:** IoT hub с тысячами устройств. Postman использует uWS в своей платформе Postbot; Socket.IO в режиме высокой нагрузки рекомендует uWS как транспорт.
uWebSockets.js: когда ws недостаточно быстр
Создатель uWebSockets.js Алекс Хулин однажды опубликовал benchmark: его библиотека обрабатывает 15 миллионов сообщений в секунду на одном ядре, тогда как стандартный `ws` упирается в 300 тысяч. Разница в 50 раз достигается без магии - только через архитектурные решения: C++ ядро (libuv + OpenSSL), zero-copy где возможно, отказ от лишних аллокаций. uWebSockets.js (uWS) оборачивает этот C++ движок тонким слоем Node.js bindings. Это не pure JavaScript - это нативный модуль, который использует N-API. Следствие: бинарники платформо-специфичны и требуют правильной версии glibc (Ubuntu 24.04 vs Alpine).
uWS совмещает HTTP и WebSocket сервер в одном event loop, в отличие от Express + ws, где HTTP и WS - разные слои с дополнительными накладными расходами. Порт принимает как HTTP upgrade запросы, так и обычный HTTP. Это критично для production: один процесс, один порт, максимальная утилизация ресурсов.
Почему uWebSockets.js значительно быстрее pure-JavaScript реализаций WebSocket?
Производительность: zero-copy и memory model
В обработчике `message` uWS передаёт `ArrayBuffer` - это прямой указатель на внутренний буфер C++ ядра. Buffer жив только во время вызова обработчика. Если данные нужны после - необходимо явное копирование через `Buffer.from(message).slice()` или `message.slice(0)`. Это zero-copy: JS-код читает данные без аллокации новой памяти. Разработчики из ws-мира часто получают пустой буфер, потому что сохраняют ссылку на `message` без копирования.
uWS поддерживает pub/sub на уровне C++ через механизм топиков. `ws.subscribe('room:42')` и `app.publish('room:42', data)` работают без O(n) перебора подписчиков в JS. Внутренний индекс на C++ стороне. Это особенно важно для fan-out к тысячам клиентов: broadcast в uWS быстрее broadcast через Set.forEach.
Почему нельзя сохранить ссылку на `message` в обработчике uWS для использования в setTimeout?
Backpressure: управление перегрузкой
Когда сервер отправляет данные быстрее, чем клиент успевает их принять, буфер TCP растёт. uWS не скрывает эту проблему, как делает большинство библиотек - он явно сигнализирует backpressure. Метод `ws.send()` возвращает значение: 0 (BACKPRESSURE - буфер заполнен), 1 (SUCCESS - отправлено или буферизовано), 2 (DROPPED - соединение закрыто). Обработчик `drain` вызывается, когда буфер освободился. Игнорирование backpressure приводит к неограниченному росту памяти и OOM.
Метод `ws.getBufferedAmount()` возвращает байты в очереди на отправку. Если значение растёт без остановки - клиент не успевает читать. Стратегии: throttle отправки, закрыть соединение при превышении порога, перевести клиента в slow-path очередь. Многие realtime игры используют последний подход: медленным клиентам отправляются снапшоты реже.
Что означает возвращаемое значение 0 (BACKPRESSURE) метода ws.send() в uWebSockets.js?
Pub/Sub и cork: батчинг на уровне ядра
uWS реализует pub/sub на уровне C++ без перебора подписчиков в JS. `app.publish('topic', data)` вызывает один C++ метод, который итерирует связный список подписчиков и записывает в их сокеты через writev() - один системный вызов на клиента. Никакого JavaScript в горячем пути. `ws.cork(callback)` - аналог Nagle's algorithm для приложений: буферизует все send() внутри callback и отправляет одним TCP сегментом. Без cork каждый send() - потенциально отдельный системный вызов.
Топики в uWS - иерархические через wildcard: subscribe('room:#') подписывает на все топики вида 'room:123', 'room:456'. publish('room:42', data) доставляет только тем, кто подписан на точный топик или wildcard. Это позволяет строить broadcast-иерархии без дополнительного слоя роутинга.
uWebSockets.js можно использовать как прямую замену библиотеке ws без изменений в коде
API uWS существенно отличается: message приходит как ArrayBuffer (не Buffer/string), отсутствует EventEmitter интерфейс, backpressure API обязателен
uWS намеренно выбрал отличный от ws API для достижения максимальной производительности. Это не wrapper над ws, а независимая реализация с другими trade-offs. Миграция требует переписывания обработчиков.
Зачем использовать ws.cork() при отправке нескольких сообщений подряд?
Ключевые идеи
- **Zero-copy message**: `message` в обработчике - ArrayBuffer на C++ память, валидный только внутри обработчика. Для сохранения нужен `Buffer.from(message).slice()`.
- **Backpressure API**: `ws.send()` возвращает 0/1/2. Значение 0 (BACKPRESSURE) требует паузы отправки до события `drain`. Игнорирование ведёт к OOM.
- **Pub/Sub и cork**: топики обрабатываются C++ ядром без JS-перебора. `ws.cork()` батчит несколько send() в один системный вызов - критично при высокочастотной отправке.
Связанные темы
uWebSockets.js - альтернатива ws при высокой нагрузке:
- ws (Node.js) — Стандартная альтернатива с более простым API и pure-JavaScript реализацией. Выбор между uWS и ws зависит от требований к throughput
- Backpressure и flow control — Backpressure в WebSocket - частный случай общей проблемы управления потоком данных в realtime системах
Вопросы для размышления
- uWS передаёт `message` как ArrayBuffer на C++ память. Какие паттерны кода в production могут привести к чтению освобождённой памяти, и как их обнаружить?
- Pub/sub в uWS работает без JS-слоя, но ограничен одним процессом. Как масштабировать pub/sub на несколько Node.js процессов, и где тут место Redis?
- cork() батчит несколько send() в один syscall. В каких сценариях cork() может навредить latency, даже если он улучшает throughput?
Связанные уроки
- rt-05 — uWebSockets.js is a bare-metal WebSocket server; protocol knowledge is mandatory
- rt-09 — Socket.IO is the abstracted alternative; comparing them makes the uWebSockets.js tradeoffs concrete
- rt-08 — uWebSockets.js is where binary serialization pays off - binary frames are first-class
- rt-12 — ws vs uWebSockets.js: pure JS vs C++ binding - same goal, different performance ceiling
- net-15-tcp-basics