Привет! Меня зовут Глеб Гончаров, я руководитель группы разработки клиентского продукта в СберМаркете. В серии статей я рассмотрю историю развития протокола HTTP. Полное обсуждение семантики выходит за рамки, но понимание ключевых изменений в устройстве HTTP и мотивов принимаемых решений даст необходимую основу для обсуждения вопросов производительности и ограничений протокола, особенно в контексте предстоящих улучшений HTTP/2 и его преемника HTTP/3. Про HTTP-NG сейчас написано только на английском и буквально в нескольких редких книгах, так что я поизучал домашние страницы членов комитета и их презентации 1996-1998 гг., чтобы понять основные мотивы. Хочу поделиться находками с аудиторией Хабра.

Протокол HTTP, пожалуй, самый распространённый протокол передачи данных в Интернете. Сегодня это основной язык между клиентами и серверами, обеспечивающий работу современного Интернета. Начав путь от простого однострочного протокола, HTTP стал выбором не только для браузеров, но и практически для всех приложений.

HTTP/0.9

В 1991-м году Тим Бернерс-Ли с группой исследователей уже несколько лет разрабатывал концепцию Всемирной паутины и решал задачу связности гипертекстовых документов, для передачи которых существующих протоколов файлового обмена было мало.

Не хватало функций передачи файлов, индексного поиска документов, определения формата и возможности перенаправления клиента на другой сервер. К тому времени существовали основные сетевые протоколы модели OSI (включая систему доменных имён DNS), язык разметки SGML (предшественник HTML) и наработки по именованию сетевых ресурсов. С появлением идеи URI оставалось дело только за протоколом прикладного уровня.

HTTP/0.9 был текстовым протоколом, использующим TCP в качестве транспортного слоя. HTTP/0.9 был ориентирован только на передачу гипертекста, как основной идеи представления данных в сети на тот момент.

В результате получился простой, однострочный протокол для загрузки документов клиентом. Основным преимуществом HTTP/0.9 в 1991 году была простота реализации и возможность работы из командной строки (хоть первый браузер WorldWideWeb для NEXTstep и был графическим, то второй универсальный браузер Line Mode для работы на терминалах был текстовым).

Веб-страница WorldWideWeb в браузере Line Mode
Веб-страница WorldWideWeb в браузере Line Mode

Взаимодействие клиента и сервера выглядит следующим образом:

  • Запрос клиента представляет собой одну строку ASCII-символов вида GET и путь до документа.

  • Запрос клиента завершается возвратом каретки (CR+LF).

  • Ответ сервера представляет собой поток ASCII-символов в виде языка гипертекстовой разметки.

  • Клиент разрывает соединение после завершения передачи документа.

$ telnet example.tld 80
Connected to example.tld

GET /index.html

(hypertext response)
(connection closed)

Эти простые правила и стали основой HTTP, который мы знаем сегодня. Несмотря на окончание поддержки HTTP/0.9, современные веб-серверы до сих пор с ним работают.

Однако HTTP/0.9 не удовлетворял потребностям пользователей. Помимо гипертекстовых документов, не хватало показа изображений, воспроизведения аудио, что, например, уже позволяла делать электронная почта.

Ещё протокол не подразумевал использование его за пределами одного сегмента сети. Компьютеры в корпоративных сетях редко имели непосредственный выход в Интернет из-за соображений безопасности. Поэтому для доступа к Интернету из закрытых сетей используются прокси-серверы, выполняющие роль посредника между клиентом и сервером. 

В HTTP/0.9 не было технической возможности проксировать трафик через посредников. Кроме того, протокол не позволял получать доступ к популярным электронным каталогам по протоколам Gopher и Z39.50, доминировавших в сети.

HTTP/1.0

Следующим этапом развития протокола стало появление HTTP/1.0 в 1995-м году. К тому времени уже организована первая конференция World Wide Web, создан консорциум W3C, и в рамках IETF создана рабочая группа HTTP-WG по вопросам развития протокола HTTP. К слову, обе эти группы продолжают играть важную роль в развитии Интернета.

К моменту появления коммутируемого доступа в Интернет, интерес к WWW становился выше, а количество веб-сайтов — больше. На конце 1995-го года их насчитывалось более 23000. Например, тогда уже запустился веб-сайт MTV, Amazon.com, BBC Online, IBM, Microsoft и Yahoo.

Тогда же компания Netscape Communications анонсирует SSL 2.0 для поддержки в своём браузере Netscape Navigator — наследника NCSA Mosaic. Протокол SSL позднее станет родоначальником TLS — де-факто стандарта среди end-to-end протоколов безопасной передачи данных в Интернете.

