HTTP/2
C Web Framework поддерживает HTTP/2 (RFC 9113) — вторую версию протокола HTTP с мультиплексированием запросов, бинарным фреймингом, сжатием заголовков HPACK и управлением потоком.
Кратко
HTTP/2 включается автоматически для любого сервера с настроенным TLS — через согласование ALPN. Отдельный флаг или секция конфигурации не требуются. Клиенты, не поддерживающие h2, прозрачно получают HTTP/1.1.
Как это работает
В рамках TLS-рукопожатия сервер через ALPN сообщает о поддержке h2 и http/1.1, отдавая предпочтение h2. Если клиент тоже предлагает h2 — соединение переходит в режим HTTP/2, иначе остается на HTTP/1.1. Согласование ALPN происходит после выбора виртуального хоста по SNI, поэтому HTTP/2 доступен индивидуально для каждого сервера.
{
"servers": {
"s1": {
"domains": ["example.com"],
"ip": "0.0.0.0",
"port": 443,
"tls": {
"fullchain": "/etc/ssl/certs/fullchain.pem",
"private": "/etc/ssl/private/privkey.pem",
"ciphers": "TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 ECDHE-RSA-AES256-GCM-SHA384"
},
"http": {
"routes": { "/": { "GET": { "file": "...", "function": "index" } } }
}
}
}
}Этого достаточно: при наличии секции tls сервер начинает принимать h2-соединения. Дополнительная конфигурация нужна только для настройки поведения и для h2c.
h2c через plaintext
HTTP/2 без TLS (h2c) поддерживается в двух вариантах и включается явно — сервер не переключается на h2c автоматически:
1. Upgrade (RFC 9113 §3.2) — запрос HTTP/1.1 с заголовками Upgrade: h2c и HTTP2-Settings получает ответ 101 Switching Protocols, после чего соединение переходит в режим h2. Реализован как готовый middleware middleware_h2c_upgrade:
{
"http": {
"middlewares": ["middleware_h2c_upgrade"],
"routes": { ... }
}
}int middleware_h2c_upgrade(httpctx_t* ctx);
// вернёт 0 — если запрос был обновлён (101 отправлен, цепочка остановлена)
// вернёт 1 — для обычного запроса (выполнение продолжается до обработчика)2. Prior-knowledge (RFC 9113 §3.4) — клиент сразу отправляет 24-байтный magic preface PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n. Сервер автоматически распознаёт такую сигнатуру на plaintext-соединении и открывает h2-сессию без рукопожатия Upgrade.
Только plaintext
h2c имеет смысл исключительно для серверов без TLS. Для HTTPS-серверов работает обычный h2 через ALPN — middleware h2c_upgrade там не нужен.
Возможности
Мультиплексирование
Несколько одновременных запросов в одном TCP-соединении. Каждый запрос — отдельный поток (stream); обработчики разных потоков выполняются параллельно. Сервер анонсирует до 100 конкурентных потоков на соединение (MAX_CONCURRENT_STREAMS), избыточные потоки получают RST_STREAM/REFUSED_STREAM.
Управление потоком
Двухуровневый flow control (соединение + поток), окно по умолчанию 65535 байт. Окно приёма автоматически масштабируется под пропускную способность (RTT берётся из TCP_INFO) вплоть до http2_recv_window_max. Это исключает зависание передачи больших тел при узком начальном окне.
HPACK
Сжатие заголовков через статическую и динамическую таблицы HPACK с кодированием Хаффмана. Поля валидируются строго по tchar-правилам — строже буквы RFC, как в nghttp2.
Трейлеры
Заголовки, отправляемые после тела ответа (например grpc-status для gRPC), через HEADERS-фрейм с END_STREAM:
int handler_with_trailers(httpctx_t* ctx) {
httpresponse_t* res = ctx->response;
res->add_header(res, "Content-Type", "application/grpc");
send_data(ctx, "...payload...");
/* Трейлер отправится после тела как отдельный HEADERS-фрейм */
res->add_trailer(res, "grpc-status", "0");
res->add_trailer(res, "grpc-message", "OK");
return 1;
}Только HTTP/2
Трейлеры работают только по h2. По HTTP/1.1 add_trailer() вернёт 0 и запишет предупреждение в лог — chunked-кодирование с заголовком Trailer не поддерживается.
Early Hints (103)
Промежуточный ответ 103 Early Hints (RFC 8297) позволяет браузеру начать предзагрузку ресурсов, пока обработчик ещё формирует основной ответ. Это рекомендуемая замена Server Push:
int handler_with_hints(httpctx_t* ctx) {
httpresponse_t* res = ctx->response;
/* Подсказки отправляются ДО основного ответа */
res->add_early_hint(res, "Link", "</style.css>; rel=preload; as=style");
res->add_early_hint(res, "Link", "</app.js>; rel=preload; as=script");
res->send_early_hints(res); /* → клиент получает 103 */
/* ... обработчик работает ... */
send_data(ctx, "<!doctype html>..."); /* → финальный ответ */
return 1;
}send_early_hints() можно вызывать несколько раз; любой вызов после начала финального ответа игнорируется (1xx обязан предшествовать основному ответу).
WebSocket поверх HTTP/2
Extended CONNECT (RFC 8441) — WebSocket-сессия внутри h2-потока вместо отдельного соединения. Клиент инициирует :method: CONNECT с :protocol: websocket. Обработчики WebSocket остаются неизменными: туннель прозрачен для прикладного кода, работают broadcasting и permessage-deflate.
Чтобы vhost принимал такие соединения, у него должна быть включена секция websockets — иначе Extended CONNECT получит 501 Not Implemented.
Настройка
Поведенческие параметры HTTP/2 — это переменные окружения из секции main.env в config.json. Читаются один раз при запуске; значения по умолчанию подобраны для типичной нагрузки.
Жизненный цикл и поток
| Параметр | По умолчанию | Описание |
|---|---|---|
http2_idle_timeout_sec | 120 | Таймаут простоя соединения (сек). 0 — отключить |
http2_ping_interval_sec | 0 | Отправлять PING после N сек тишины. 0 — контроль отключён |
http2_ping_ack_timeout_sec | min(interval, 15) | Ожидание ACK на PING, после которого соединение закрывается |
http2_settings_ack_timeout_sec | 10 | Таймаут подтверждения SETTINGS (§6.5.3). 0 — отключить |
http2_recv_window_initial | 65535 | Начальный размер окна приёма |
http2_recv_window_max | 4194304 | Потолок авторасширения окна (4 МБ). Равно initial — отключить масштабирование |
http2_write_quantum | 65536 | Сколько байт поток отдаёт за раунд, прежде чем уступить сокет (мин. 1024) |
http2_idle_timeout_sec — соединение закрывается, когда в нём нет открытых потоков и активности не было N секунд. Соединение с идущим запросом или недописанным ответом «простаивающим» не считается, даже если клиент замолчал навсегда, — пропавшего посреди работы клиента ловит PING-сторож (следующий параметр). Закрытие мягкое: клиент получает GOAWAY и успевает завершить начатое.
http2_ping_interval_sec — сторож для полуживых клиентов. Если клиент молчит N секунд, сервер отправляет PING и ждёт подтверждения; не дождался — соединение закрывается. В отличие от idle-таймаута, сторож работает и при открытых потоках. По умолчанию выключен: здоровому соединению keepalive не нужен, а тишину без потоков убирает idle-таймаут. Включайте, когда серверу нечего отправлять по своей инициативе (долгополлинг, редкие события), а разрывать зависшее соединение должен сервер, а не клиентский таймаут.
http2_ping_ack_timeout_sec — сколько ждать ACK на сторожевой PING, прежде чем признать клиента пропавшим. Умолчание вычисляется из интервала — min(interval, 15) — чтобы даже с редкими интервалами пропавший клиент ловился за ограниченное время. На соединении одновременно висит только один PING.
http2_settings_ack_timeout_sec — правило RFC 9113 §6.5.3: клиент обязан подтвердить наши SETTINGS, и только после подтверждения применяются анонсированные лимиты — окна, число потоков, размер заголовков. Клиент, который не подтверждает вовсе, получает GOAWAY(SETTINGS_TIMEOUT); без этой проверки он мог бы просидеть до самого idle-таймаута. 10 секунд — с запасом: «как можно скорее» не значит «мгновенно», и медленно стартующий клиент срезаться не должен.
http2_recv_window_initial — окно приёма, с которого соединение начинается; анонсируется в серверном префейсе и для соединения, и для каждого потока. Умолчание 65535 — минимум RFC, и он же потолок скорости: входящая не выше окна/RTT, что на канале со 100 мс даёт ~0,6 МБ/с на соединение, сколько бы потоков ни грузило. Дальше вступает автомасштабирование — окно растёт к измеренному произведению полосы на RTT. Меньше 65535 задать нельзя (значение поднимается до него), больше — вплоть до 2³¹−1.
http2_recv_window_max — потолок автомасштабирования. Каждый RTT сервер смотрит, сколько байт реально пришло, и если окно мало — удваивает его, до потолка; выученное значение переносится на новые потоки, чтобы каждый запрос не разгонял окно заново. Правило то же, что в TCP: окно должно вмещать полосу×RTT, иначе скорость упирается в окно, а не в канал. Равенство с initial выключает масштабирование и фиксирует окно. Это окно только приёма: анонсируя его, сервер рискует своей памятью, а тела больших запросов выгружаются во временный файл, а не копятся в ней.
http2_write_quantum — сколько байт тела один поток может положить в сокет за раунд, прежде чем запись уступит место следующему готовому потоку. Без предела большой ответ занял бы сокет от начала до конца, и мелкие запросы того же соединения ждали бы весь файл — ровно то head-of-line-блокирование, которое мультиплексирование должно убрать. 64 КБ — четыре DATA-фрейма стандартного размера: достаточно много, чтобы лишний проход по циклу записи был шумом на фоне копирования, и достаточно мало, чтобы запрос, стоящий позади большой загрузки, ждал миллисекунды, а не её длительность. Больше — отдача на соединениях с одной большой передачей, меньше — отзывчивость, когда мелких ответов много. RFC здесь молчит: приоритеты потоков в RFC 9113 объявлены устаревшими, и планирование между готовыми потоками — целиком решение сервера.
Защита от злоупотреблений (DoS)
| Параметр | По умолчанию | Описание |
|---|---|---|
http2_max_header_list_size | 32768 | Лимит размера блока заголовков. 0 — отключить |
http2_max_continuation_frames | 64 | Лимит CONTINUATION-фреймов на блок. 0 — без лимита |
http2_abort_rate | 100 | Бюджет Rapid Reset (RST/сек, CVE-2023-44487). 0 — отключить |
http2_abort_burst | 200 | Пиковый размер бакета Rapid Reset |
http2_ctrl_rate | 100 | Бюджет фреймов, заставляющих сервер работать без продвижения (PING, SETTINGS, пустые DATA). 0 — отключить |
http2_ctrl_burst | 200 | Пик этого бакета |
http2_max_out_backlog | 1048576 | Предел очереди ответов для пира, который перестал читать: при превышении соединение закрывается GOAWAY(ENHANCE_YOUR_CALM). 0 — отключить |
http2_max_header_list_size — лимит на суммарный размер декодированных заголовков запроса. Мягкое превышение — ответ 431 Request Header Fields Too Large, соединение выживает; жёсткое (×8) — GOAWAY(ENHANCE_YOUR_CALM): список в восемь раз больше лимита — не запрос, а атака. Лимит ограничивает и память до декодирования: блок заголовков в h2 собирается из HEADERS и цепочки CONTINUATION целиком, поэтому потолок этого блока привязан к лимиту с запасом на хаффмановское сжатие — ужимая лимит, вы ужимаете и худший случай декодера.
http2_max_continuation_frames — лимит CONTINUATION-фреймов на один блок заголовков. Байтовый предел ограничивает память, но не работу: пустой CONTINUATION не добавляет ни байта, и без счётчика фреймов клиент мог бы крутить цикл сборки блока сколько угодно. Превышение — GOAWAY(ENHANCE_YOUR_CALM).
http2_abort_rate / http2_abort_burst — бюджет Rapid Reset (CVE-2023-44487): клиент открывает поток и тут же сбрасывает его RST_STREAM. Каждый такой поток стоит диспетчеризации и декодирования HPACK, почти не занимая слот конкурентности, — поэтому MAX_CONCURRENT_STREAMS против этой атаки не защищает вовсе. Отмена потока списывается из бакета; пик бакета не бывает ниже лимита одновременных потоков, чтобы клиент, просто отменяющий всё открытое, бакет не исчерпал. Серии отмен закрывают соединение GOAWAY(ENHANCE_YOUR_CALM).
http2_ctrl_rate / http2_ctrl_burst — отдельный бакет для фреймов, которые заставляют сервер работать, ничего не продвигая: PING без нагрузки (обязаны ответить — CVE-2019-9512), SETTINGS без подтверждения (обязаны подтвердить — CVE-2019-9515), DATA нулевой длины (не тратит окно контроля потока — CVE-2019-9518), устаревший PRIORITY (CVE-2019-9513), WINDOW_UPDATE для несуществующего потока. Всё это легально и дёшево отправляющему. Бакет отдельный, а не общий с отменами потоков: у честного клиента скорости этих категорий не связаны, и общий пришлось бы настраивать по слабейшей. Исчерпание — GOAWAY(ENHANCE_YOUR_CALM).
http2_max_out_backlog работает в обратную сторону — он защищает от клиента, который перестал читать. Сокет отправки забивается, записать больше нечего, и каждый кадр, который сервер обязан отправить (ответы параллельных потоков), копится в очереди соединения. Предел останавливает рост очереди и закрывает соединение GOAWAY(ENHANCE_YOUR_CALM), пока оно не съело память: без него один зависший клиент удерживал бы накопление неограниченно. 0 отключает предел — стоит только там, где перед клиентом есть свой ограничитель.
Точные счётчики всех срабатываний — в секции http2_abuse маршрута /metrics.
Пример настройки:
{
"main": {
"env": {
"http2_idle_timeout_sec": 60,
"http2_ping_interval_sec": 30,
"http2_max_header_list_size": 16384
}
}
}Ограничения
| Возможность | Статус | Комментарий |
|---|---|---|
| Server Push | Убран по решению | Был реализован и прошёл h2spec, затем удалён: Chrome убрал push в 106, Firefox/Safari по умолчанию выключен. Замена — 103 Early Hints |
| Plain CONNECT | Отклонён | Запрещён намеренно, чтобы сервер не стал открытым прокси. Не путать с Extended CONNECT для WebSocket |
| HTTP/2-клиент | Нет | Только серверная роль. HTTP-клиент фреймворка работает по HTTP/1.1 |
| Трейлеры / 103 по HTTP/1.1 | Нет | Эти возможности существуют только в h2 |
| PRIORITY | Игнорируется | SETTINGS_NO_RFC7540_PRIORITIES = 1; приоритеты устарели в RFC 9113 |
| Приоритеты (RFC 9218) | Не реализованы | Заголовок priority и PRIORITY_UPDATE здесь игнорируются; готовые потоки планируются по кругу с шагом http2_write_quantum. Схема реализована в HTTP/3 |
Проверка
curl
# Принудительно HTTP/2 (нужен TLS)
curl -v --http2 https://example.com/
# Проверка согласования ALPN
curl -v https://example.com/ 2>&1 | grep ALPN
# → * ALPN: server accepted h2.nghttp
# h2c через Upgrade на plaintext-сервере
nghttp -v http://example.com/
# h2 over TLS
nghttp -v https://example.com/Соответствие стандарту
Реализация проходит h2spec (147 тестов, 0 ошибок по TLS):
h2spec -S -h example.com -p 443