Learning Platform
Глоссарий Troubleshooting
Урок 11.03 · 22 мин
Начальный
HTTP/2MultiplexingHPACKBinary framesPerformance

HTTP/2 — бинарные фреймы, мультиплексирование и HPACK

В 2026 году более половины интернет-запросов идёт по HTTP/2. Когда вы открываете GitHub, YouTube, Twitter — между вами и сервером работает именно HTTP/2 (или уже HTTP/3 — о нём в следующем уроке). И это происходит абсолютно прозрачно: вы не меняете код, не пишете ничего нового. Семантика осталась прежней: те же методы, статус-коды, заголовки. Изменилось только то, как байты передаются по проводам.

Зачем вообще понадобился HTTP/2? И почему в HTTP/2 формат сообщений теперь бинарный, а не текстовый? Давайте разбираться.


Проблема HTTP/1.1: latency

К середине 2010-х средняя веб-страница выросла с пары страничек HTML до нескольких мегабайт ресурсов: 50-100 запросов на одну загрузку (CSS, JS, картинки, шрифты, API). На HTTP/1.1 это превратилось в боль.

HTTP/1.1: ограничения на параллелизм
Один TCPHTTP/1.1: на одном TCP-соединении в данный момент один запрос. Сервер отвечает, потом следующий запрос. Сериализация на уровне connection
PipeliningОпционально: клиент может послать несколько запросов подряд, не дожидаясь ответа. Но ответы должны прийти в порядке отправки -- head-of-line blocking. Поддержка плохая, в основном отключено
Workaround: domain shardingБраузер открывает 6 TCP-connection на домен. Сайты разносили ассеты по static1.example.com, static2.example.com, чтобы обойти лимит. Грязный хак
Workaround: bundlingСборка всех JS в один bundle.js и CSS в один styles.css. Минус: вся страница ждёт самого медленного ассета

Также каждый запрос HTTP/1.1 нёс килобайты заголовков (cookies + user-agent + custom headers) — и эти заголовки повторялись на каждом запросе без сжатия. Для API, где запросов сотни, это бессмысленный overhead.

HTTP/2 решает эти проблемы радикально, через четыре ключевых механизма:

  1. Бинарный формат фреймов — эффективный парсинг, чёткие границы.
  2. Мультиплексирование streams — много запросов одновременно в одном TCP.
  3. HPACK header compression — сжатие заголовков.
  4. Server push — сервер может отправить ресурс до запроса (но в практике почти не используется).

Бинарный формат: фреймы вместо текста

В HTTP/1.1 сообщение — это текст: разделители из CRLF, заголовки в формате Имя: Значение. Парсер сервера читает символ за символом, ищет \r\n, парсит до пустой строки. Это просто и понятно человеку, но неэффективно для машины: переменная длина, нечёткие границы, сложно вычислить размер.

В HTTP/2 всё передаётся как набор фреймов. Фрейм — это блок бинарных данных с заголовком фиксированной длины.

HTTP/2 frame structure
Length (24 бита)Длина payload в байтах -- сервер знает заранее, сколько байт читать, никаких CRLF-сканеров
Type (8 бит)Тип фрейма: DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION
Flags (8 бит)Битовые флаги: END_STREAM, END_HEADERS, ACK, PADDED, PRIORITY -- зависят от типа
Stream ID (31 бит)Идентификатор stream'а: к какому 'логическому запросу' этот фрейм принадлежит. 0 -- управляющие фреймы. Нечётные -- инициированные клиентом, чётные -- сервером
Payload (variable)Содержимое фрейма. Для HEADERS -- сжатый HPACK-блок. Для DATA -- байты тела. Для SETTINGS -- key-value параметры протокола

Самые важные типы фреймов:

  • HEADERS — заголовки запроса/ответа (включая статус-код, метод и т.д. как pseudo-headers :method, :path, :status).
  • DATA — тело сообщения. Может быть несколько DATA-фреймов на один stream.
  • SETTINGS — параметры соединения (max concurrent streams, initial window size).
  • WINDOW_UPDATE — flow control на уровне stream и соединения.
  • RST_STREAM — немедленно прерывает stream (как cancel).
  • GOAWAY — сервер сообщает клиенту, что закрывает соединение и не примет новые streams.
  • PING — проверка живости соединения.

