Разработка Server-Sent Events (SSE) для веб-приложения
Server-Sent Events (SSE) — технология для отправки событий от сервера к клиенту через HTTP-соединение. Однонаправленная: сервер → клиент. Проще WebSocket для задач, где не нужна двунаправленная связь: уведомления, прогресс длинных операций, realtime-ленты.
Преимущества SSE перед WebSocket
- HTTP/1.1 совместимость — SSE работает через обычный HTTP, без upgrade
-
Автоматическое переподключение — браузер сам переподключается при обрыве (через
retryполе) - Стандартные заголовки — Authorization, cookies работают без танцев с бубном
- Proxy-friendly — обычный HTTP, меньше проблем с корпоративными прокси
- Не требует CORS preflight для GET-запросов
Формат SSE
Ответ сервера — text/event-stream с полями data:, event:, id:, retry::
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"type":"notification","message":"Новый заказ"}
event: order_update
data: {"orderId":"123","status":"shipped"}
id: msg_456
retry: 3000
: comment (игнорируется клиентом)
Реализация сервера (Node.js + Express)
app.get('/api/events', (req, res) => {
const userId = req.user.id;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // для Nginx: отключить буферизацию
});
// Немедленно отправляем первый пакет (обход nginx буферизации)
res.write(':ok\n\n');
// Добавляем клиента в реестр
const clientId = nanoid();
clients.set(clientId, { res, userId });
// Heartbeat каждые 15 секунд
const heartbeat = setInterval(() => {
res.write(': ping\n\n');
}, 15000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(clientId);
});
});
// Отправка события конкретному пользователю
function sendToUser(userId: string, event: string, data: object) {
clients.forEach(({ res, userId: uid }) => {
if (uid === userId) {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
});
}
// Broadcast всем
function broadcast(event: string, data: object) {
clients.forEach(({ res }) => {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
}
Клиент (браузер)
const eventSource = new EventSource('/api/events', { withCredentials: true });
// Дефолтные события (event: без имени)
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log('Message:', data);
};
// Именованные события
eventSource.addEventListener('order_update', (e) => {
const order = JSON.parse(e.data);
updateOrderStatus(order.orderId, order.status);
});
eventSource.addEventListener('notification', (e) => {
showNotification(JSON.parse(e.data).message);
});
// Обработка ошибок
eventSource.onerror = (e) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Соединение закрыто, автоматически переподключается...');
}
};
Масштабирование с Redis Pub/Sub
На нескольких серверах — Redis Pub/Sub для распределённой доставки событий (аналогично WebSocket):
sub.subscribe('user:events', (message) => {
const { userId, event, data } = JSON.parse(message);
sendToUser(userId, event, data);
});
// Из любого сервиса
await pub.publish('user:events', JSON.stringify({
userId: 'user_123',
event: 'payment_completed',
data: { amount: 5000 }
}));
Ограничения SSE
- Только сервер → клиент — клиент не может отправить данные через SSE (только новые EventSource-запросы или отдельный API-запрос)
- Лимит соединений в HTTP/1.1 — браузер ограничивает 6 соединений на домен. SSE занимает одно. Решение: HTTP/2 (один мультиплексируемый поток).
-
IE не поддерживает — полифилл
eventsourceдля старых браузеров
Практические кейсы
- Прогресс длинных операций (импорт файла, генерация отчёта)
- Realtime-уведомления в ЛК
- Обновление счётчиков (новые сообщения, заказы)
- Live-лента новостей или спортивных результатов
Сроки
SSE-эндпоинт с аутентификацией, heartbeat, Redis-scaling: 3–5 дней. С типизированными событиями, client-side хуком (useSSE), интеграцией уведомлений: 1–2 недели.







