Зачем 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) сжимает через:
- Static table - 61 предопределённый header (
:method GET,content-type text/html, ...) - Dynamic table - расширяемый, headers с предыдущих запросов
- 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: </styles.css>; rel=preload; as=style
Link: </app.js>; 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: <base64-encoded 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.