Девять частей мы прокладывали дорогу: нашли адрес, открыли соединение, зашифровали его. Только теперь браузер наконец говорит то, ради чего всё затевалось: «дай мне страницу». Это HTTP — самый простой протокол во всей цепочке, текстовый и читаемый глазами. Посмотрим, как выглядит запрос и ответ, и чем три живые версии протокола отличаются друг от друга.
Это десятая часть цикла «Что происходит, когда открываешь сайт».
Запрос и ответ руками
HTTP/1.1 настолько прост, что его можно набрать вручную. openssl s_client откроет TLS-соединение, а дальше мы просто пишем текст:
$ printf 'GET /robots.txt HTTP/1.1\r\nHost: alekseychikun.ru\r\nConnection: close\r\n\r\n' \
| openssl s_client -quiet -connect alekseychikun.ru:443 -servername alekseychikun.ru -alpn http/1.1
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Fri, 16 Oct 2026 09:18:21 GMT
Server: nginx
Vary: Accept-Encoding
X-Cache-Status: MISS
Connection: close
Transfer-Encoding: chunked
af
User-agent: *
Disallow: /wp-admin/
…
0
Всё сообщение — строки текста, разделённые \r\n. Пустая строка отделяет заголовки от тела. Разберём, что здесь важно.
Запрос
- первая строка — метод, путь и версия:
GET /robots.txt HTTP/1.1. Метод говорит, что сделать: GET — получить, POST — отправить данные, HEAD — только заголовки; Host— имя сайта. На одном адресе живут десятки сайтов, и без этого заголовка сервер не знает, чей файл отдать. Это и есть главная причина, почему у одного IP может быть много сайтов;Connection: close— «после ответа закрой». Без него соединение остаётся открытым для следующих запросов.
Ответ
- код —
200 OK. Первая цифра — класс: 2xx — успех, 3xx — иди по другому адресу, 4xx — ошибка в запросе (404 — нет такого, 403 — нельзя), 5xx — сломалось на сервере; Content-Type— что внутри и в какой кодировке. По нему браузер решает, показывать как страницу, текст или скачивать;Vary: Accept-Encoding— подсказка кэшам: ответ зависит от того, умеет ли клиент сжатие. Кэш должен хранить сжатую и несжатую копии отдельно;X-Cache-Status— наш собственный заголовок: показывает, взят ли ответ из кэша страниц (HIT) или собран заново (MISS);Transfer-Encoding: chunked— сервер не знал заранее длину ответа и шлёт тело кусками.af— длина куска в шестнадцатеричном виде (175 байт),0— конец.
Сжатие: в пять раз меньше
Если клиент написал в запросе Accept-Encoding: gzip, br, сервер сожмёт ответ. Страница блога этого сайта:
$ curl -so /dev/null -w '%{size_download}\n' https://alekseychikun.ru/blog/
44912
$ curl -so /dev/null -H 'Accept-Encoding: gzip, br' -w '%{size_download}\n' https://alekseychikun.ru/blog/
8750
44,9 КБ без сжатия против 8,8 КБ со сжатием — в пять с лишним раз. Помните «начальную порцию» TCP из пятой части, около 14 КБ? Сжатая страница влезает в неё целиком и приходит за один круг. Несжатая — нет. Браузеры просят сжатие всегда, а вот самописные скрипты и интеграции часто забывают — и платят трафиком и временем.
HTTP/2: один провод, много разговоров
Наш curl из седьмой части договорился в ALPN о версии h2. Вот как выглядит тот же запрос в HTTP/2:
* [HTTP/2] [1] OPENED stream for https://alekseychikun.ru/robots.txt
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: alekseychikun.ru]
* [HTTP/2] [1] [:path: /robots.txt]
Смысл тот же, форма другая. HTTP/2 — бинарный: заголовки упакованы и сжаты, первая строка превратилась в псевдозаголовки :method, :path, а Host стал :authority. Главное новшество — потоки: [1] в выводе — номер потока. По одному соединению параллельно идут десятки запросов, и ответы приходят вперемешку, кусками.
В HTTP/1.1 запросы по соединению идут строго по очереди, поэтому браузеры открывали к сайту по шесть соединений и всё равно ждали. Со второй версией хватает одного — и одного рукопожатия TCP и TLS на всю страницу.
HTTP/3: долой TCP
У HTTP/2 осталась одна беда, и она в TCP. Если потерялся один пакет, TCP задерживает всё, что пришло после него, пока потерянное не перешлют, — даже данные других потоков, которые к потерянному не относятся. На мобильной сети с потерями это заметно.
HTTP/3 работает поверх QUIC — протокола на UDP, который сам следит за доставкой, но для каждого потока отдельно. Потеря в одном потоке не тормозит другие. Заодно QUIC объединяет рукопожатие соединения и TLS в одно: новое соединение стоит один круг вместо двух, а повторное — может не стоить ни одного.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| формат | текст | бинарный | бинарный |
| под ним | TCP + TLS | TCP + TLS | QUIC (UDP), TLS 1.3 встроен |
| параллельные запросы | по очереди, 6 соединений | потоки в одном соединении | потоки, независимые при потерях |
| новое соединение | 2 круга (TCP + TLS 1.3) | 2 круга | 1 круг |
Как браузер узнаёт, что сайт умеет HTTP/3: первый ответ по HTTP/2 содержит заголовок Alt-Svc: h3=":443", и следующие запросы браузер пробует по QUIC. Если UDP на пути режется, тихо остаётся на HTTP/2. Этот сайт пока отвечает без Alt-Svc, то есть работает на HTTP/2.
Шпаргалка по кодам, которые вы увидите чаще всего
| Код | Что значит | Где искать причину |
|---|---|---|
| 301 / 308 | насовсем переехало по другому адресу | поисковики переносят вес на новый адрес |
| 302 / 307 | временно по другому адресу | часто — переадресация на вход |
| 304 | не изменилось, берите из своего кэша | норма, экономит трафик |
| 403 | нельзя | права на файлы, правило защиты, бан |
| 404 | нет такого | опечатка в адресе, удалённая страница |
| 499 | клиент ушёл, не дождавшись (только в журналах nginx) | медленный ответ или робот |
| 502 | прокси не получил ответ от сервера за ним | упал PHP или приложение |
| 504 | прокси не дождался ответа | зависший запрос, нет таймаута |
Сайт отвечает странными кодами или медленно отдаёт ресурсы?
Пришлите адрес — посмотрю заголовки, сжатие и версию протокола и скажу, что включить.
// загружаю комментарии…