Преимущества бинарного формата:

  1. Парсинг быстрее. Сервер читает фиксированные 9 байт заголовка фрейма, узнаёт длину payload, читает ровно столько байт.
  2. Чёткие границы. Не нужно сканировать на CRLF — длина известна сразу.
  3. Удобно для битмаскинга и параллельной обработки.

Минус: вы больше не можете прочитать сырой HTTP/2-трафик через telnet. Но curl, Wireshark и DevTools браузера декодируют его обратно в человекочитаемое представление.


Stream и мультиплексирование

Ключевая идея HTTP/2: на одном TCP-соединении одновременно живёт много streams. Stream — это логический «запрос/ответ» (или для server-push — сервер -> клиент). Каждый stream имеет свой ID и независим.

Мультиплексирование streams в одном TCP
TCP connectionОдно TCP-соединение клиент -- сервер. Внутри текут фреймы разных streams вперемешку
Stream 1: GET /a.cssHEADERS frame с :method GET, :path /a.css. Через несколько фреймов -- HEADERS с :status 200 + DATA frames с содержимым
Stream 3: GET /b.jsПараллельный запрос на JS. Идёт одновременно со Stream 1 в том же TCP
Stream 5: GET /img.pngТретий параллельный запрос. Сервер может слать DATA-фреймы для всех trех streams в любом порядке
...Сотни streams в одном TCP. Лимит задаётся через SETTINGS_MAX_CONCURRENT_STREAMS (обычно 100-1000)

На проводе фреймы разных streams перемежаются. Например, последовательность может быть:

HEADERS(stream=1) HEADERS(stream=3) DATA(stream=1) DATA(stream=3) DATA(stream=1, END_STREAM)

То есть сервер отвечает на запрос 1 и 3 параллельно, перемежая chunks данных. Каждый stream независим, и приоритеты можно регулировать (стимы можно перевешивать — но в HTTP/2 этот механизм был сложным и поставщики часто его игнорировали, в HTTP/3 он упрощён).

Pseudo-headers

В HTTP/2 первая строка запроса (request-line) разбита на отдельные «pseudo-headers», начинающиеся с двоеточия:

:method: GET
:path: /api/users
:scheme: https
:authority: api.example.com
accept: application/json
user-agent: curl/8.4.0

В ответе:

:status: 200
content-type: application/json

Заметьте: имена в lowercase. Это обязательно в HTTP/2. И :authority заменяет Host из HTTP/1.1.


HPACK: header compression

В HTTP/1.1 каждый запрос несёт полный набор заголовков, включая повторяющиеся (cookies, user-agent). На странице с 100 ресурсами это значит 100x повторений тех же байт. HPACK решает это умным сжатием.

HPACK использует две техники:

HPACK: dual approach
Static table61 предопределённый header common-pair: например, :method GET = 2, :status 200 = 8. Клиент и сервер знают эту таблицу одинаково. Заменяет частые headers на 1 байт
Dynamic tableHeaders, которые уже встретились в соединении. Если ты уже посылал 'cookie: abc...', он сохранён в dynamic table и в следующий раз заменяется индексом
Huffman codingДля значений, которые не в таблице, применяется Huffman compression с фиксированной таблицей частот символов HTTP. Сжимает текст-значения на 20-50%

Пример: первый запрос содержит cookie длиной 200 байт. Он передаётся как Huffman-сжатая строка (~150 байт) и добавляется в dynamic table с индексом 62. На втором запросе тот же cookie передаётся как один байт с индексом «62» — сжатие в 200 раз.

Реальный эффект: типичный API-запрос HTTP/2 шлёт заголовки в 5-10 байт, тогда как HTTP/1.1 та же информация занимает 500-2000 байт. На страницах с десятками запросов это экономит сотни КБ traffic’а.

WARNING

HPACK имел известную уязвимость — HPACK Bomb / CRIME-style атаки. Если атакующий может вставлять кусочек строки в ваш header и видеть размер сжатого ответа — он может угадать остальные значения. Workaround: серверы ограничивают размер dynamic table и могут не сжимать чувствительные headers. В HTTP/3 алгоритм называется QPACK — модификация, более устойчивая к out-of-order доставке.


Flow control в HTTP/2

В HTTP/1.1 flow control делает только TCP. В HTTP/2 добавляется flow control на уровне stream и соединения — через WINDOW_UPDATE фреймы.

