# HTTP/2 internals - binary framing, HPACK, stream multiplexing _Сеть: L4 и выше · LinuxLab Knowledge Base_ **TL;DR:** HTTP/2 - бинарный мультиплексинг поверх одного TCP-соединения. HPACK сжимает headers через индексированный словарь. Streams независимы. Server push deprecated. На loss-friendly link HoL-blocking - проблема, которую решил QUIC. ## Зачем HTTP/2 HTTP/1.1 (RFC 7230) текстовый, head-of-line-blocking на уровне request'ов, нельзя посылать parallel запросы в одном соединении. Браузеры открывали 6 параллельных TCP-соединений на host - 6 TLS handshake'ов, дублирование headers, плохое использование congestion control. В 2015 IETF опубликовал HTTP/2 (RFC 7540, переиздан как RFC 9113): - **Бинарный** - не парсится глазами, но быстрее и однозначнее - **Один TCP** на host - меньше handshake'ов, лучше congestion window - **Stream multiplexing** - параллельные запросы как независимые streams - **HPACK** - сжатие headers через словарь - **Server push** (теперь deprecated) Отношение к HTTP/1.1 - не replacement, а **transport optimization**. Семантика (метод, URI, status codes, body) такая же. Меняется только wire format. В 2020 пришёл [[quic-http3|HTTP/3]] - тот же мультиплекс, но поверх QUIC/UDP, без TCP HoL. ## Binary framing layer Каждое HTTP/2 communication разбивается на **frame'ы** (минимальная единица). Frame header - 9 байт: ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +=+=============================================================+ | Frame Payload (0...) ... +---------------------------------------------------------------+ ``` - **Length** - размер payload (max 2^14 = 16384 байт по умолчанию, может быть увеличен до 2^24) - **Type** - тип frame'а (см. ниже) - **Flags** - bit-flags зависящие от type - **Stream Identifier** - 0 для control, иначе идентификатор stream ## Frame types | Type | Назначение | |------|------------| | `DATA` (0x0) | request/response body | | `HEADERS` (0x1) | request/response headers (после HPACK) | | `PRIORITY` (0x2) | priority hint (deprecated в RFC 9113) | | `RST_STREAM` (0x3) | завершить stream с error code | | `SETTINGS` (0x4) | connection params (frame size, max streams, ...) | | `PUSH_PROMISE` (0x5) | server push (deprecated) | | `PING` (0x6) | keepalive + RTT measurement | | `GOAWAY` (0x7) | initiate connection close | | `WINDOW_UPDATE` (0x8) | flow control credit | | `CONTINUATION` (0x9) | продолжение HEADERS если не вместился | ## Streams - independent virtual connections Каждый запрос/ответ - **stream** с уникальным 31-битным ID. Streams независимы: фреймы разных streams могут чередоваться в TCP-соединении. Lifecycle stream: ``` idle → reserved (server push) ─┐ └→ open ──┐ │ ├→ half-closed ──┼→ closed └→ half-closed ──┘ ``` ID правила: - **Чётные** ID - инициированы сервером (server push) - **Нечётные** ID - инициированы клиентом - Каждый раз увеличиваются - reuse запрещён Один TCP может держать тысячи streams (ограничено `SETTINGS_MAX_CONCURRENT_STREAMS`). ## HPACK - сжатие headers HTTP-headers повторяются: `User-Agent`, `Accept-Encoding`, `Cookie` - одинаковые на каждом запросе. HTTP/1.1 шлёт текстом каждый раз. **HPACK (RFC 7541)** сжимает через: 1. **Static table** - 61 предопределённый header (`:method GET`, `content-type text/html`, ...) 2. **Dynamic table** - расширяемый, headers с предыдущих запросов 3. **Huffman coding** - сжатие литералов Передаётся индекс в таблице (1-2 байта) вместо текста (50+ байт). Сжатие 80-90% на типичных headers. Минус: dynamic table требует **per-connection state**, sync между сторонами. CRIME/HEIST-style атаки требуют осторожности с user-controlled headers (cookie compression). HTTP/3 использует **QPACK** (адаптация HPACK для QUIC) - похожая идея, но без head-of-line на decompression. ## Stream priority - и почему deprecated HTTP/2 (RFC 7540) ввёл сложный priority tree: каждый stream имеет parent stream, weight (1-256), exclusive flag. Идея - сервер обрабатывает critical streams (HTML > CSS > image) раньше. На практике: - Браузеры реализовали по-разному - Сервера часто игнорировали - Race-conditions в дереве RFC 9113 (2022) **deprecated** priority. Новый RFC 9218 ввёл **Extensible Priorities** - простые `Priority:` headers со scheme `urgency=N, incremental=?1`. ## Flow control - per-stream и per-connection HTTP/2 имеет credit-based flow control (как TCP receive window): - Каждый sender знает window size receiver'а - Шлёт DATA пока window > 0 - Receiver шлёт `WINDOW_UPDATE` чтобы дать больше credit'ов **Два уровня**: per-stream и per-connection. Без credit'а нельзя слать DATA - HEADERS можно (control plane). Default window 65535 байт - **очень мало**, на 100ms RTT даёт ~5 Mbps per connection. Сервера обычно сразу шлют `WINDOW_UPDATE` до 16-64 МБ. ## Server push - deprecated Идея: сервер шлёт ресурсы до запроса (HTML и связанный CSS вместе). RFC 7540 описал, многие сервера реализовали. На практике: - Браузеры **уже имели** ресурс в кэше → wasted bandwidth - Сложно правильно решить, что push'ить - **103 Early Hints** покрывает использование лучше Chrome **отключил** support в 2022. RFC 9113 (2022) пометил PUSH_PROMISE как **deprecated**. Замена - **103 Early Hints**: ``` HTTP/2 103 Early Hints Link: ; rel=preload; as=style Link: ; rel=preload; as=script HTTP/2 200 OK Content-Type: text/html ... ``` Браузер начинает грузить preload'ы пока сервер генерирует основной ответ. ## TCP head-of-line blocking - the problem Big "but" HTTP/2: **один TCP connection, один in-order byte-stream**. Если один пакет потерян - **все** streams ждут retransmit: ``` Stream 1: [seg1][seg2][LOST][seg4] ←─ Stream 1 ждёт seg3 Stream 2: [data][data][data][data] ←─ И Stream 2 ждёт тоже! Stream 3: [data][data][data][data] ←─ И Stream 3! ``` Хотя данные Stream 2 уже в буфере NIC - kernel не отдаст app пока не дойдёт seg3 (TCP в order). Это и есть **TCP HoL blocking**. На лоси-фрукт wifi/4G это убивает преимущество multiplexing'а - HTTP/1.1 с 6 connections иногда быстрее. Решение - [[quic-http3|HTTP/3 / QUIC]]: streams в QUIC независимы на уровне UDP, потеря одного не блочит другие. ## Negotiation - как клиент узнаёт что server поддерживает h2 Два механизма: ### ALPN в TLS (стандарт) В TLS ClientHello клиент шлёт **ALPN** (Application-Layer Protocol Negotiation) с списком: `h2, http/1.1`. Сервер выбирает в ServerHello. Browsers только так делают - HTTP/2 в clear text **не поддерживается** ни одним браузером. ### Upgrade header (h2c, plaintext) ``` GET / HTTP/1.1 Host: example.com Connection: Upgrade, HTTP2-Settings Upgrade: h2c HTTP2-Settings: ``` Сервер если поддерживает - отвечает `101 Switching Protocols`. Используется только server-to-server (gRPC часто). RFC 9113 убрал Upgrade-механизм (но многие сервера и клиенты поддерживают). ## HTTP/2 vs HTTP/3 - сравнение | Свойство | HTTP/2 | HTTP/3 | |----------|--------|--------| | Транспорт | TCP | QUIC (UDP) | | TLS | отдельный (TLS 1.2+) | встроен в QUIC (TLS 1.3) | | Handshake | TCP + TLS = 2-3 RTT | QUIC = 1 RTT (или 0-RTT) | | HoL на packet loss | да (TCP) | нет (per-stream) | | Headers compression | HPACK | QPACK | | Multiplexing | да | да | | Connection migration | нет | да (мобильный сменил wifi → 4G) | | Server push | deprecated | не было | | Adoption (2025) | ~95% сайтов | ~30% (в основном Cloudflare/Google) | ## Когда что-то пошло не так - **`HTTP/2: protocol error`** - frame с неправильным format'ом или state-violation. `tcpdump` + Wireshark dissector покажут конкретный frame. Часто в client/server stub'ах баги. - **GOAWAY с last_stream_id** - сервер закрывает connection. Запросы с stream_id > last - нужно retry в новом connection. Часто из-за rate limit или idle timeout. - **Window starvation** - app не отправляет WINDOW_UPDATE, sender залип. Проверь библиотеку, увеличь buffer'ы. - **Plaintext h2c не работает в браузере** - все требуют TLS+ALPN. Используй `--insecure-http2` в curl для тестирования. - **HPACK decompression bomb** - в реализации может быть DoS если клиент шлёт много новых headers (раздувает dynamic table). Лимиты есть в RFC, но реализации варьируются (CVE-2019-9518). - **Throughput хуже HTTP/1.1** - вероятно много packet loss и multiplexing блокирован TCP HoL. Перейти на HTTP/3. - **`SETTINGS_MAX_CONCURRENT_STREAMS = 100`** - default многих серверов. Если клиент шлёт больше - стримы queue'ятся, latency растёт. Поднять или использовать pool connections. ## Команды ```bash curl --http2 -v https://example.com 2>&1 | head -40 ``` Curl с HTTP/2 - в выводе видно ALPN handshake и frame'ы ```bash curl --http2-prior-knowledge http://localhost:8080/ ``` h2c - HTTP/2 в plain text, минуя ALPN. Только если знаешь, что server h2c ```bash openssl s_client -alpn h2 -connect example.com:443 -tls1_2 < /dev/null ``` Проверить, что сервер договаривается на h2 через ALPN ```bash nghttp -nv https://example.com ``` nghttp из nghttp2-utils - подробный HTTP/2 client с frame-уровневым выводом ```bash h2load -n 1000 -c 10 -m 100 https://example.com/ ``` Бенчмарк HTTP/2: 1000 запросов, 10 connections, 100 streams concurrent ```bash tshark -Y 'http2' -Tfields -e http2.streamid -e http2.headers -i eth0 ``` Wireshark CLI - dissect HTTP/2 frames live ```bash echo 'h2 alpn alpn-list = h2' | nginx -t ``` В nginx http2 включается через 'listen 443 ssl http2;' (deprecated в 1.25.1+) ```bash curl --http2 -H 'Accept-Encoding: gzip' --raw -v https://example.com 2>&1 | grep -i hpack ``` HPACK не виден в curl напрямую - нужен h2c-dump или Wireshark ## См. также - [HTTP/1.1, HTTP/2, HTTP/3](/kb/http-protocol.md) - [QUIC и HTTP/3 - современный транспорт поверх UDP](/kb/quic-http3.md) - [TLS handshake](/kb/tls-handshake.md) - [gRPC - HTTP/2 + Protobuf RPC framework](/kb/grpc-basics.md) - [curl - HTTP-клиент из терминала](/kb/cmd-curl.md) - [TCP three-way handshake](/kb/tcp-handshake.md) - [Distributed tracing: span, context propagation, sampling](/kb/tracing-basics.md)