Сети и протоколы
HTTP-стриминг, SSE, WebSocket и gRPC
REST API возвращают полный ответ. Но живые дашборды, чат, цены в реальном времени и потоки токенов AI требуют другого: данные поступают по частям или в двух направлениях. HTTP растягивается до этих случаев — через chunked transfer, SSE, апгрейды WebSocket и gRPC-трейлеры — но каждый выбор несёт свои компромиссы.
Chunked transfer encoding
HTTP/1.1 поддерживает Transfer-Encoding: chunked для потоковой передачи ответов без заранее известной общей длины. Сервер отправляет чанки с префиксом размера до тех пор, пока чанк нулевой длины не закрывает тело:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/plain
5\r\n
Hello\r\n
6\r\n
World\r\n
0\r\n
\r\nЭто позволяет серверу начать отправку данных до того, как вычислен весь ответ — полезно для генерируемого HTML, потоковых AI-завершений и слежки за логами. HTTP/2 и HTTP/3 не используют chunked encoding: DATA-фреймы нативно обрабатывают тела переменной длины без явного слоя фреймирования.
Server-Sent Events (SSE)
SSE (Content-Type: text/event-stream) — долгоживущий HTTP-ответ, доставляющий читаемые JavaScript’ом события от сервера браузеру в одном направлении. Браузер делает один GET-запрос; сервер держит соединение открытым и толкает события:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"temp": 72.4}\n\n
data: {"temp": 72.6}\n\nSSE построен на стандартном HTTP, проходит через все прокси без апгрейда протокола, автоматически переподключается при разрыве соединения и работает на HTTP/2 (где едет в одном потоке среди многих, не блокируя другие запросы). Ограничение: только сервер → клиент — клиент не может толкать данные обратно через тот же SSE-поток (нужен отдельный запрос).
SSE — правильный выбор для: живых дашбордов, потоков уведомлений, потоковой передачи AI-токенов (text/event-stream — то, как ChatGPT-подобные интерфейсы стримят токены). WebSocket — правильный выбор для: двустороннего реального времени (чат, совместное редактирование, состояние мультиплеерной игры).
WebSocket
WebSocket (RFC 6455) начинается как HTTP/1.1 и апгрейдится до бинарного двустороннего фрейм-протокола:
- Клиент отправляет
GET /ws HTTP/1.1сUpgrade: websocketиConnection: Upgrade. - Сервер отвечает
101 Switching Protocols. - TCP-соединение теперь — WebSocket-туннель: бинарные фреймы (текстовые или бинарные) текут в обоих направлениях.
WebSocket несовместим с мультиплексированием HTTP/2 — после переключения соединения на протокол WebSocket оно больше не является HTTP/2-соединением. Решение: RFC 8441 (WebSocket over HTTP/2) — клиент отправляет расширенный CONNECT-запрос с :protocol: websocket, создавая один HTTP/2-поток, ведущий себя как WebSocket-туннель. Это позволяет одному HTTP/2-соединению мультиплексировать несколько WebSocket-соединений рядом с обычными запросами.
- SSE — направление
- Сервер → Клиент
- SSE — требует апгрейда протокола
- Нет — обычный HTTP
- WebSocket — направление
- Двустороннее
- WebSocket — поддержка HTTP/2
- RFC 8441 extended CONNECT
- gRPC — транспорт
- HTTP/2 (всегда)
- gRPC — двусторонний стриминг
- Да (4 режима: unary, server-streaming, client-streaming, bidi)
gRPC: трейлеры и зависимость от HTTP/2
gRPC — высокопроизводительный RPC-фреймворк, работающий поверх HTTP/2. Ключевая механика:
- gRPC кодирует запросы и ответы как Protocol Buffers (бинарно сериализованные структуры) в HTTP/2 DATA-фреймах.
- Каждый gRPC-вызов — один HTTP/2-поток. Множество одновременных RPC мультиплексируются на одном соединении.
- Трейлеры несут результат: gRPC-статус и сообщение приходят в trailing HEADERS-фреймах после DATA-фреймов (тела). gRPC-статус-код отделён от HTTP-статус-кода — gRPC всегда шлёт
200 OKна HTTP-уровне; реальный результат (OK, NOT_FOUND и т.д.) приходит в трейлереgrpc-status.
gRPC-Web существует потому, что браузеры не могут надёжно читать HTTP-трейлеры. gRPC-Web переписывает trailing-заголовки сервера как финальное base64-кодированное сообщение в теле ответа, которое браузеры умеют читать. Прокси (Envoy, gRPC-Web proxy) транслирует между настоящим gRPC (с трейлерами) и gRPC-Web (трейлеры встроены в тело).
Сжатие
Серверы сжимают тела ответов через Content-Encoding: gzip|br|zstd. Brotli (br) сжимает текст примерно на 20% лучше gzip при сопоставимой скорости декомпрессии. Серверы выбирают алгоритм на основе заголовка Accept-Encoding клиента.
Угроза безопасности — атаки CRIME и BREACH: эти атаки эксплуатировали HTTPS + сжатие для утечки сессионных токенов через наблюдение за изменениями размера ответа, когда атакующий внедрял контролируемый контент. Меры защиты: отключить сжатие для ответов, включающих и управляемые атакующим данные, и конфиденциальные данные в одном теле ответа. Сжатие статических ресурсов безопасно (предварительно вычисленное, нет пользовательского ввода). Динамические API-ответы, смешивающие сессионные токены с отражёнными запросом данными, должны либо пропускать сжатие, либо использовать случайное набивание.
HTTP-ограничение скорости
Серверы возвращают 429 Too Many Requests с Retry-After: <секунды> для регулирования агрессивных клиентов. Распространённые алгоритмы:
- Token bucket: корзина пополняется с фиксированной скоростью; всплески разрешены до ёмкости. Лучше всего для рабочих нагрузок с пиками, но в среднем умеренных.
- Leaky bucket: запросы вытекают с фиксированной скоростью; излишки ставятся в очередь или сбрасываются. Сглаживает пики.
- Fixed window: подсчёт запросов за временное окно (например, 100/минута). Уязвим к пикам на границе окна.
- Sliding window: подсчёт запросов за последние N секунд. Точнее, требует больше памяти.
CDN (Cloudflare, Fastly) ограничивают скорость на краю до того, как запросы достигают origin — первая линия обороны. Origin-серверы добавляют второй слой. Клиенты должны уважать Retry-After и использовать экспоненциальный откат для предотвращения “стада грома” повторных запросов.
Почему gRPC использует HTTP/2-трейлеры для статуса вместо HTTP-статус-кода?
Когда выбирать SSE вместо WebSocket?
Упорядочьте шаги WebSocket-хендшейка поверх HTTP/1.1:
- 1 Клиент отправляет GET /ws HTTP/1.1 с Upgrade: websocket и Sec-WebSocket-Key
- 2 Сервер отвечает 101 Switching Protocols с Sec-WebSocket-Accept
- 3 HTTP-протокол завершается на этом TCP-соединении
- 4 Двусторонний фрейм-протокол WebSocket начинается на том же TCP-соединении
- 5 Клиент и сервер теперь могут отправлять фреймы друг другу в любой момент
Почему это работает
Почему gRPC-Web переписывает трейлеры в тело. Браузерные API XHR и fetch() не могут читать HTTP-трейлеры — API просто не предоставляет к ним доступ. Трейлеры — заголовки, приходящие после тела ответа, и HTTP-клиент браузера их скрывает. gRPC-Web решает это, кодируя трейлеры как финальное фреймированное сообщение в теле, с особым байтом-префиксом для различия данных ответа и трейлеров. JavaScript-библиотека gRPC-Web парсит это финальное сообщение для извлечения grpc-status. На стороне сервера прокси Envoy или nginx-grpc-web транслирует между настоящим gRPC (с трейлерами) и gRPC-Web (трейлеры встроены в тело).
- 01В чём разница между SSE и WebSocket и когда выбирать каждый?
- 02Почему атаки CRIME и BREACH нацелены на HTTPS+сжатие и как защититься?
- 03Как RFC 8441 позволяет WebSocket сосуществовать с мультиплексированием HTTP/2?
HTTP-стриминг выходит за рамки единственного запрос-ответ через chunked transfer encoding (тела переменной длины), SSE (долгоживущие текстовые потоки событий сервер→клиент), WebSocket (двусторонние бинарные фреймы через апгрейд 101, или RFC 8441 extended CONNECT на HTTP/2) и gRPC (бинарные RPC на HTTP/2, с RPC-статусом в trailing HEADERS-фреймах). gRPC-Web прокси переписывают трейлеры в фреймы тела для совместимости с браузерами. Сжатие (Brotli/gzip через Content-Encoding) экономит полосу пропускания, но требует осторожности с ответами, смешивающими управляемые данные с конфиденциальными (CRIME/BREACH). Ограничение скорости через 429 + Retry-After использует алгоритмы token bucket или sliding window; CDN-ограничение на краю — первая линия обороны.