HTTP — это язык, на котором браузер разговаривает с сервером, а сервисы между собой. Сам смысл запросов почти не менялся десятилетиями: GET /orders/42, заголовки, тело, код ответа. А вот то, как эти запросы упаковываются и бегут по сети, за это время переписали трижды. Так появились HTTP/1.1, HTTP/2 и HTTP/3 — три версии одного протокола, каждая быстрее предыдущей.

Хорошая новость: ваш код почти всегда остаётся прежним. Разбираться в версиях нужно не чтобы переписывать логику, а чтобы понимать, почему одна страница грузится рывками, а другая плавно, и что за галочку «HTTP/2» вы включаете на балансировщике. Пойдём по порядку — от самого старого к самому новому.

HTTP/1.1: просто, но по очереди

HTTP/1.1 — версия, на которой интернет прожил бо́льшую часть своей истории. Она текстовая: запрос — это буквально строки, которые можно прочитать глазами. Открыли соединение, послали GET /page, получили ответ. Удобно для отладки, легко для понимания.

Раннее неудобство HTTP/1.0 — на каждый запрос новое соединение — здесь уже решено механизмом keep-alive: соединение остаётся открытым, и по нему можно послать несколько запросов подряд, не переустанавливая связь каждый раз. Это экономит время: установка соединения сама по себе не бесплатна (подробнее — в статье про соединения).

Но есть фундаментальное ограничение. По одному соединению запросы идут строго по очереди: послали первый — ждём ответа — только потом можно послать следующий. Если первый ответ большой или сервер задумался, все остальные стоят в очереди за ним. Это называется head-of-line blocking — «блокировка головой очереди»: один медленный элемент впереди держит всех, кто за ним, как одна застрявшая машина держит целую полосу.

Как с этим жили? Браузеры открывали к одному сайту несколько параллельных соединений — обычно шесть. Пока по одному качается картинка, по другому летит стиль, по третьему скрипт. Костыль работает, но дорогой: каждое соединение — это отдельная установка связи, отдельная память на сервере, отдельные накладные расходы. Шесть — потолок, а на тяжёлой странице ресурсов сотни.

HTTP/2: много потоков в одной трубе

HTTP/2 родился именно чтобы убрать эту очередь. Главная идея — мультиплексирование: по одному-единственному соединению одновременно едет много независимых запросов и ответов, не мешая друг другу.

Чтобы это стало возможным, протокол сделали бинарным. Вместо текстовых строк данные режутся на маленькие пронумерованные кусочки — кадры (frames). Каждый кадр помечен, к какому потоку он относится. Сервер и клиент отправляют кадры вперемешку, а на другой стороне собирают их обратно по номерам. Аналогия — не колонна машин на однополосной дороге, а посылки на общей ленте конвейера: едут вперемешку, но у каждой свой адрес, и на выходе их раскладывают по получателям.

Что это даёт на практике:

  • Одно соединение вместо шести. Меньше установок связи, меньше нагрузки на сервер — а параллелизм при этом выше, чем был на шести соединениях.
  • Сжатие заголовков. У каждого запроса ворох повторяющихся заголовков (одни и те же cookie, user-agent, host). HTTP/2 их сжимает и не гоняет одно и то же по сто раз — заметная экономия там, где запросов много.
  • Server push — сервер мог сам, не дожидаясь запроса, дослать клиенту то, что тому наверняка понадобится (например, стиль к странице). На практике идея не прижилась и её постепенно свернули, но в описаниях HTTP/2 она встречается.

Важно: сам смысл запросов не изменился. Те же методы, заголовки, коды ответов — поменялась только «упаковка». Поэтому переход на HTTP/2 обычно не требует трогать код сервиса.

Что осталось нерешённым в HTTP/2

HTTP/2 убрал очередь на уровне самого HTTP. Но он по-прежнему живёт поверх TCP — транспорта, который гарантирует, что байты придут целыми и строго по порядку (разбор TCP — в статье про TCP и UDP).

И вот тут прячется ловушка. TCP отдаёт данные наверх строго по порядку. Если в пути потерялся один пакет, TCP останавливает выдачу всего, что пришло после него, и ждёт, пока потерянный кусок доедет заново. А в этом «всём, что после» лежат кадры разных потоков. Получается: логически потоки независимы, но один потерянный пакет тормозит их все — потому что все они делят одно TCP-соединение.

