Раздел 8 · Frameworks и Libs

HTTP и архитектура веба

junior~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

HTTP и архитектура веба

Представь обычное действие: ты открыл сайт, нажал кнопку, через секунду увидел список статей. За этот миг браузер отправил серверу сообщение (“дай мне статьи”), а сервер ответил (“вот они”). Язык, на котором они договорились, называется HTTP. И почти весь веб, который ты видишь, держится на этом простом обмене: запрос, ответ, запрос, ответ.

Дальше в курсе мы будем писать серверы на фреймворках: Fastify, Nest, Express. Фреймворк, это готовый помощник, который берёт на себя всю рутину вокруг этого обмена. Но если начать сразу с него, легко получить ложное ощущение: команды вроде app.get(...) работают, а что происходит внутри, непонятно. Тогда плагины, хуки и схемы кажутся магией: вроде надо, но зачем именно и где они вклиниваются, неясно.

Поэтому в этом уроке мы фреймворк пока отложим и посмотрим, что лежит под ним. План такой:

  1. Разберём, из каких частей вообще состоит веб и где в этой картине стоит твой сервер.
  2. Посмотрим на HTTP вблизи: как выглядит запрос и ответ, что такое методы, статусы, заголовки, тело.
  3. Своими руками напишем маленький сервер на голом Node, без единого фреймворка, и почувствуем, сколько рутины приходится делать самому.
  4. И тогда станет видно, что фреймворк, это не магия, а та же самая работа, только написанная за тебя один раз и аккуратно.

Никакого предварительного знания фреймворков не нужно. Достаточно того, что ты уже умеешь запускать 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"}

Покрути этот разбор руками на виджете ниже: меняй метод, заголовки и тело, смотри, как меняется сырой текст запроса.

Анатомия HTTP-запроса
сырой запрос на проводеrequest lineзаголовкитело

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.