Зачем второй уровень? Представьте: один stream шлёт огромный файл, второй — маленький JSON. Без stream-level flow control один тяжёлый stream забил бы буфер сервера и заблокировал других. С stream-level каждый stream имеет свой window, и сервер регулирует pace отдельно.

Клиент:  WINDOW_UPDATE(stream=1, increment=65536)  -- 'я готов принять ещё 64КБ для stream 1'
Сервер:  DATA(stream=1, 65536 bytes)               -- шлёт ровно столько
Клиент:  WINDOW_UPDATE(stream=1, increment=65536)  -- готов ещё

Это даёт точное управление: можно паузить, ускорять, отменять отдельные streams не влияя на других.


Server push (умер, но был интересной идеей)

HTTP/2 включал механизм, позволяющий серверу отправить ресурс ДО того, как клиент его запросил. Идея: клиент шлёт GET /index.html. Сервер видит, что html ссылается на /style.css и /app.js. Зачем ждать, пока клиент попросит CSS и JS — сервер сразу пушит их.

Stream 1: GET /index.html        (клиент -> сервер)
Stream 1: PUSH_PROMISE for /style.css (сервер -> клиент: 'я пушу этот ресурс')
Stream 1: HEADERS + DATA          (HTML)
Stream 2: HEADERS + DATA          (CSS, pushed)

На бумаге звучит круто. На практике:

  1. Сложно решать, что пушить. Сервер не знает, что у клиента уже в кэше.
  2. Push часто бесполезен — клиент уже имел в кэше. Тратит bandwidth впустую.
  3. Реализации браузеров глючные.

В 2022 году Chrome отказался от поддержки server push для HTTP/2. Большинство серверов её не используют. В HTTP/3 server push формально есть, но не считается best practice. Вместо неё рекомендуется 103 Early Hints — сервер шлёт «вот ссылки на ресурсы, которые понадобятся» до тела основного ответа, и клиент сам решает запросить их.


Один большой недостаток HTTP/2: HoL blocking на уровне TCP

Мультиплексирование решило HoL blocking на уровне HTTP. Но TCP сам по себе сериализует байты. Если один пакет в TCP потерян, TCP не отдаёт следующие пакеты приложению, пока не получит потерянный (потому что TCP гарантирует in-order delivery).

HoL blocking в HTTP/2 over TCP
Stream 1 packetЧасть DATA-фрейма для stream 1. TCP-пакет идёт по сети
LOSTПакет потерялся в сети (collision, congestion, drop). TCP видит gap в sequence numbers
Stream 1 packetСледующий пакет для stream 1 пришёл, но TCP не отдаст его приложению, пока не дойдёт packet B
Stream 1 ждётХотя stream 1 имеет все нужные данные на канальном уровне, TCP блокирует доставку всех данных, пока не переотправят packet B. HoL blocking

На стабильной wired-сети это редко проблема. Но на мобильных, на WiFi с потерями, особенно при больших задержках — HoL blocking ощутимо снижает performance HTTP/2 по сравнению с теоретически возможным. Именно эту проблему решает HTTP/3, переходя на QUIC поверх UDP — следующий урок.


ALPN: как клиент и сервер договариваются о версии

Браузер не знает заранее, поддерживает ли сервер HTTP/2. Узнаёт через ALPN (Application-Layer Protocol Negotiation) — расширение TLS, в котором клиент в TLS handshake перечисляет поддерживаемые протоколы:

ClientHello: ALPN [h2, http/1.1]
ServerHello: ALPN h2

Сервер выбирает один из предложенных. Если сервер поддерживает h2 — использует HTTP/2 поверх TLS. Если нет — падают обратно на http/1.1. Это причина, по которой HTTP/2 в реальности всегда идёт через HTTPS — chrome и firefox не поддерживают HTTP/2 без TLS.

Без TLS HTTP/2 теоретически возможен (h2c — HTTP/2 cleartext), на практике почти не используется.


Попробуй сам

# 1. Посмотреть, использует ли сервер HTTP/2
curl -I --http2 -w 'HTTP version: %{http_version}\n' https://github.com 2>&1 | grep -i 'http\|http_version'

# 2. Заставить curl использовать только HTTP/1.1
curl -I --http1.1 -w 'HTTP version: %{http_version}\n' https://github.com 2>&1

