За последние годы WebSockets стали стандартом де-факто для реализации приложений, работающих в режиме реального времени: чаты, биржевые терминалы, совместные редакторы документов. Но несмотря на популярность, у технологии есть свои недостатки, из-за которых WebSockets не всегда оправдывают возложенные ожидания.
Попробуем разобраться, почему иногда стоит отказаться от WebSockets в пользу более простых и проверенных решений, например HTTP-стриминга, и какие технические особенности стоят за таким выбором.
🔌 WebSockets: преимущества и «подводные камни»
Идея WebSockets выглядит привлекательно: устанавливаем одно постоянное двунаправленное соединение между сервером и клиентом и свободно обмениваемся сообщениями. Однако практика оказывается сложнее:
🚩 Отсутствие транзакционности сообщений
Основная проблема возникает в случаях, когда важно подтвердить, дошло ли сообщение до сервера и корректно ли оно было обработано. Например, если клиент отправляет команду уменьшить счётчик, и сервер отвечает ошибкой, то становится сложно понять, какое конкретно сообщение вызвало проблему. Для решения этого нужно добавлять специальные идентификаторы запроса (requestId) и усложнять клиентскую логику, которая должна помнить все отправленные сообщения.
🚩 Сложность управления состоянием соединения
Каждое WebSocket-соединение требует обработки множества событий: открытие, закрытие, ошибки. Если соединение обрывается, необходимо обеспечить переподключение, предусмотреть механизмы буферизации данных и повторных попыток отправки с экспоненциальной задержкой. В итоге простое приложение становится сложнее на порядок, а разработчик теряет в гибкости и простоте.
🚩 Усложнение серверного кода
WebSockets не просто устанавливаются и работают — для их использования сервер обязан обрабатывать специальный HTTP-запрос «Upgrade». Этот «рукопожатный» механизм требует дополнительных заголовков, проверки валидности запроса и управления состоянием, что заметно усложняет архитектуру.
🌊 HTTP Streaming как элегантная альтернатива
HTTP часто воспринимается исключительно как запрос-ответный протокол, где клиент отправляет запрос и сразу же получает ответ. Однако, HTTP прекрасно подходит для потоковой передачи данных. Так работают стриминговые платформы, например YouTube и Netflix. Но то же самое можно использовать и для простых realtime-приложений.
Рассмотрим пример реализации:
Сервер:
Используя асинхронные генераторы, сервер легко отправляет клиенту изменения состояния сразу же, как только они появляются:
async function* valueGenerator() {
while (true) {
yield await new Promise(resolve => resolvers.add(resolve));
}
}
app.get("/stream", (req, res) => {
const stream = ReadableStream.from(valueGenerator());
return new Response(stream);
});
Клиент:
На клиенте обработка также проста — браузер читает поток по мере поступления:
const response = await fetch("/stream");
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
updateUI(chunk);
}
Такой подход решает сразу несколько проблем:
- ✅ Транзакционность — HTTP-запрос на изменение состояния сразу возвращает ответ о его корректности.
- ✅ Проще обработка ошибок — если что-то идёт не так, сервер просто возвращает HTTP-статус ошибки.
- ✅ Простое управление жизненным циклом — клиент не беспокоится о переподключении и сложных событиях. Соединение закрыто — поток закончился.
🚀 Технологические детали и тонкости реализации
Используя HTTP-стриминг, важно учитывать некоторые технические моменты:
- 🌐 Поддержка на стороне клиента: Потоковый HTTP работает во всех современных браузерах через стандартный Fetch API и ReadableStream.
- 🧑💻 Асинхронные генераторы: Использование генераторов в JavaScript позволяет просто и элегантно реализовывать асинхронные потоки данных.
- 🎯 Лёгкость масштабирования: В отличие от WebSockets, HTTP-стриминг значительно проще масштабируется — для него подходят стандартные решения, такие как балансировщики нагрузки, прокси и CDN.
🔧 Библиотека Eventkit как удобный инструмент
Автор оригинальной статьи, Хантер Ловелл, представил собственную библиотеку — Eventkit, которая упрощает работу с асинхронными потоками и делает реализацию потоковых HTTP-серверов максимально простой.
Пример использования Eventkit на сервере:
import { Stream } from "eventkit";
let counter = 0;
const stateUpdates$ = new Stream();
stateUpdates$.subscribe(value => { counter = value });
app.post("/increment", async (req, res) => {
const { value } = await req.json();
stateUpdates$.push(counter + value);
return new Response("OK", 200);
});
app.get("/stream", (req, res) => {
const stream = ReadableStream.from(stateUpdates$);
return new Response(stream);
});
Это значительно сокращает объём кода и упрощает поддержку.
🔖 Заключение и личный взгляд автора
Как показывает практика, не стоит бросаться на каждую новую технологию лишь из-за её популярности. WebSockets отлично решают узкий круг задач, связанных с настоящей двунаправленной связью (например, интерактивные игры или высокочастотный трейдинг), но во многих приложениях достаточно куда более простых решений, таких как HTTP-стриминг.
Мне лично кажется, что для большинства типовых задач, особенно в веб-приложениях с не самым высоким трафиком, WebSockets — это избыточная сложность, которая только вредит проекту. Используя простые и надёжные подходы, такие как HTTP-стриминг, мы экономим время разработки, повышаем надёжность и уменьшаем количество ошибок.
🔗 Полезные ссылки:
- Оригинальная статья «You might not need WebSockets»
- MDN о WebSockets ссылка
Надеюсь, статья была полезной! 😊