Как работает веб: DNS, HTTP, браузер, клиент и сервер
В предыдущей статье трека мы разобрали, как компьютеры вообще обмениваются данными: как
байты нарезаются на пакеты, как IP доставляет их между машинами, а TCP собирает обратно
в надёжный поток (Как компьютеры общаются). Но
«надёжный поток байтов между двумя IP-адресами» — это ещё не веб. Между этим потоком и той
страницей, что вы читаете сейчас, лежит целый этаж абстракций: система имён, которая
превращает example.com в число; протокол HTTP, который договаривается о том, что именно
байты значат; и браузер, который из полученного текста строит живой интерактивный документ.
Веб — это не «интернет». Интернет — это трубы (статья 11). Веб — это один из сервисов, работающих поверх этих труб: договорённость о том, как клиенты просят у серверов документы и как серверы их отдают. Придумал её Тим Бернерс-Ли в ЦЕРН в 1989–1991 годах, собрав три кирпича, которые живы до сих пор: URL (как адресовать ресурс), HTTP (как его запросить) и HTML (как его описать). Разберём весь путь по порядку — так, как он разворачивается, когда вы вводите адрес и нажимаете Enter. Это один из лучших сквозных примеров того, как слои абстракции складываются в работающую систему, поэтому пройдём его медленно и до дна.
Клиент и сервер: базовая модель
Веб построен на модели клиент — сервер. Сервер — это программа (не «железный
ящик»), которая постоянно слушает сеть и ждёт запросов; получив запрос, отвечает и снова
ждёт. Клиент — программа, которая инициирует общение: браузер, мобильное приложение,
curl, поисковый робот. Клиент всегда говорит первым, сервер отвечает. Один сервер
обслуживает миллионы клиентов; один клиент за одну страницу обращается к десяткам серверов.
Ключевое свойство этой модели в вебе — HTTP не имеет состояния (stateless). Каждый запрос самодостаточен: сервер по умолчанию не помнит, что вы к нему уже обращались минуту назад. Это кажется недостатком (как тогда работает «вход в аккаунт»?), но именно оно позволило вебу масштабироваться: раз сервер ничего не помнит между запросами, любой из тысячи серверов за балансировщиком может обслужить любой ваш запрос. Как поверх этой беспамятности всё-таки строят сессии — разберём ниже.
рисует результат"] end subgraph net["Интернет — трубы (статья 11)"] direction TB N["DNS · TCP · TLS · IP
доставка байтов"] end subgraph server["Сервер"] direction TB S["слушает, отвечает
логика + данные"] end B -- "HTTP-запрос →" --> N --> S S -- "← HTTP-ответ" --> N --> B style client fill:#4f8ac9,fill-opacity:0.1 style server fill:#6aa86a,fill-opacity:0.1 style net fill:#8a8f98,fill-opacity:0.08
URL: как адресуется ресурс
Прежде чем что-то запросить, надо это что-то назвать. В вебе имя ресурса — URL (Uniform Resource Locator). Он выглядит как одна строка, но на деле это структурированный адрес, где каждая часть отвечает на свой вопрос: по какому протоколу, к какому серверу, к какому его ресурсу и с какими параметрами обращаться.
- Схема (
https) — по какому протоколу говорить. Для веба этоhttpилиhttps(HTTP поверх шифрования), но бываютftp,mailto,ws(веб-сокеты). - Хост (
api.example.com) — доменное имя сервера. Именно его предстоит превратить в IP-адрес через DNS. Слева — поддомены (api), справа — регистрируемое имя (example.com). - Порт (
:443) — номер «двери» на сервере (см. про порты в статье 11). Обычно опускают: дляhttpподразумевается80, дляhttps—443. - Путь (
/v2/users) — какой именно ресурс на сервере нужен. Раньше это был путь к файлу на диске; сегодня чаще — маршрут, который сервер разбирает программно. - Строка запроса (
?id=42&sort=name) — набор параметровключ=значениечерез&. - Фрагмент (
#profile) — якорь внутри документа. Важнейшая деталь: фрагмент на сервер не отправляется вообще, его обрабатывает только браузер. Это регулярно удивляет новичков.
Стоит сразу отметить протекающую абстракцию: в URL можно писать не-ASCII символы и пробелы,
но по проводам они уходят в процентном кодировании (%20 вместо пробела, %D0%BF для
кириллической буквы). Отсюда классические баги, когда + в строке запроса означает пробел, а
в пути — уже нет.
Шаг 1. DNS: превращаем имя в адрес
Компьютеры маршрутизируют пакеты по IP-адресам (93.184.216.34), а люди помнят имена
(example.com). Мост между ними — DNS (Domain Name System), гигантская распределённая
телефонная книга интернета. Прежде чем послать хоть один байт запроса, браузер должен
узнать IP хоста из URL.
DNS устроен иерархически, и это ключ к тому, почему он вообще работает в масштабе всей
планеты: ни одна машина не хранит всю книгу. Имя читается справа налево как дерево:
. (корень) → .com (домен верхнего уровня, TLD) → example.com → api.example.com. За
каждый уровень отвечают свои серверы, и поиск спускается по дереву.
(провайдер / 8.8.8.8) participant Root as Корневой сервер participant TLD as Сервер .com participant Auth as Авторитетный сервер
example.com B->>R: где api.example.com? Note over R: сначала смотрит в свой кэш R->>Root: где .com? Root-->>R: спроси серверы .com (вот они) R->>TLD: где example.com? TLD-->>R: спроси авторитетный сервер (вот он) R->>Auth: где api.example.com? Auth-->>R: A-запись = 93.184.216.34 (TTL 300с) R-->>B: 93.184.216.34 Note over R,B: резолвер кэширует ответ на TTL секунд
Разберём участников:
- Резолвер (recursive resolver) — сервер вашего провайдера или публичный вроде
8.8.8.8(Google) /1.1.1.1(Cloudflare). Именно он делает всю грязную работу обхода дерева, а браузеру возвращает готовый ответ. - Корневые серверы знают, где искать серверы каждого TLD (
.com,.org,.ru). - Серверы TLD знают, где искать авторитетный сервер конкретного домена.
- Авторитетный сервер хранит настоящие записи домена:
A(IPv4-адрес),AAAA(IPv6),CNAME(псевдоним),MX(почта),TXT(произвольные данные, часто для верификации).
Без кэширования DNS немедленно рухнул бы: каждый клик слал бы запросы к корневым серверам. Спасает кэш и TTL (time-to-live): каждая запись живёт заданное число секунд, и резолвер, браузер и ОС держат её у себя. Отсюда две практические истины. Первая: обычно DNS-поиск почти мгновенен, потому что ответ уже в кэше. Вторая — знаменитая головная боль: сменив IP-адрес сайта, вы ждёте «распространения DNS» — на самом деле ждёте, пока по всему миру истекут закэшированные старые записи. Поэтому перед переездом TTL заранее уменьшают.
Тонкость безопасности: обычный DNS ходит открытым текстом по UDP, и провайдер (или атакующий в той же сети) видит, какие имена вы запрашиваете, и может их подменить. Отсюда DNS over HTTPS/TLS (DoH/DoT), шифрующий сами запросы к резолверу.
Шаг 2. Соединение: TCP и TLS
IP у нас есть — теперь нужно надёжное соединение до порта 443. Браузер открывает
TCP-соединение через трёхстороннее рукопожатие (SYN → SYN-ACK → ACK), разобранное в
статье 11. Это один круг туда-обратно (1 RTT) ещё до того, как отправлен хоть байт HTTP.
Для https сверху добавляется TLS-рукопожатие — установка шифрования. Клиент и сервер
договариваются о версии протокола и наборе шифров, сервер предъявляет сертификат
(подписанный доверенным центром сертификации документ, доказывающий «я действительно
example.com»), и стороны вырабатывают общий сеансовый ключ. С этого момента весь HTTP-трафик
зашифрован — провайдер видит только с каким сервером вы говорите, но не что передаёте.
TLS 1.3 ужал рукопожатие до 1 RTT (а при повторном подключении — до нуля).
Обратите внимание, сколько кругов туда-обратно набегает до первого байта данных: DNS + TCP + TLS. На межконтинентальном канале с задержкой 150 мс это уже полсекунды на пустом месте. Поэтому веб так одержим сокращением RTT: постоянные соединения, TLS 1.3, размещение серверов ближе к пользователю (CDN, о них ниже).
Шаг 3. HTTP: язык запросов и ответов
Канал установлен — пора говорить по делу. HTTP (HyperText Transfer Protocol) — это протокол уровня приложения, простой до элегантности: клиент шлёт запрос, сервер шлёт ответ, и оба — обычный текст с жёсткой структурой из четырёх частей: стартовая строка, заголовки, пустая строка, тело.
Запрос начинается со стартовой строки: метод, путь и версия. Метод объявляет намерение:
| Метод | Смысл | Меняет данные? | Идемпотентен? |
|---|---|---|---|
GET |
получить ресурс | нет | да |
POST |
создать / отправить данные | да | нет |
PUT |
заменить ресурс целиком | да | да |
PATCH |
частично изменить | да | нет |
DELETE |
удалить | да | да |
HEAD |
как GET, но только заголовки | нет | да |
Различие безопасных (не меняющих состояние: GET, HEAD) и идемпотентных (повтор
даёт тот же результат) методов — не педантизм, а контракт, на который опираются кэши, прокси
и повторные попытки при обрыве. Браузер спокойно повторяет GET при сбое, но переспросит
вас перед повтором POST — потому что второй POST может создать второй заказ.
Ответ начинается со статусной строки: версия, числовой код состояния и текстовая причина. Коды сгруппированы по первой цифре, и эту таксономию стоит держать в голове:
Практическое правило: 4xx — виноват клиент (не туда постучался, нет прав, кривые данные), 5xx — виноват сервер (упал, перегружен, за ним лежит зависимость). Спутать их — значит чинить не ту сторону.
Заголовки — метаданные пары «ключ: значение», и именно в них живёт вся тонкая механика
веба. Host (какой из виртуальных сайтов на этом IP нужен — без него один сервер не мог бы
держать тысячи доменов), Content-Type (что в теле: text/html, application/json,
image/png), Content-Length, Accept-* (что клиент готов принять), Authorization,
Cache-Control, Set-Cookie. Тело несёт полезную нагрузку: у ответа это сам документ,
у POST — отправляемые данные; у GET тела обычно нет.
Вот тот же запрос-ответ вживую, через curl:
$ curl -v https://api.example.com/v2/users?id=42
> GET /v2/users?id=42 HTTP/1.1 # стартовая строка запроса
> Host: api.example.com # обязательный заголовок
> Accept: application/json
> # пустая строка — конец заголовков
< HTTP/1.1 200 OK # статусная строка ответа
< Content-Type: application/json
< Cache-Control: max-age=60
< # пустая строка
< {"users":[{"id":42,"name":"Ann"}]} # тело ответа
Эволюция HTTP: та же семантика, всё быстрее транспорт
Семантика (методы, коды, заголовки) от версии к версии почти не менялась — менялось то, как эти сообщения едут по проводу, ради скорости.
Ключевая боль, которую лечили: в HTTP/1.1 по одному соединению запросы шли строго по очереди
(head-of-line blocking — один медленный ответ держит остальные), поэтому браузеры
открывали по 6 соединений на домен. HTTP/2 ввёл мультиплексирование: много параллельных
«потоков» в одном соединении. Но так как всё это лежало на одном TCP, потеря одного пакета
тормозила все потоки. HTTP/3 перенёс транспорт на QUIC поверх UDP, где потоки независимы.
Снаружи для вашего кода это невидимо — тот же GET, тот же 200 OK; протекает лишь тогда,
когда вы отлаживаете производительность.
Шаг 4. Браузер: из текста в живую страницу
Сервер вернул тело — обычно HTML. Дальше начинается вторая половина истории, целиком на стороне клиента. Браузер — это не «просмотрщик текста», а сложный runtime, который должен разобрать документ, догрузить десятки связанных ресурсов и превратить всё это в пиксели и интерактивность.
(структура документа)"] CSS["CSS"] -->|парсинг| CSSOM["CSSOM
(дерево стилей)"] DOM --> RT["Render tree
(что видимо + как выглядит)"] CSSOM --> RT RT --> LAYOUT["Layout / reflow
(геометрия: где и какого размера)"] LAYOUT --> PAINT["Paint
(растеризация в слои)"] PAINT --> COMPOSITE["Composite
(сборка слоёв на экране)"] JS["JavaScript"] -.->|"меняет DOM/CSSOM →
повторный layout/paint"| DOM HTML -.->|"встретил <img>, <script>,
<link> → новые запросы"| SUB["Подзапросы
(снова DNS→TCP→HTTP)"] style DOM fill:#4f8ac9,fill-opacity:0.12 style RT fill:#6aa86a,fill-opacity:0.14 style JS fill:#c98a4f,fill-opacity:0.16
Что здесь важно понять:
- Парсинг HTML в DOM. Браузер читает HTML и строит DOM (Document Object Model) — дерево объектов, по одному узлу на тег. DOM — это уже не текст, а структура в памяти, с которой работает JavaScript.
- Подзапросы каскадом. Встретив
<img>,<link rel="stylesheet">,<script>, браузер запускает новые HTTP-запросы — и каждый может тянуть свой DNS-резолв, соединение и ответ. Одна «страница» — это часто 50–100 запросов к разным серверам. Вот почему в первом SVG путь «DNS → соединение → HTTP» проходится не раз, а десятки раз. - CSSOM и render tree. Параллельно из CSS строится CSSOM; вместе с DOM он даёт render tree — только видимые элементы с вычисленными стилями.
- Layout → paint → composite. Браузер считает геометрию каждого элемента (layout), растеризует их в слои (paint) и собирает слои воедино (composite). Любое изменение размеров запускает дорогой повторный reflow — отсюда советы по производительности фронтенда «не трогай layout в цикле».
- JavaScript оживляет страницу. JS-движок (V8 в Chrome, разобранный как пример JIT в
статье От кода к исполнению) исполняет
скрипты, которые меняют DOM, шлют фоновые запросы (
fetch/AJAX) и перерисовывают части страницы без перезагрузки. Так статический документ превратился в приложение.
Исторически важен именно пункт 5: сначала веб отдавал готовые HTML-страницы целиком, каждый клик — новая загрузка. Сегодня распространён обратный подход — сервер отдаёт почти пустой HTML и пачку JavaScript, который уже в браузере рисует интерфейс и общается с сервером через JSON-API. Обе модели живут бок о бок, и выбор между ними — большая тема прикладной разработки.
Собираем всё вместе: полный путь запроса
Теперь склеим все шаги в одну картину — что происходит от Enter до первого пикселя.
Заметьте: сервер на шаге «запросить данные» сам стал клиентом — для базы данных. Как эти данные хранятся и почему обращение к ним переживает выключение сервера — тема следующей статьи. А само «строки результата» скрывает ещё один пласт: SQL, индексы, транзакции.
Как веб помнит вас, будучи беспамятным
Мы говорили: HTTP не имеет состояния. Как же тогда сайт узнаёт вас между запросами? Ответ —
клиент носит удостоверение с собой в каждом запросе. Классический механизм — cookie.
Сервер в ответе шлёт заголовок Set-Cookie: session=abc123, браузер сохраняет его и
прикладывает Cookie: session=abc123 к каждому последующему запросу к этому домену.
Сервер по этому идентификатору находит вашу сессию в своём хранилище.
Вариаций много: серверные сессии (в cookie лишь ссылка, данные на сервере) против
токенов (JWT — вся информация в самом токене, сервер ничего не хранит); заголовок
Authorization: Bearer ... для API. Но принцип один: раз сервер не помнит — клиент
доказывает, кто он, в каждом запросе. Отсюда же и класс уязвимостей (кража cookie, CSRF,
XSS), к которым мы вернёмся в статье
Основы безопасности; флаги HttpOnly, Secure,
SameSite на cookie — как раз оборона этого рубежа.
Кэширование и CDN: не делать работу дважды
Самый быстрый запрос — тот, которого не было. Веб пронизан кэшами на каждом уровне.
Браузерный кэш хранит скачанные ресурсы согласно заголовку Cache-Control: max-age=....
Если ресурс мог измениться, работает условный запрос: браузер шлёт If-None-Match с
сохранённым тегом ETag, и сервер, если ничего не поменялось, отвечает 304 Not Modified
с пустым телом — экономя трафик. CDN (Content Delivery Network) — сеть серверов-зеркал по
всему миру: статику (картинки, CSS, видео) отдаёт ближайший к вам узел, сокращая тот самый
RTT. Именно поэтому крупный сайт грузится одинаково быстро из Токио и из Берлина.
Где эти абстракции протекают
Веб — башня протекающих абстракций, и знание их стыков отличает того, кто «чинит по наитию», от того, кто понимает систему.
- «Загрузилось у меня — загрузится у всех». DNS-кэш, старые записи, региональные CDN и разный RTT означают, что у другого пользователя всё иначе. «У меня работает» — не диагноз.
- HTTPS шифрует не всё. Содержимое — да, но имя хоста (через SNI и DNS) обычно видно. И замок в адресной строке говорит лишь «канал шифрован и это правда тот домен» — не «сайт честный». Фишинговый сайт тоже бывает с валидным сертификатом.
localhostи127.0.0.1— не всегда одно. Первое идёт через резолвинг имени (/etc/hosts, IPv6::1), второе — прямой IPv4. На этом ломаются локальные окружения.- CORS. Браузер из соображений безопасности запрещает JS со страницы
a.comсвободно читать ответы сb.com, покаb.comявно не разрешит это заголовками. Первое столкновение с CORS сбивает с толку всех — это не баг, а политика браузера, не сервера. - Кэш — источник половины «мистики». «Я же поправил, а браузер показывает старое» — почти
всегда закэшированный ресурс. Отсюда приём cache busting: добавлять хеш в имя файла
(
app.9f2a.js), чтобы новая версия имела новый URL. - Состояние соединения хрупко. За NAT, прокси и балансировщиками «одно TCP-соединение» может рваться и переустанавливаться; веб-сокеты и long-polling живут с этим постоянно.
Типичные заблуждения
- «Веб = интернет». Веб (HTTP/HTML/URL) — лишь один сервис поверх интернета; почта, видеозвонки, игры используют те же трубы, но не веб.
- «URL — это адрес файла на диске сервера». Раньше — да; сегодня путь почти всегда обрабатывается программно, за ним нет никакого файла.
- «DNS — это часть HTTP». Нет, это отдельная система (обычно поверх UDP), срабатывающая до всякого HTTP. Её сбой роняет доступ к сайту при полностью живом сервере.
- «Сервер помнит, что я залогинен». Сам HTTP — нет; помнить помогает cookie/токен, который клиент носит с собой в каждом запросе.
- «GET и POST — просто два способа послать форму». У них разный контракт:
GETбезопасен, идемпотентен и кэшируется,POST— нет. Отправлять деньги черезGET— беда.
Мини-итог
Путь от Enter до страницы — это лестница абстракций, каждая ступень которой стоит на предыдущей. URL структурированно называет ресурс. DNS иерархически и с кэшированием превращает имя в IP. Поверх IP поднимаются TCP (надёжность) и TLS (шифрование и подлинность), и лишь затем по этому каналу идёт HTTP — простой текстовый протокол запрос-ответ с методами, кодами состояния и заголовками, не имеющий собственной памяти о клиенте. Браузер превращает полученный HTML в DOM, догружает каскад ресурсов, строит render tree и оживляет страницу через JavaScript. Беспамятность HTTP компенсируют cookie и токены, а скорость — кэши и CDN. Понимая, где каждая из этих абстракций протекает — от DNS-кэша до CORS и HTTPS, — вы отлаживаете веб как систему, а не гадаете. Это и есть тот «верхний этаж» стека, ради которого строились все нижние слои трека.
Источники для углубления: спецификации HTTP — RFC 9110 (семантика) и RFC 9114 (HTTP/3); учебник и справочник MDN Web Docs: HTTP; DNS — RFC 1034/1035; книга Ilya Grigorik, «High Performance Browser Networking» (бесплатно на hpbn.co).
Что дальше
На пути запроса сервер сам обратился к базе данных — и вернул данные, которые пережили его перезапуск. Как именно данные хранятся так, чтобы не исчезнуть при выключении, чем файл отличается от базы данных и что гарантируют транзакции — в следующей статье трека.
Как данные переживают выключение: файлы, БД, транзакции — обзор