Real-Time Backend
Server-Sent Events
Цели урока
- Понимать как SSE работает поверх HTTP (chunked encoding, text/event-stream)
- Использовать четыре поля формата: data, event, id, retry
- Строить SSE-сервер на Node.js с авто-реконнектом и Last-Event-ID
- Выбирать между SSE, WebSocket и polling исходя из требований
Предварительные знания
2013 год. Twitter переводит live-ленту с polling на SSE - нагрузка на серверы падает в 10 раз. Один HTTP-запрос вместо тысяч повторных, и новые твиты появляются мгновенно. 2022 год: ChatGPT использует SSE для эффекта печатания - каждый токен летит отдельным событием. SSE - первая настоящая push-технология, встроенная прямо в браузер.
- Facebook/Twitter - live-ленты и уведомления через SSE
- GitHub - стриминг логов CI/CD в реальном времени
- Финансовые платформы - обновления котировок и цен
- ChatGPT - стриминг ответов модели (эффект печатания через SSE)
Ian Hickson и HTML5 EventSource
Ян Хиксон, главный редактор спецификации HTML5 в WHATWG, добавил Server-Sent Events в черновик HTML5 в 2006 году. До этого единственным способом получать события от сервера был polling или long polling - обходные решения поверх HTTP. Хиксон хотел дать браузеру нативный механизм push без WebSocket (который был отдельной спецификацией). В 2014 году SSE вошёл в финальный стандарт W3C как самостоятельная спецификация.
SSE: Push поверх обычного HTTP
2013 год. Twitter переводит live-ленту с polling на SSE - нагрузка на серверы падает в 10 раз. Один HTTP-запрос вместо тысяч повторных. ChatGPT в 2022 использует SSE для эффекта печатания - каждый токен прилетает отдельным событием. Это и есть SSE: сервер отправляет события клиенту, а не наоборот.
**Server-Sent Events (SSE)** - стандартный механизм W3C, при котором сервер отправляет события клиенту через обычное HTTP-соединение. Клиент делает один GET-запрос, сервер держит соединение открытым и шлёт данные по мере появления.
SSE работает поверх HTTP/1.1 chunked transfer encoding. Никакого специального протокола - обычный HTTP. Поэтому SSE проходит через любые прокси и файрволы без дополнительной настройки.
- **Однонаправленный:** только сервер -> клиент (unidirectional)
- **Content-Type:** text/event-stream
- **Транспорт:** обычный HTTP/1.1, chunked encoding
- **Автореконнект:** браузер сам переподключается при обрыве
- **Встроен в браузер:** EventSource API, никаких библиотек
В отличие от polling, где клиент забрасывает сервер запросами, SSE - это **настоящий push**. Сервер решает, когда отправить данные. Соединение устанавливается один раз и живёт до тех пор, пока одна из сторон не закроет его.
SSE - это двусторонний канал связи, как WebSocket
SSE - строго однонаправленный: только сервер может отправлять данные клиенту. Для отправки данных от клиента нужен отдельный HTTP-запрос
Какой Content-Type используется для SSE?
Формат text/event-stream
SSE-ответ под капотом - обычный текст с простыми правилами форматирования. Никакого бинарного протокола, никаких handshake-ов. Это один из главных плюсов: SSE легко отлаживать в браузере прямо в Network tab.
Каждое событие - набор полей, разделённых двойным переводом строки (`\n\n`). Четыре поля формата:
| Поле | Назначение | Пример |
|---|---|---|
| data: | Данные события (обязательное) | data: {"price": 100} |
| event: | Имя события (по умолчанию "message") | event: notification |
| id: | Уникальный ID для восстановления после обрыва | id: 42 |
| retry: | Время переподключения в мс | retry: 3000 |
Каждое событие **обязательно** заканчивается двойным `\n\n`. Одинарный `\n` разделяет поля внутри одного события. Без двойного перевода строки браузер не распознает границу события.
Что произойдёт, если не указать поле `event:` в SSE-событии?
EventSource API в браузере
Сервер готов. Подключаемся из браузера через встроенный **EventSource** API - без npm install, без webpack, без ничего. Это один из редких случаев когда платформа даёт готовый инструмент.
| readyState | Значение | Описание |
|---|---|---|
| CONNECTING | 0 | Подключение или переподключение |
| OPEN | 1 | Соединение активно, данные поступают |
| CLOSED | 2 | Соединение закрыто, переподключение не будет |
**Автореконнект и Last-Event-ID** - главная суперсила SSE. При обрыве соединения браузер автоматически переподключается и отправляет заголовок `Last-Event-ID` с последним полученным `id:`. Сервер может использовать это, чтобы дослать пропущенные события.
Интервал переподключения по умолчанию зависит от браузера (обычно 3-5 секунд). Сервер может переопределить его полем `retry: 5000` (в миллисекундах).
После вызова source.close() можно снова открыть это же соединение
EventSource после close() переходит в состояние CLOSED навсегда. Для нового подключения нужно создать новый объект EventSource
Что произойдёт при обрыве SSE-соединения, если сервер отправлял поле `id:` в каждом событии?
Ограничения SSE и когда его использовать
SSE - производительный инструмент, но у него есть чёткие границы применимости. Понимание этих границ - ключ к правильному выбору технологии.
- **Однонаправленный:** только сервер -> клиент. Для отправки данных клиентом нужен отдельный POST/PUT
- **Лимит 6 соединений** на домен в HTTP/1.1 (ограничение браузера, не протокола). В HTTP/2 этот лимит снят через мультиплексирование
- **Только текст:** бинарные данные (картинки, аудио) передавать нельзя. Можно Base64, но это +33% к размеру
- **Нет заголовка Authorization** в конструкторе EventSource. Аутентификация - через cookie или query-параметры
- **Не работает в Service Workers** (ограничение API, не протокола)
| Критерий | Polling | Long Polling | SSE |
|---|---|---|---|
| Направление | Клиент -> Сервер | Клиент -> Сервер | Сервер -> Клиент |
| Задержка | Интервал опроса | Низкая | Минимальная (реальный push) |
| Нагрузка на сервер | Высокая (много запросов) | Средняя | Низкая (одно соединение) |
| Автореконнект | Нужно вручную | Нужно вручную | Встроен в браузер |
| Бинарные данные | Да | Да | Нет (только текст) |
| Двусторонний обмен | Да (новый запрос) | Да (новый запрос) | Нет |
Когда SSE - идеальный выбор
- SSE подходит — Live-ленты новостей и соцсетей, уведомления (push notifications в браузере), дашборды с метриками в реальном времени, стриминг логов, обновления цен и котировок, прогресс длительных операций (загрузка, обработка), стриминг ответов LLM
- SSE не подходит — Чаты и мессенджеры (нужен двусторонний обмен), онлайн-игры (нужна низкая задержка в обе стороны), видео/аудио стриминг (бинарные данные), collaborative editing вроде Google Docs
В HTTP/2 лимит 6 соединений на домен **не актуален** - все SSE-потоки мультиплексируются в одном TCP-соединении. Если сервер поддерживает HTTP/2, ограничение соединений снимается.
Для какого сценария SSE подходит лучше всего?
Ключевые идеи SSE
- SSE - однонаправленный push от сервера к клиенту поверх обычного HTTP
- Формат text/event-stream: поля data:, event:, id:, retry: разделённые \n\n
- EventSource API - встроенный в браузер, с авто-реконнектом и Last-Event-ID
- Лимит 6 соединений на домен в HTTP/1.1 (снят в HTTP/2)
- Идеален для live-фидов, уведомлений, дашбордов, стриминга LLM-ответов
Связь с другими темами
SSE - первый шаг к настоящему real-time. Следующий - WebSocket, который добавит двусторонний канал.
- WebSocket — Следующий шаг - полнодуплексный протокол для двустороннего обмена
- HTTP/2 Server Push — Альтернативный push-механизм на уровне протокола
- Long Polling — Предшественник SSE - эволюция push через polling
Вопросы для размышления
- Почему разработчики ChatGPT выбрали SSE для стриминга ответов, а не WebSocket?
- Как реализовать аутентификацию SSE-соединения в production-приложении?
- В каких сценариях хранение last-event-id и replay пропущенных событий критичны?
Связанные уроки
- rt-02-http-limits — Polling и long polling - фон для понимания зачем нужен SSE
- rt-04 — WebSocket - следующий шаг: двунаправленный канал
- rt-05 — HTTP/2 Server Push - альтернативный push на уровне протокола
- rt-11 — Streaming ответов LLM (ChatGPT эффект печатания) через SSE
- stream-03 — Message brokers как источник событий для SSE endpoint
- rt-06 — WebRTC для случаев когда SSE недостаточно
- net-22-http-headers
- net-21-http-basics