HTTP/1.0 тоже был текстовым и работал поверх TCP, как и его предшественник, но теперь включал в себя заголовки клиента при запросе и сервера при ответе.

Преимуществом такого протокола является ориентированность на графические браузеры. Выполнять запросы через telnet стало сложнее, но совместимость по-прежнему есть. 

Для передачи метаданных клиентом и сервером используются заголовки. К примеру, в HTTP/1.0 появился заголовок Content-Type, задающий MIME-тип передаваемого документа. Таким образом возможно передавать не только гипертекст, но и изображения, аудио и файлы других типов. Появилась поддержка кэширования, аутентификации и работы через прокси-серверы и шлюзы. Для доступа к ресурсам по протоколу Gopher и Z39.50 появились реализации прокси-серверов (SQUID). Тот же SQUID используется до сих пор и много где. 

Также появились статус-коды результата выполнения запроса и методы HEAD и POST в дополнение к GET.  Появление метода POST в корне меняет суть работы с документами. Ранее веб был идемпотентным, а после — стало возможным менять состояние (неидемпотентные запросы).

Не все оценили это изменение. Henrik Frystyk Nielsen (один из авторов HTTP) считал туннелирование через POST-запрос ошибкой, что было одной из причин появления HTTP-NG.

Рассмотрим пример запроса и ответа HTTP/1.0. 

$ telnet example.tld 80
Connected to example.tld

