SSE

SSE (Server-Sent Events) — стандартный веб-механизм однонаправленного стриминга сообщений от сервера к клиенту поверх обычного HTTP. Проще WebTransport и WebSocket, работает через все прокси, поддерживается всеми браузерами кроме IE. Определён в HTML Living Standard.

Как работает

  1. Клиент открывает обычный HTTP GET к endpoint'у.
  2. Сервер отвечает Content-Type: text/event-stream и не закрывает соединение.
  3. Сервер стримит текстовые события пока хочет.
  4. Клиент читает построчно через API EventSource.
  5. Если соединение рвётся — браузер автоматически переподключается через 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

SSEWebSocket
НаправлениеServer → Client толькоДвустороннее
ТранспортОбычный HTTPHTTP upgrade → свой протокол
ФорматТекстовый (UTF-8)Текст + бинарный
Авто-reconnectДа, встроен в браузерНет, реализуется руками
Last-Event-IDДа, автоматическиНет
Прокси-совместимостьОтлично (обычный HTTP)Многие прокси режут upgrade
Compressiongzip HTTPpermessage-deflate
Число открытых соединений6 на origin (HTTP/1.1)6 на origin (HTTP/1.1)
С HTTP/2~100 concurrent~100

Когда что выбирать

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. Просто, работает через любые прокси.

Ограничения

См. также