WebSocket vs SSE vs Long Polling: выбираем real-time транспорт для веб-приложения
HTTP создавался для request-response: клиент спрашивает, сервер отвечает. Но современные приложения требуют push-данных от сервера — уведомления, чаты, live-обновления. Три основных подхода: Long Polling, Server-Sent Events и WebSocket. Каждый со своими trade-offs.
Long Polling: старый добрый hack
Клиент отправляет запрос, сервер держит соединение открытым до появления данных (или таймаута). Получив ответ, клиент немедленно отправляет новый запрос:
// Клиент: рекурсивный long polling
async function poll() {
try {
const res = await fetch('/api/events?timeout=30000');
const data = await res.json();
handleEvent(data);
} catch (e) {
await sleep(1000); // backoff при ошибке
}
poll(); // сразу следующий запрос
}
// Сервер: держим запрос до события
app.get('/api/events', async (req, res) => {
const timeout = Number(req.query.timeout) || 30000;
const event = await waitForEvent(req.user.id, timeout);
if (event) {
res.json(event);
} else {
res.status(204).end(); // таймаут, нет данных
}
});
Overhead: каждый цикл — полный HTTP-запрос с заголовками (1-2 KB). При высокой частоте событий — десятки запросов в секунду на клиента. Когда актуален: legacy-системы без WebSocket-поддержки, корпоративные прокси, которые режут upgrade.
SSE: однонаправленный поток
Server-Sent Events — стандартный механизм push от сервера. Одно HTTP-соединение, данные текут в формате text/event-stream:
// Сервер: SSE endpoint
app.get('/api/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Отправка события
function send(event: string, data: unknown) {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
// Подписка на события
const unsub = eventBus.subscribe(req.user.id, (event) => {
send(event.type, event.payload);
});
// Heartbeat каждые 30 сек (keep-alive через прокси)
const heartbeat = setInterval(() => res.write(': ping\n\n'), 30000);
req.on('close', () => {
unsub();
clearInterval(heartbeat);
});
});
// Клиент: EventSource API (встроен в браузер)
const source = new EventSource('/api/stream');
source.addEventListener('notification', (e) => {
const data = JSON.parse(e.data);
showNotification(data);
});
// Автоматический reconnect при обрыве!
Преимущества SSE: автоматический reconnect с Last-Event-ID, работает через HTTP/2 мультиплексирование, не требует специальных библиотек на клиенте. Ограничение: только сервер → клиент.
WebSocket: полный дуплекс
WebSocket начинается как HTTP Upgrade, затем переключается на бинарный фреймовый протокол. Оба направления одновременно:
// Сервер: WebSocket с ws
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server: httpServer });
wss.on('connection', (ws, req) => {
const userId = authenticate(req);
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.type === 'chat') {
broadcastToRoom(msg.roomId, {
type: 'chat',
from: userId,
text: msg.text,
ts: Date.now()
});
}
});
ws.on('close', () => removeFromRooms(userId));
// Ping/pong для обнаружения мёртвых соединений
const pingInterval = setInterval(() => {
if (ws.readyState === ws.OPEN) ws.ping();
}, 30000);
ws.on('close', () => clearInterval(pingInterval));
});
// Клиент
const ws = new WebSocket('wss://api.example.com/ws');
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
renderMessage(msg);
};
ws.send(JSON.stringify({ type: 'chat', roomId: '1', text: 'Hello' }));
Сравнение: latency, масштабирование, совместимость
+-------------------+---------------+----------------+----------------+
| Критерий | Long Polling | SSE | WebSocket |
+-------------------+---------------+----------------+----------------+
| Направление | server→client | server→client | bidirectional |
| Latency | высокая | низкая | минимальная |
| Overhead | высокий | низкий | минимальный |
| Reconnect | ручной | автоматический | ручной |
| HTTP/2 multiplex | нет | да | нет (отд. TCP) |
| CDN/proxy | отлично | хорошо | проблемы |
| Макс. соединений | 6 per domain | 6 (HTTP/1.1) | без лимита |
| Бинарные данные | нет | нет | да |
+-------------------+---------------+----------------+----------------+
Масштабирование: SSE и WebSocket держат постоянные соединения. На одном сервере — десятки тысяч. Но при горизонтальном масштабировании нужен sticky sessions или pub/sub (Redis) для broadcast между инстансами.
Когда что выбрать
- Notifications, live feed, stock prices → SSE. Однонаправленный поток, автоматический reconnect, работает через CDN
- Chat, collaborative editing, gaming → WebSocket. Нужен двусторонний обмен с минимальной задержкой
- Legacy, корпоративные прокси → Long Polling. Работает везде, где работает HTTP
- Редкие обновления (раз в минуту) → обычный polling с интервалом. Не усложняйте
Частая ошибка — использовать WebSocket для уведомлений. SSE проще, автоматически переподключается и работает через HTTP/2. WebSocket оправдан только когда клиент активно отправляет данные серверу.