GET /index.html HTTP/1.0
User-Agent: Netscape
Accept: */*

HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: (length)
Server: Apache/0.2

(plain-text response)
(connection closed)

Клиент устанавливает TCP-соединение, отправляет строку запроса в формате «метод», путь до документа и версию протокола.

Следом клиент сообщает заголовки в формате ключ-значение, разделённые двоеточием. В конце запроса клиент указывает символ возврата каретки (CR+LF).

В ответ сервер сообщает версию протокола, статус-код. Статус-код тоже ввели с HTTP/1.0. В новых строках заголовки ответа, разделенные одной пустой строкой от тела ответа. Тело ответа теперь могло быть не только гипертекстовым. Тип документа определяет сервер по расширению файла и соответствию его MIME-типу — уже существовавшему в то время механизму передачи информации внутри текстовых данных.

В конце каждого запроса клиент закрывает TCP-соединение.

Основные проблемы HTTP/1.0 исходили из его дизайна. В HTTP/1.0 одному запросу соответствует ровно один ответ, а каждая пара выполняется синхронно и последовательно, что менее эффективно, чем конвеерная передача нескольких запросов (pipelining).

На важность конвееризации запросов указывали исследователи W3C: согласно исследованию, поддержка pipelining в HTTP позволила бы улучшить производительность и сократить объём передаваемого трафика.

Учитывая, что сокращение времени загрузки страницы было важным для пользователей и бизнеса, то для сокращения издержек разработчики вместо одного TCP-соединения устанавливали несколько. С одной стороны, это упрощает задачу программиста и действительно ускоряет загрузку, с другой — негативно влияет на сеть, используя много ресурсов на поддержание TCP-подключений.

Также скорость загрузки страницы зависит от задержек в сети. В случае, если один HTTP-запрос выполняется медленно, остальные ресурсы не будут загружены до конца передачи. Вы также не можете остановить эту передачу, не разорвав TCP-соединение.

Представьте себе, вы пришли в супермаркет за йогуртом, идёте на кассу и видите очередь. Перед вами в ней стоит покупатель с полной корзиной товаров и долго ищет кошелёк в сумке, а вы всего-то пришли за йогуртом. 

Эта инженерная проблема имеет название Head Of Line блокировка (HoL) — ограничение производительности, возникающее при задержке очереди первым пакетом.

Немаловажно и то, что для работы веб-сайта по HTTP/1.0 требовался новый IP-адрес, что стремительно истощало пул IPv4-адресов.

Примечательно и то, что HTTP/1.0 был стандартизован по реализациям веб-серверов и клиентов, нежели изначально разработан как стандарт. IETF занялись стандартизацией уже после того, как протокол стал стремительно изменяться. В результате HTTP-WG больше ничего не оставалось, как составить RFC 1945 в 1996 году в роли информационного документа, обобщающем детали реализаций уже нескольких готовых продуктов (CERN HTTPD, Apache HTTPD, Jetty и пр.).

Эти проблемы не были сюрпризом и стали очевидны ещё в конце 20 века, а потому исследователи приступили к разработке альтернатив.

HTTP-NG

Так в 1997-м году консорциум W3C занялся разработкой экспериментального протокола HTTP-NG (Next Generation). К старту проекта HTTP-NG, консорциум уже работал над HTTP/1.1 (RFC 2068) и устранением основных проблем HTTP/1.0, однако авторы считали это только полумерой и ставили задачи:

  • Сокращение большого числа TCP-соединений.

  • Увеличение эффективность передачи и кэширование данных.

  • Сокращение использование IP-адресов.

  • Стандартизация способы доставки веб-приложений.

  • Установка контроль над появлением расширений к HTTP.

  • Избавление от тенденции использования HTTP в роли универсального протокола и стремление пользователей к универсальному применению (см. проблема «Золотого молотка»).

  • Обеспечение совместимость веб-приложений друг с другом.

В основе архитектуры HTTP-NG лежит распределённая сеть с объектно-ориентированным обменом между узлами. Реализация предполагала выделение трёх протоколов:

  • Транспортный протокол WebMUX.

  • Протокол передачи сообщений Binary wire.

  • Прикладной протокол (eCommerce, WebDAV, IPP, …).

Задачей WebMUX была обеспечение кадрирования данных и их эффективная (бинарная), безопасная, двунаправленная и параллельная передача между узлами. Предполагали, что WebMUX может работать поверх любых существующих транспортных протоколов (TCP, SSL over TCP, UDP, …).

Задачей Binary wire являлось предоставление легковесной системы типов, обеспечение эффективного (де-)кодирования сообщений на базе XDR, кеширование ресурсов, конвеерная (pipelining) и групповая (batching) пересылка запросов, а также поддержка опциональных и обязательных расширений. Важной частью была совместимость с современными коммерческими платформами для распределённых вычислений.

В конце 20 века Интернет был распространён не так широко, как крупные локальные сети в компаниях. В таких компаниях стандартом распределённых вычислений являлись проприетарные платформы CORBA, DCOM и Java RMI, а потому важно было поддержать работу с ними для лёгкого внедрения в протоколе Binary wire.

На прикладном уровне же планировалось создавать протоколы самих приложений на базе обмена сообщений в формате XML. К примеру, протокол WebDAV для файлового обмена, IPP для печати, протокол статичных приложений, протокол интернет-магазина и пр. Таким образом унификация протоколов позволила бы разным приложениям одинаковым образом интегрироваться между собой.

К примеру, вымышленный протокол интернет-магазина мог бы предполагать взимание платы со счёта клиента за выполнение запроса, а протокол приложения «фотогалереи» — (диз-)лайк от пользователя.

Инженеры также предусмотрели план перехода. Он подразумевал сохранение протокольной схемы (http) и использование прокси-серверов для преобразования HTTP-NG в HTTP и обратно или передачу HTTP-NG в теле POST-запроса HTTP. Для неанонсированного HTTP/1.1 использовать заголовок Upgrade для обновления версии протокола.

Однако прогрессивность идей HTTP-NG компенсировалась сложностью реализации и миграции. В 1998 году число веб-сайтов уже насчитывало 2 500 000, а потому несовместимость с HTTP/1.0 и отсутствие плана бесшовного перехода на HTTP-NG свело всю идею на нет. В 1999-м году разработка стандарта была полностью прекращена в пользу HTTP/1.1.

HTTP/1.1

В 1999-м году IETF выпускает стандарт HTTP/1.1 (RFC 2616) — наиболее более известный сейчас всем современным разработчикам.

HTTP/1.1 — это текстовый протокол, расширивший семантику своего предшественника, но сохранивший основные принципы работы. Протокол работает поверх TCP с опциональным слоем шифрования SSL/TLS. В отличие от HTTP/1.0, IETF сначала опубликовала стандарт, а позднее появились реализации.

В HTTP/1.1 клиент устанавливает TCP-соединение и опционально согласовывает SSL/TLS-соединение. Далее клиент отправляет серверу строку с методом, путём до документа и версией протокола.

Затем клиент передаёт заголовки в формате ключ-значение с символом двоеточия (colon) в качестве разделителя. Теперь клиент с помощью заголовка Connection (keep-alive) может уведомить о поддержке постоянных подключений и попросить сервер не завершать соединение после окончания выполнения запроса.

В конце запроса клиент указывает символ возврата каретки (CL+RF). В ответ сервер сообщает версию протокола, статус-код и, если возможно, тело ответа. Если клиент и сервер поддерживают постоянные подключения, то сервер может не закрывать TCP-соединение. Это изменение решает проблему высокой утилизации TCP-подключений.

$ telnet example.tld 80
Connected to example.tld

GET /index.html HTTP/1.1
Host: example.tld
User-Agent: Internet Explorer
Connection: Keep-Alive

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: (length)
Connection: Keep-Alive
Server: Apache

(plain-text response)
[...]
(connection closed)

Однако HTTP/1.1 не решало полностью проблемы своего предшественника. HTTP/1.1 по-прежнему чувствителен к задержкам в сети и Head of Line блокировкам. С развитием беспроводных сетей эта проблема стала ощущаться острее. 

Использование TCP предполагает сохранение порядка передаваемых пакетов, отчего задержки при передаче будут неизбежно тормозить загрузку документов. То же касается и HTTP: документы передаются по одному и последовательно без возможности отменить передачу посередине.

Появление нового стандарта вынуждает вендоров и разработчиков адаптировать под них свои клиентские и серверные приложения. Рост RFC и вокруг HTTP, неоднозначность пользы некоторых возможностей и сложности в реализации, привели к избирательности и отличиям в ПО.

Ярким примером является поддержка конвееризации. В RFC 7230 описана поддержка конвеерной передачи запросов (pipelining), но не является обязательной для клиента. На практике HTTP/1.1 pipelining была только в Firefox до 2017, но отключена по умолчанию. В 2019 году разработчики cURL также прекратили поддержку этой возможности.

С увеличением числа расширений, росло и число передаваемых HTTP-заголовков, что создавало избыточность в трафике. Например, если запросить с сервера 100 изображений, то клиент 100 раз отправит HTTP-заголовок User-Agent. В ряде случаев размер заголовков и вовсе в несколько раз больше самого тела запроса.

Из-за несовершенств HTTP, в разработке веб-приложений стали популярны сразу несколько хаков оптимизации фронтенда. 

Так, например, в HTTP/1.1 эффективнее склеивать несколько изображений (спрайтинг), стилей и скриптов в один файл (конкатенация), чтобы экономить на числе запросов к статическим ресурсам. В случае, когда размер документа меньше передаваемых заголовков, то вставлять код в тело страницы (инлайнинг) ценой отсутствия их кеширования.

Параллелизм загрузки документов по-прежнему достигался за счёт открытия TCP-соединений, потому в разработческой среде было популярно шардировать статические файлы по доменам. Степень параллелизма определялась вендором: значения варьировались от браузера к браузеру.

После нескольких лет активного пользования протокола и проблем HTTP/1.1, в 2007 году было принято решение создать группу по улучшению спецификации под названием HTTPbis (от лат. “bis” — «дважды» или «повтор»)

В рамках группы HTTPbis начался рефакторинг RFC 2616, результатом которого стало как исправление ошибок и внедрение некоторых аспектов из других спецификаций, так и разделение документа на части.

В декабре 2007 года было HTTPbis опубликовала шесть черновиков, в том числе и стандарты, отделяющие сообщения от семантики протокола. Ещё спустя 7 лет — в 2014 году вышла серия RFC 723x, заменившие стандарт RFC 2616. В то время как IETF занимался доработкой RFC 723x, мир не стоял на месте. 

За всё время протокол обрастал расширениями, коих накопилось более 30 стандартизирующих документов и черновиков. Во многом это стало возможным благодаря дизайну HTTP/1.0, расширяющем возможности клиента и сервера через HTTP-заголовки.

Даже сейчас в IETF обсуждается несколько десятков изменений, связанных с изменением и расширением семантики протокола, начиная с подписи HTTP-сообщений, заканчивая заменой User-Agent на Client Hints.

Параллельно с развитием HTTP изменялся и SSL. Рост популярности HTTP, спрос на безопасную коммуникацию в электронной коммерции и банкинге стал триггером к переменам в криптографии. Так, например, появился стандарт TLS Extentions с расширением Server Name Indication для передачи виртуального хоста веб-серверу, чтобы обслуживать несколько веб-сайтов на одном IP-адресе.

Ключевое значение в развитии HTTP играли инженеры крупных ИТ-компаний. Среди них и инженеры Google, которые стали экспериментировать с собственным протоколом под названием SPDY.

Именно с SPDY начинается новый этап развития сетей. О новых (хорошо забытых старых из HTTP-NG) идеях и новых реализациях я расскажу в следующей части. 

Tech-команда СберМаркета завела соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и YouTube.

Комментарии (1)


  1. Landgraph
    04.12.2022 07:48
    +1

    Приветствую!

    Поправьте пожалуйста пример с http/1.1. Там вы говорите о киллер-фиче keep-alive, а пример у вас по сути обычный http/1.0. Лучше user-agent выкиньте. А ещё вы употребляете убер-мега-киллер-фичу Host, которая распахнула дверь в виртуальные хосты, но просто проходите мимо. Ждать в продолжении?

    И будет ли в продолжении про такие киллер-фичи как Content-Range-спаситель диалапа и родоначальник тысяч download-manager’ов? Accept/Content-Encoding, которую до сих пор не научились нормально использовать?