SSE
SSE (Server-Sent Events) — стандартный веб-механизм однонаправленного стриминга сообщений от сервера к клиенту поверх обычного HTTP. Проще WebTransport и WebSocket, работает через все прокси, поддерживается всеми браузерами кроме IE. Определён в HTML Living Standard.
Как работает
- Клиент открывает обычный HTTP GET к endpoint'у.
- Сервер отвечает
Content-Type: text/event-streamи не закрывает соединение. - Сервер стримит текстовые события пока хочет.
- Клиент читает построчно через API
EventSource. - Если соединение рвётся — браузер автоматически переподключается через 3 секунды.
Формат
data: hello world
data: line one
data: line two
id: 42
event: userMessage
data: {"from": "alice", "text": "yo"}
retry: 10000
: этой строчкой сервер держит соединение живым (comment)
Каждое событие разделено пустой строкой. Поля:
| Поле | Значение |
|---|---|
| data: | Данные события. Несколько строк — конкатенируются через \n |
| event: | Тип события. Клиент подписывается на конкретные типы |
| id: | ID события. При переподключении клиент шлёт заголовок Last-Event-ID |
| retry: | Переопределить интервал переподключения (мс) |
| : | Comment. Часто используется как keepalive |
Клиентский API
const es = new EventSource('/events');
es.onmessage = (ev) => {
console.log('default event:', ev.data);
};
es.addEventListener('userMessage', (ev) => {
const msg = JSON.parse(ev.data);
console.log('from', msg.from);
});
es.onerror = () => {
console.log('соединение оборвано, будет переподключение');
// Браузер сам переподключится, если es.readyState !== CLOSED
};
// Закрыть
es.close();
Серверная сторона (Node.js)
app.get('/events', (req, res) => {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // отключить буферизацию nginx
});
res.flushHeaders();
let id = 0;
const interval = setInterval(() => {
id++;
res.write(`id: ${id}\n`);
res.write(`data: ${JSON.stringify({time: Date.now()})}\n\n`);
}, 1000);
// Keepalive
const keepalive = setInterval(() => {
res.write(': keepalive\n\n');
}, 30000);
req.on('close', () => {
clearInterval(interval);
clearInterval(keepalive);
});
});
Nginx
Nginx буферизует ответы по умолчанию → клиент не видит события сразу. Нужно:
location /events {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h; # не таймаутить долгое соединение
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}
SSE vs WebSocket
| SSE | WebSocket | |
|---|---|---|
| Направление | Server → Client только | Двустороннее |
| Транспорт | Обычный HTTP | HTTP upgrade → свой протокол |
| Формат | Текстовый (UTF-8) | Текст + бинарный |
| Авто-reconnect | Да, встроен в браузер | Нет, реализуется руками |
| Last-Event-ID | Да, автоматически | Нет |
| Прокси-совместимость | Отлично (обычный HTTP) | Многие прокси режут upgrade |
| Compression | gzip HTTP | permessage-deflate |
| Число открытых соединений | 6 на origin (HTTP/1.1) | 6 на origin (HTTP/1.1) |
| С HTTP/2 | ~100 concurrent | ~100 |
Когда что выбирать
- SSE — уведомления, стриминг логов, live-обновления UI, real-time аналитика, стрим текста от LLM (ChatGPT, Claude — все используют SSE).
- WebSocket — чаты, редакторы (collaborative), онлайн-игры, любой двунаправленный обмен.
SSE для стрима LLM
Большинство современных LLM-API стримят токены через SSE:
curl -N https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4","stream":true,"messages":[...]}'
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" world"}}]}
data: [DONE]
Anthropic, Google, Mistral, все — SSE. Просто, работает через любые прокси.
Ограничения
- Только UTF-8 текст. Бинарь → base64 (плохой overhead).
- Только Server → Client. Для запросов клиент делает обычный HTTP.
- Timeout в браузере. Обычно 5 минут idle — нужны keepalive.
- Custom headers невозможно задать в EventSource (только через
polyfills).