# 3. Посмотреть на ALPN-negotiation в TLS handshake
curl -v --http2 https://github.com 2>&1 | grep -i alpn

# 4. Сравнить размер заголовков HTTP/1.1 vs HTTP/2 (визуально)
# В Chrome DevTools (F12) -> Network tab -> любой запрос:
#   В Headers сверху увидите Protocol (h2, h3, http/1.1)
#   Размер запроса/ответа в Size column

# 5. Запустить простой HTTP/2 сервер на Python (через aiohttp поддержки HTTP/2 нет,
# нужно httpx-aiohttp или hyper-h2 -- эксперимент для интересующихся)
# Простой вариант: использовать nghttpd из nghttp2-tools
# Установка: brew install nghttp2 / apt install nghttp2

# 6. nghttp -- HTTP/2 клиент с детальным выводом
# nghttp -nv https://github.com    # покажет фреймы
# Для каждого фрейма видим: тип (HEADERS, DATA), stream_id, flags

# 7. Wireshark с фильтром http2 -- видно все фреймы (если расшифровать TLS через SSLKEYLOGFILE)
# (SSLKEYLOGFILE export в .env для chrome/firefox, потом Wireshark подцепит ключи)

Если у вас Chrome и любой современный сайт — DevTools -> Network показывает Protocol колонку. Большинство современных сайтов отдают h2 или h3.


Что вы должны вынести

  1. HTTP/2 = HTTP/1.1 по семантике (методы, статус-коды, заголовки одни и те же).
  2. Изменился формат на проводе: бинарные фреймы вместо текста.
  3. Мультиплексирование — много streams в одном TCP. Решает HoL blocking уровня HTTP.
  4. HPACK сжимает заголовки — особенно эффективно на повторяющихся (cookies, user-agent).
  5. Server push в HTTP/2 — неудачная фича, де-факто заменена 103 Early Hints.
  6. TCP HoL blocking остаётся проблемой — его решает HTTP/3 через QUIC.
  7. Версия согласуется через ALPN в TLS handshake. HTTP/2 в реальности всегда через HTTPS.

HTTP/2 multiplexing с точки зрения API: connection pooling и gRPC Kafka producer и HTTP/2: параллелизм на уровне протокола
Проверка знанийKnowledge check
У вас API на бэкенде, в нём идёт сотни параллельных запросов от SPA. На HTTP/1.1 это работало плохо (открывались 6 TCP, остальное в очереди). Перешли на HTTP/2 -- стало значительно быстрее. Но через месяц коллега говорит: 'давай попробуем HTTP/3, там ещё лучше'. Когда HTTP/3 даст реальный выигрыш, а когда переход не оправдает усилий?
ОтветAnswer
HTTP/3 устраняет TCP head-of-line blocking, которое в HTTP/2 -- единственный значимый источник задержек при мультиплексировании. Решает ли это вашу проблему -- зависит от вашей сети. Реальный выигрыш HTTP/3 будет в условиях: (1) Высокий packet loss. Если канал между клиентом и сервером теряет 1-5% пакетов (мобильные сети, дальние международные ссылки, busy hour в datacenter), то TCP HoL ощутим -- HTTP/3 даст 10-30% ускорение. (2) Мобильные пользователи. 4G/5G имеют высокий jitter и эпизодические потери. На стабильной wired-сети разница минимальна. (3) Connection migration. Если ваше приложение работает на мобильных и пользователи часто переключаются между WiFi и LTE -- HTTP/3 продолжает соединение, TCP пришлось бы переустанавливать (snapchat, mobile streaming). (4) Cross-continent latency. На высоком RTT (200+ мс) экономия handshake'ов в QUIC становится заметнее. Когда переход НЕ оправдает усилий: (a) ваш бэкенд за reverse proxy в data center с практически нулевой потерей и low latency -- внутренние API на HTTP/2 уже идеальны; (b) если ваш upstream (nginx + Python backend) не поддерживает HTTP/3 нативно -- придётся усложнять инфраструктуру; (c) если основная аудитория на стабильном wired-internet -- разница пара процентов. Главное -- измеряйте. Включите HTTP/3 на части traffic'а через Cloudflare/Fastly, сравните p95 latency и lcp в RUM-метриках. Не надо переходить из принципа "новее лучше".

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. В чём принципиальное отличие 'мультиплексирования' в HTTP/2 от 'pipelining' в HTTP/1.1?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6