Часто доводится слышать, будто протокол HTTP прост. Естественно, чаще всего — от тех, кто не слишком поднаторел в обращении с ним и слабо представляет, как именно этот протокол реализован. Думаю, сам я тоже мыслил в таком ключе, когда ещё только начинал работать с HTTP.
К настоящему времени я уже без малого три десятка лет усердно пишу клиентский код, взаимодействующий с HTTP. Я участвую в работе IETF и приложил руку ко всем спецификациям HTTP, составленным примерно с 2008 года. Поэтому полагаю, что вполне имею право развёрнуто высказаться об этом протоколе. Протокол HTTP не прост. Далеко не прост. Даже если предположить, что те, кто отмечает его простоту, имеют в виду лишь HTTP/1.
Идея и концепция HTTP, пожалуй, до сих пор могут считаться простыми, а в чём-то и гениально простыми, но о внутреннем устройстве такого не скажешь.
Правда, действительно можно достучаться до сервера HTTP/1 по telnet, вручную ввести команду GET / и посмотреть ответ. Но я не думаю, что одного этого достаточно, чтобы считать весь инструмент простым.
Не думаю, что кто-то пытался настаивать на простоте HTTP/2 или HTTP/3. Чтобы как следует реализовать вторую или третью версию, для начала необходимо реализовать версию 1, и в данном случае сложность накапливается, не говоря уж о том, что в спецификациях новых версий HTTP как таковых немало своих нетривиальных аспектов.
Давайте подробно разберём некоторые детали протокола HTTP/1, с учётом которых я берусь утверждать, что он не прост.
Переходы на новую строку
HTTP имеет не просто текстовую, но, более того, построчную основу; это касается его заголовочных частей. Строка может быть сколь угодно длинной, так как в спецификациях она не ограничивается — но в конкретных реализациях такое ограничение предусматривать нужно, чтобы защититься от DoS-атак и т.д. Какой длины может достичь строка, прежде, чем сервер её отклонит? Каждая строка заканчивается возвратом каретки и переводом строки. Но бывает и так, что достаточно лишь перевода строки.
Кроме того, заголовки не закодированы в UTF-8, они являются октетами, поэтому нельзя рассчитывать, что через них вы сможете произвольно передавать всё, что вам заблагорассудится.
Пробелы
Такая проблема легко возникает с протоколами, основанными на тексте. Между полями могут оказаться один или несколько пробелов. Среди них есть как обязательные, так и опциональные. Зачастую HTTP также использует токены, и токен может представлять собой как последовательность символов без каких-либо пробелов, так и текст, заключённый в двойные кавычки (“). В некоторых вариантах текст обязательно заключается в кавычки.
Конец тела документа
Существует не один способ, позволяющий убедиться, что загрузка документа по HTTP/1 завершилась – то есть, что достигнут конец «тела» документа. Таких способов даже не два, а как минимум три (проверка длины содержимого, кодировка частями и проверка закрытия соединения). В двух из этих вариантов требуется, чтобы HTTP-клиент разобрал контент, предоставленный в текстовом формате, и определил его размер. Из-за того, что есть множество вариантов окончания тела документа, с годами возникли многочисленные проблемы с безопасностью, касающиеся протокола HTTP/1.
Синтаксический разбор чисел
Если числа представлены как текст, то синтаксический разбор такой информации идёт медленно, а иногда чреват ошибками. Следует особенно внимательно избегать целочисленных переполнений, правильно обрабатывать пробелы, префиксы +/-, ведущие нули и пр. Текст, который легко читать человеку, совсем не так удобен для машин.
Завёрстывание заголовков
Мало нам проблем с заголовками произвольной длины и неочевидными окончаниями строк — так оказывается, что заголовки ещё могут «завёрстываться», причём, двумя способами. Во-первых, прокси-сервер может объединить несколько заголовков в один, просто расставив между ними разделительные запятые. Это касается почти всех заголовков (кроме, например, куки). Во-вторых, сервер может отправить очередной заголовок как продолжение предыдущего, добавив ведущий пробел. Сейчас так делают редко (а в новейших версиях спецификаций это прямо не рекомендуется), но при реализации протокола нужно уделять внимание этой детали, поскольку такая практика до сих пор существует.
Никогда не реализовывалось
В HTTP/1.1 амбициозно добавляли такие фичи, которые на тот момент не использовались или не получили широкого распространения в общедоступном интернете. Например, в спецификации описано, как работает конвейеризация HTTP, но, если вы попробуете задействовать эту фичу «на местности», то сами напрашиваетесь на проблемы и… всё окончится грустно.
В более новых версиях HTTP добавились фичи, которые в большей степени отвечают тем критериям, которые не удовлетворяются средствами такой конвейеризации; в основном подобные задачи решаются через мультиплексирование.
Код состояния 100 примерно такого же рода: он описан в спецификации, но на практике используется редко. В новых реализациях от него одни проблемы. Совсем недавно возникла дискуссия о различных деталях обработки состояния при отклике 100, притом, что первый вариант спецификации, в котором он учитывается, опубликован 28 лет назад. Думаю, это говорит о многом.
Множество заголовков
В спецификации HTTP/1 рассмотрено множество заголовков и рассказано, как они работают, но этого не хватает, чтобы поддерживать нормальную современную реализацию HTTP. Дело в том, что такие вещи как куки-файлы, аутентификация, новые коды отклика и различные другие вещи, которые могут потребоваться в реализации, не охвачены основной спецификацией и описаны в отдельных документах. Некоторые детали, например, NTLM, не найти даже в документах RFC.
Следовательно, современному клиенту для HTTP/1 приходится реализовывать и поддерживать целый спектр дополнительных вещей и заголовков, чтобы нормально работать в широком Интернете. «HTTP/1.1» упоминается как минимум в 40 отдельных RFC-документах. Некоторые из них весьма сложны сами по себе.
Метод методу рознь
Притом, что в идеале должно быть можно использовать совершенно одинаковый синтаксис независимо от того, с каким именно методом вы работаете (иногда методы называются verb), в реальности всё совсем не так.
Например, при работе с методом GET действительно можно послать тело сообщения прямо в запросе примерно так, как это делается с POST и PUT. Но, поскольку ранее это ни разу как следует не проговаривалось, в настоящий момент с интероперабельностью этой функции накопились большие проблемы. Этот метод — верный путь к провалу, и примеров его неудачной реализации в Вебе более чем достаточно.
В частности, именно поэтому сейчас разрабатывают новый HTTP-метод QUERY, который должен работать как GET + тело запроса. Но протокол от этого не упрощается.
Заголовок заголовку рознь
Поскольку некоторые заголовки создавались, развёртывались и развивались сами по себе, прокси-сервер не может просто вслепую объединить два заголовка в один, хотя, в шаблонных правилах это разрешено. Но есть заголовки, идущие вразрез с этими правилами, поэтому и обращаться с ними нужно иначе. Типичный пример — куки.
Бесхребетные браузеры
Помните, как в браузерных реализациях протоколов всегда предпочитается что-то показать пользователю, а затем угадать, чего он хотел — вместо того, чтобы вывести ошибку? Дело в том, что при излишней строгости в этом вопросе есть риск, что пользователь откажется от вашего браузера в пользу другого, более снисходительного.
Это повлияло на практику обращения с HTTP во всех остальных сферах, поскольку пользователь рассчитывает: то, что работало в браузере, определённо должно работать и вне браузеров, а также в соответствующих реализациях HTTP.
В результате становится не столь важно уметь интерпретировать и понимать спецификацию — достаточно лишь придерживаться той линии, что избрана основными браузерами в каждом конкретном случае. Со временем браузеры даже могут пересматривать те или иные позиции, и в некоторых случаях они могут прямо противоречить тому, что сказано в спецификации.
Размеры спецификаций
В первом документе по HTTP/1.1 RFC 2068 от января 1997 года насчитывалось 52165 слов обычного текста. Это почти втрое больше, чем объём документа RFC 1945 по версии HTTP/1.0, где было всего 18615 слов. Красноречиво свидетельствует, насколько усложнился в версии 1.1 протокол, который, пожалуй, в версии HTTP 1.0 ещё был прост.
В июне 1999 года обновлённая версия RFC 2616 увеличилась ещё на несколько сотен строк и завесила на 57 897 слов. То есть, почти на 6000 слов больше.
Затем в рамках IETF была проделана огромная работа, и за последующие 15 лет спецификация HTTP/1.1, которая когда-то являлась единым документом, оказалась разделена на шесть отдельных документов.
В июне 2014 года были опубликованы запросы на спецификацию от RFC 7230 до RFC 7235, в них в совокупности насчитывается 90 358 слов. Материал прирос ещё на 56%. Теперь он сравним по размеру со средним романом.
Впоследствии вся спецификация переупорядочивалась и реорганизовывалась, чтобы лучше соответствовать новым версиям HTTP. Последнее обновление было опубликовано в июне 2022 года. После этого элементы HTTP/1.1 были умещены в три документа от RFC 9110 до RFC 9112 общим объёмом в 95 740 слов.
Навскидку, допустим, кто-то в состоянии штудировать эти документы со скоростью 200 слов в минуту. Пожалуй, это чуть ниже средней скорости чтения, но посудите сами – спецификацию-стандарт всё-таки читаешь несколько медленнее, чем беллетристику. При этом допустим, что 10% слов — это мусор, который можно пропускать.
Таким образом, если без остановки прочитать всего три самых свежих RFC, касающихся HTTP/1.1, на это уйдёт более семи часов.
В топку?
В одном недавнем докладе с достаточно кликбейтным названием автор предположил, что, если HTTP/1 в самом деле так сложно реализовать правильно, от него вообще следует отказаться.
А иначе никак?
Всё это верно, но, тем не менее, в Интернете совсем мало протоколов, которые могут потягаться с HTTP/1 по широте использования, усвоенности и популярности. HTTP — это флагман Интернета. Может быть, такой уровень сложности — необходимая составляющая такого успеха?
Сравнивая его с другими популярными протоколами, которые до сих пор в ходу, например, с DNS или SMTP, думаю, что им присущи схожие черты: все они зародились довольно давно как весьма простые технологии. Прошли десятилетия, и простыми их уже не назовёшь.
Возможно, просто такова жизнь?
Заключение
HTTP — не простой протокол.
Вероятно, в будущем всё ещё более усложнится по мере того, как HTTP будет от версии к версии обрастать новыми фичами.
Комментарии (11)
CaptainFlint
09.08.2025 23:29А если взять спецификацию языка C++, то там 870 тысяч слов. И как вообще на нём писать умудряются…
korvin13
09.08.2025 23:29Протокол http прост. Можно конечно придумать сложности по любому вопросу, тем не мение ... Этот протокол хорошо документирован. Я работаю с ним тоже не 1 десяток и версии 1 и 1.1 довольно просты в реализации.
acDev
09.08.2025 23:29Например, в спецификации описано, как работает конвейеризация HTTP, но, если вы попробуете задействовать эту фичу «на местности», то сами напрашиваетесь на проблемы и… всё окончится грустно.
Верно подмечено. Я изучал имплементацию pipelining'а на примере некоторых топовых фреймворков из списка TechEmpower FrameworkBenchmarks. Разрабы делают реализацию просто в лоб и тупо. К примеру, если скорость приёма сервера больше, чем его отдача, то просто пожирается память на выделение всё новых буферов в памяти.
Вот тут я немного возмущался: https://github.com/TechEmpower/FrameworkBenchmarks/discussions/8048
Я же в проекте fastwsgi сделал эту часть HTTP-сервера по умному. Т.е. сервер умеет притормаживать чтение входящих запросов, в зависимости от состояния части, отвечающей за отправку данных.
---------------
Ну и ссылочка на топик, где я сетую на то, что в Python-лидерах находится "поделка", которая просто базовые требования TCP-сервера не соблюдает, не то что HTTP:
https://github.com/TechEmpower/FrameworkBenchmarks/issues/9055
Вот такие чудеса на виражах.
Kelbon
09.08.2025 23:29Интересные цифры в бенчмарках, как-то не верится в 9МЛН запросов в секунду, не разбирались откуда взялись эти цифры?
И отдельный вопрос, зачем реализовывать HTTP 1, если существует HTTP/2?
acDev
09.08.2025 23:29Разбирался. Всё так и есть. Даже было время, когда на работе 99% такую же железку взял и установил там некоторые бенчи и лично проверил:
https://github.com/TechEmpower/FrameworkBenchmarks/issues/7402#issuecomment-1482918369
Получил довольно близкие цифры.
Раньше у них стояли 10Gbit сетевухи, а теперь уже 40Gbit. Мне на работе удалось проверить только 10Gbit и 25Gbit.
Вот точный конфиг: https://github.com/TechEmpower/FrameworkBenchmarks/issues/8736#issuecomment-1973835104
Kelbon
09.08.2025 23:29не понимаю, там фигурируют цифры количества потоков, количества соединений. То есть это не то что я думал на один поток и одно соединение, а
20 threads and 512 connections Thread Stats Avg Stdev Max +/- Stdev Latency 208.87us 167.10us 12.36ms 83.52% Req/Sec 1.07M 50.41k 1.53M 70.88%
20-тредов и 512 соединений, при этом запросы это буквально / и отсылаются они громадными пачками (///////) и сервер их также обрабатывает. Это настолько нереалистичный сценарий, что лучше было просто замерять скорость интернета
Zalechi
09.08.2025 23:29Точно та же самая история и с программированием. На пике всех этих курсов по программированию, даже тут на Хабре встречалось много людей утверждавших, что математика для кодера не нужна…
Ага, думал я. Какой из тебя кодер без матеши, такой же простой протокол HTTP.
MonkAlex
Я ждал чего-то интересного. Жаль, что мои ожидания - мои проблемы.