HTTP и архитектура веба
Представь обычное действие: ты открыл сайт, нажал кнопку, через секунду увидел список статей. За этот миг браузер отправил серверу сообщение (“дай мне статьи”), а сервер ответил (“вот они”). Язык, на котором они договорились, называется HTTP. И почти весь веб, который ты видишь, держится на этом простом обмене: запрос, ответ, запрос, ответ.
Дальше в курсе мы будем писать серверы на фреймворках: Fastify, Nest,
Express. Фреймворк, это готовый помощник, который берёт на себя всю рутину
вокруг этого обмена. Но если начать сразу с него, легко получить ложное
ощущение: команды вроде app.get(...) работают, а что происходит внутри,
непонятно. Тогда плагины, хуки и схемы кажутся магией: вроде надо, но
зачем именно и где они вклиниваются, неясно.
Поэтому в этом уроке мы фреймворк пока отложим и посмотрим, что лежит под ним. План такой:
- Разберём, из каких частей вообще состоит веб и где в этой картине стоит твой сервер.
- Посмотрим на HTTP вблизи: как выглядит запрос и ответ, что такое методы, статусы, заголовки, тело.
- Своими руками напишем маленький сервер на голом Node, без единого фреймворка, и почувствуем, сколько рутины приходится делать самому.
- И тогда станет видно, что фреймворк, это не магия, а та же самая работа, только написанная за тебя один раз и аккуратно.
Никакого предварительного знания фреймворков не нужно. Достаточно того, что ты уже умеешь запускать Node и читать JavaScript.
1. Из чего состоит веб
Когда браузер открывает страницу, запрос почти никогда не идёт напрямую в твой процесс Node. По дороге стоит несколько узлов, и у каждого своя роль.
| Узел | Что делает | Пример |
|---|---|---|
| Клиент | формирует запрос, ждёт ответ | браузер, мобильное приложение, другой сервис |
| Обратный прокси | принимает соединение, терминирует TLS, маршрутизирует | nginx, Caddy, Traefik |
| Балансировщик | раскидывает запросы по инстансам приложения | nginx, HAProxy, облачный LB |
| CDN | кеширует статику ближе к пользователю | Cloudflare, Fastly, CloudFront |
| Сервер приложения | твой код: бизнес-логика, БД | процесс Node с Fastify или Nest |
Запрос за статикой (картинка, JS-бандл) часто заканчивается на CDN и до
тебя не доходит вовсе. Запрос за данными (GET /api/articles) проходит
прокси и балансировщик и приходит в один из инстансов приложения. Для
твоего кода это важно по двум причинам.
Первая: между клиентом и тобой стоят узлы, которые меняют запрос. Прокси
добавляет заголовки (X-Forwarded-For с реальным IP клиента,
X-Forwarded-Proto со схемой), терминирует TLS (твой процесс видит обычный
HTTP, шифрование сняли раньше), может сжать ответ. Если читать IP напрямую
из сокета, увидишь адрес прокси, а не клиента.
Вторая: инстансов приложения обычно несколько. Значит, нельзя хранить состояние в памяти процесса и рассчитывать, что следующий запрос того же пользователя попадёт туда же. Сессии, кеш, счётчики живут во внешнем хранилище (Redis, БД), а процесс остаётся stateless.
Глубже про прокси, балансировку и масштабирование, в разделах 09-backend и 06-node. Здесь нам хватит карты: твой сервер это последний узел в цепочке, и говорит он на HTTP.
2. HTTP как протокол
HTTP устроен предельно просто: клиент шлёт запрос, сервер отдаёт ответ. Один запрос, один ответ. Никакого состояния между ними протокол не хранит, это и называется stateless.
Сырой HTTP-запрос это текст. Вот как он выглядит на проводе:
POST /api/articles HTTP/1.1
Host: batschool.dev
Content-Type: application/json
Content-Length: 38
Accept: application/json
{"title":"HTTP","body":"первый абзац"}
Разбор по частям:
- Request line: метод (
POST), путь (/api/articles), версия протокола (HTTP/1.1). Одна строка, три слова через пробел. - Заголовки: пары
Имя: значение, по одной на строку. Метаданные запроса. - Пустая строка: разделяет заголовки и тело. Ровно один
\r\n. - Тело: полезная нагрузка. Для
POST/PUTэто данные, дляGETобычно пусто.
Ответ устроен зеркально: status line, заголовки, пустая строка, тело.
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/articles/42
{"id":42,"title":"HTTP"}
Покрути этот разбор руками на виджете ниже: меняй метод, заголовки и тело, смотри, как меняется сырой текст запроса.
2.1 Методы
Метод это глагол: что мы хотим сделать с ресурсом по адресу. У методов есть два свойства, которые определяют, как с ними безопасно обращаться.
Безопасность:
метод безопасен, если не меняет состояние сервера. GET только читает,
повторить его можно сколько угодно раз без вреда. POST меняет, повторять
вслепую нельзя.
Идемпотентность: метод идемпотентен, если N одинаковых запросов дают тот же эффект, что один. Это про надёжность: если ответ потерялся в сети, идемпотентный запрос можно повторить без страха задвоить эффект.
| Метод | Зачем | Безопасный | Идемпотентный |
|---|---|---|---|
GET | прочитать ресурс | да | да |
HEAD | как GET, но без тела | да | да |
POST | создать, запустить действие | нет | нет |
PUT | заменить ресурс целиком | нет | да |
PATCH | частично изменить | нет | нет |
DELETE | удалить | нет | да |
Ключевая пара для практики: POST против PUT. POST /articles каждый раз
создаёт новую статью, повтор задвоит. PUT /articles/42 кладёт статью по
известному адресу, повтор перезапишет тем же, состояние не изменится. Когда
клиент ретраит запрос после таймаута, разница между этими двумя становится
разницей между одной статьёй и десятью дублями.
2.2 Статусы
Статус-код это первое, что говорит ответ. Три цифры, и первая задаёт класс.
- 1xx информационные, редко нужны руками (
100 Continue,101 Switching Protocolsпри апгрейде на WebSocket). - 2xx успех.
200 OK,201 Created(создали ресурс, обычно с заголовкомLocation),204 No Content(успех без тела). - 3xx перенаправление.
301навсегда,302/307временно,304 Not Modified(кеш клиента актуален, тело не шлём). - 4xx ошибка клиента: он прислал что-то не то.
400 Bad Request,401 Unauthorized(не аутентифицирован),403 Forbidden(прав нет),404 Not Found,409 Conflict,422 Unprocessable Entity,429 Too Many Requests. - 5xx ошибка сервера: клиент ни при чём, упали мы.
500 Internal Server Error,502 Bad Gateway,503 Service Unavailable.
Граница 4xx против 5xx это не формальность, а распределение вины. 4xx
значит: не повторяй тот же запрос, он сам по себе неправильный. 5xx
значит: попробуй ещё раз, проблема на нашей стороне и может быть временной.
По этой границе настраивают ретраи, алерты и автоскейлинг.
2.3 Заголовки и согласование контента
Заголовки это метаданные запроса и ответа. Самые ходовые:
Content-Type: какой формат у тела (application/json,text/html,multipart/form-data).Content-Length: длина тела в байтах.Authorization: токен или учётка (Bearer <jwt>).Accept: какие форматы готов принять клиент в ответе.
Последний запускает
согласование контента:
клиент в Accept перечисляет, что готов принять (application/json,
text/html), а сервер выбирает формат ответа. Похоже работают
Accept-Language (язык) и Accept-Encoding (сжатие, gzip или brotli).
Отдельно стоит keep-alive: по умолчанию в HTTP/1.1 соединение TCP не закрывается после ответа, а переиспользуется под следующий запрос. Без этого каждый запрос заново платил бы за рукопожатие TCP и TLS, что дорого.
2.4 Версии: 1.1, 2, 3
Семантика (методы, статусы, заголовки) одна во всех версиях. Меняется транспорт, то, как байты летят по проводу.
- HTTP/1.1: текстовый, один запрос на соединение за раз. Параллелизм, через несколько TCP-соединений. Знакомая голова болит: head-of-line blocking.
- HTTP/2: бинарный, мультиплексирование (много запросов в одном соединении параллельно), сжатие заголовков (HPACK), server push.
- HTTP/3: поверх QUIC (UDP вместо TCP), убирает head-of-line blocking на транспортном уровне, быстрее восстанавливается при потере пакетов.
На практике версию обычно терминирует прокси или CDN: снаружи HTTP/2 или HTTP/3, а до твоего процесса Node долетает простой HTTP/1.1. Поэтому код приложения почти не зависит от версии. Глубина, в разделе 11-network.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.