Это снова head-of-line blocking, только спустившийся на уровень TCP. HTTP/2 честно убрал блокировку у себя, но не мог достать до транспорта под собой. На хорошей сети разница незаметна, а вот на мобильной связи с потерями пакетов HTTP/2 иногда работал не лучше, чем несколько отдельных соединений HTTP/1.1.

HTTP/3: сменить фундамент под протоколом

Чтобы убрать блокировку окончательно, нужно было заменить сам транспорт. Так появился HTTP/3 — тот же бинарный мультиплексируемый HTTP, но поверх QUIC вместо TCP.

QUIC — это транспорт, построенный на UDP (быстром протоколе «отправил и забыл», без гарантии порядка) с досыпанной поверх надёжностью. Ключевое отличие: QUIC знает про потоки. Если теряется пакет одного потока, страдает только этот поток — остальные едут дальше, их никто не держит. Тот самый TCP-level head-of-line blocking исчезает, потому что TCP под протоколом больше нет.

Бонусом QUIC быстрее устанавливает соединение. В TCP сначала идёт рукопожатие транспорта, потом отдельно рукопожатие шифрования — два круга задержки. QUIC складывает их в одно: шифрование встроено в него изначально, и связь поднимается за меньшее число обменов. На мобильной сети, где каждый круг до сервера ощутим, это заметно ускоряет первый ответ.

Цена — QUIC ходит по UDP, а некоторые старые сети и firewall относятся к UDP настороженно. Поэтому HTTP/3 обычно работает как ускорение поверх: клиент и сервер умеют HTTP/2, а при возможности переключаются на HTTP/3, откатываясь обратно, если QUIC не проходит.

Где это применяется

Для backend-сервиса версия HTTP — это не абстракция, а вполне конкретные точки, где она всплывает.

Первое и главное — gRPC работает поверх HTTP/2. Мультиплексирование и бинарные кадры для gRPC не бонус, а требование: множество параллельных вызовов и потоковая передача держатся именно на этом. Если вы поднимаете gRPC-сервис, HTTP/2 у вас уже включён, хотите вы того или нет.

Второе — где включается версия. Обычно не в коде приложения. Наружу смотрит прокси или балансировщик (nginx, Envoy, облачный load balancer), и HTTP/2 или HTTP/3 терминируется на нём: браузер общается с балансировщиком по HTTP/2, а тот дальше до вашего сервиса может идти хоть по обычному HTTP/1.1 — на коротком быстром участке внутри дата-центра разница уже не так важна. Где именно рвётся и пересобирается соединение — тема статьи про балансировщики. Ещё одна деталь: браузеры включают HTTP/2 только поверх шифрования, так что на практике он идёт в паре с HTTPS и TLS.

Где спотыкаются начинающие:

  • Думают, что HTTP/2 меняет смысл запросов. Нет. Методы, заголовки, коды ответов — те же, что в HTTP. Меняется только упаковка на проводе, а ваш обработчик запроса остаётся прежним.
  • Считают, что HTTP/2 полностью убил head-of-line blocking. Он убрал его на уровне HTTP, но на уровне TCP при потере пакета блокировка остаётся — окончательно её снимает только HTTP/3 поверх QUIC.
  • Пытаются «включить HTTP/3 в приложении». Обычно версия живёт на прокси/балансировщике, а не в коде сервиса. Искать переключатель нужно там.
  • Путают версию протокола с шифрованием. HTTP/2 и TLS — разные вещи, просто браузеры требуют их вместе. Версия отвечает за то, как гоняются данные, TLS — за то, что их не прочитать по дороге.

Что учить дальше

Версии HTTP стоят на транспорте под ними, поэтому логично разобрать сам транспорт: TCP и UDP — там же живёт QUIC, на котором держится HTTP/3. Полезно понять и то, что именно версии оптимизируют — установку и переиспользование соединений: keep-alive, рукопожатия, цена нового соединения. Дальше — сам прикладной слой HTTP (методы, коды, заголовки) и шифрование поверх него, HTTPS и TLS, в паре с которым HTTP/2 и HTTP/3 обычно и работают.