Сети и веб
Как работает HTTP: запросы, ответы, заголовки и коды состояния
Разбираем настоящий обмен HTTP построчно: методы, URL, заголовки, классы кодов состояния и cookie, с проверкой через curl на локальном сервере.
Порт — это пронумерованная дверь на вашей машине. Разбираем, как TCP и UDP работают с портами, затем запускаем, находим и проверяем локальный сервер на 127.0.0.1.
Порт — это число, по которому операционная система понимает, какой программе отдать порцию сетевого трафика. У вашей машины есть IP-адрес, и на ней могут одновременно работать десятки сетевых программ: браузер, который загружает страницы, почтовый клиент, база данных, локальный веб-сервер. IP-адрес доводит трафик до нужного компьютера. Порт доводит его до нужной программы.
Если вы читали, что делает DNS, то уже знаете, как имя превращается в адрес. Здесь разобрано, откуда берутся порты, как их используют два основных транспортных протокола и как посмотреть, какие порты слушает ваша машина. Лабораторная работает на 127.0.0.1, loopback-адресе, который всегда означает «эта машина», поэтому ничего за пределы компьютера не уходит.
Порт — это 16-битное число, то есть значение от 0 до 65535. В заголовках TCP и UDP есть порт источника и порт назначения. Когда пакет доходит до компьютера, операционная система читает порт назначения и отдаёт данные программе, которая попросила принимать на этом номере.
Представьте многоквартирный дом. Почтовый адрес доводит посылку до здания. Номер квартиры подсказывает курьеру, в какую дверь стучать. В этой картине IP-адрес — здание, а порт — квартира. Кто-то решает, какая квартира какая, а жилец может съехать и уступить номер другому.
Слушающий порт — это порт, про который программа, по сути, сказала операционной системе: «присылай мне соединения на этот номер». Программа в таком состоянии слушает. Когда программа завершается, операционная система освобождает порт, и там больше никто не отвечает.
Каждый запущенный сервис открывает хотя бы один порт. Это относится и к программам, которые вы не запускали намеренно: программам обновления, клиентам синхронизации, серверам разработки, плагинам и софту, который сам ставит себя фоновой службой. Список слушающих портов — самая простая и точная картина того, что ваша машина предлагает сети.
Если вы проверяете собственный хост, этот список надо записать первым. Прежде чем спрашивать, обновлён ли сервис и хорошо ли он настроен, нужно знать, что он существует. Работа по безопасности начинается с инвентаризации, а инвентаризация начинается с дверей.
TCP/IP — это набор протоколов, на котором работает интернет. Он устроен по уровням, и пока вам нужны не все из них. Здесь важны четыре:
Каждый уровень оборачивает данные верхнего уровня в свой заголовок. В IP-заголовке адреса источника и назначения. В заголовке TCP или UDP порты источника и назначения. Внутри лежат данные приложения. Когда пакет приходит, каждый уровень снимает свой заголовок и передаёт остальное выше.
Порт живёт на транспортном уровне. IP не знает, какой программе отдать данные. Это работа транспортного уровня, и поэтому один IP-адрес может одновременно отдавать веб-страницу на порту 443 и базу данных на порту 5432.
TCP, описанный в RFC 9293, даёт программе надёжный упорядоченный поток байтов. Прежде чем пойдут данные приложения, две стороны договариваются о начальных порядковых номерах и подтверждают, что каждая может отправлять и получать. Эта договорённость называется трёхсторонним рукопожатием.
Когда порт никто не слушает, целевая машина SYN-ACK не отправляет. Она шлёт сегмент RST (reset), и клиент сразу сдаётся с сообщением вроде «connection refused». Такой быстрый ответ полезен. Он говорит, что хост получил ваш пакет и что на этом порту соединения никто не принимает.
Молчаливое ожидание, которое заканчивается таймаутом, значит другое. Очень часто файрвол отбросил пакеты без ответа. «Refused» и «timed out» — две разные улики, и различать их надо уметь.
Соединения на 127.0.0.1 используют ту же логику TCP, хотя пакеты никогда не доходят до сетевой карты. Операционная система проводит рукопожатие программно, поэтому на loopback-адресе безопасно тренироваться.
UDP, описанный в RFC 768, намного проще. Его заголовок занимает 8 байт: порт источника, порт назначения, длина и контрольная сумма. Рукопожатия, подтверждений, повторной передачи и упорядочивания нет. Отправитель передаёт датаграмму, а сеть либо доставляет её, либо нет. Получатель ничего не подтверждает, если приложение само не реализует такую логику.
У этой простоты есть применение. DNS-запросы обычно идут по UDP на порт 53: короткий вопрос и короткий ответ укладываются в один обмен, а рукопожатие добавило бы лишние обмены пакетами. Голос и видео в реальном времени часто используют UDP, как и QUIC, транспорт под HTTP/3. У каждого из этих протоколов свои правила на случай потерь и повторов.
Порты работают для UDP так же. Порт назначения выбирает программу. Разница в том, что у UDP-порта нет состояния соединения. Любая программа, привязанная к этому порту, получает всё, что приходит, от кого угодно. Слушатель TCP видит только соединения, завершившие рукопожатие. Вопрос в обоих случаях один: какая программа получит этот пакет.
Internet Assigned Numbers Authority (IANA) ведёт реестр Service Name and Transport Protocol Port Number Registry. В нём записано, какой сервис обычно привязан к каждому номеру порта для TCP, UDP и нескольких других транспортных протоколов. Известные записи: 22 для ssh, 53 для domain (DNS), 80 для http и 443 для https.
RFC 6335 делит пространство номеров на три диапазона:
Реестр фиксирует назначения и соглашения. В самих TCP и UDP ничто их не навязывает. Любая программа с нужными правами может привязаться почти к любому свободному порту. Реестр говорит, что обычно работает на этом номере, и про вашу машину может ошибаться. Прежде чем делать вывод, посмотрите, что на самом деле слушает.
Нужны терминал и Python 3. Лабораторная использует встроенный в Python HTTP-сервер, поэтому дополнительно ничего ставить не надо.
Команды для Windows ниже я запускал на Windows 11 Pro с Git Bash, Python 3.12.10 и curl 8.12.1. Команды для Linux и macOS на этой машине я не запускал, поэтому их вывод не показан. Там, где есть блок вывода, он получен от команды прямо над ним.
Одно замечание про Python: в моей сессии Git Bash python3 не нашёлся, а python запустил Python 3.12. Если на Windows python3 не работает, используйте python. Остальная часть команды не меняется.
Сервер публикует всё, что лежит в папке, где он запущен, поэтому сначала положите туда что-нибудь небольшое.
mkdir -p ~/port-lab && cd ~/port-lab
printf '<!doctype html><title>lab</title><h1>Port 8000 lab</h1>\n' > index.htmlВ PowerShell создайте такую же папку и запишите эти две строки в index.html любым редактором.
Держите этот терминал открытым. Сервер работает, пока вы его не остановите.
cd ~/port-lab
python3 -m http.server 8000 --bind 127.0.0.1Флаг --bind 127.0.0.1 говорит Python слушать только на loopback-интерфейсе. Без него Python слушает на всех интерфейсах машины. Эта разница пригодится позже, когда будете читать список слушающих портов.
Выберите команду для своей операционной системы. Каждая выводит сокеты в состоянии LISTEN, которые используют порт 8000.
Linux:
ss -tlnp | grep 8000macOS:
lsof -iTCP -sTCP:LISTEN -n -P | grep 8000Windows PowerShell:
Get-NetTCPConnection -State Listen -LocalPort 8000Вот что вывела команда для Windows на моей машине, с конвейером через Format-Table:
LocalAddress LocalPort State OwningProcess
------------ --------- ----- -------------
127.0.0.1 8000 Listen 45608На вашей машине идентификатор процесса будет другим. Запустите Get-Process -Id <OwningProcess> со своим номером и убедитесь, что владелец — запущенный вами процесс Python. В моём случае это был python.
На что смотреть:
LocalAddress равен 127.0.0.1. Сокет принимает соединения только с этой машины. Если бы вы не указали --bind, здесь стоило бы ожидать 0.0.0.0. Этот вариант на своей машине я не проверял, так что считайте его ожидаемым результатом, а не наблюдённым.State равен Listen. Сокет ждёт соединений.OwningProcess указывает на ожидаемую программу. Если там что-то незнакомое, остановитесь и выясните, что это, прежде чем продолжать.Откройте второй терминал и выполните:
curl -v http://127.0.0.1:8000/Вот вывод из моего запуска, оставлены только нужные строки:
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000
> GET / HTTP/1.1
> Host: 127.0.0.1:8000
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.12.10
< Content-type: text/html
<!doctype html><title>lab</title><h1>Port 8000 lab</h1>Читайте по порядку. Trying означает, что curl открывает TCP-соединение. Адрес уже IP, поэтому DNS-запроса нет. Connected означает, что трёхстороннее рукопожатие завершилось. GET / HTTP/1.1 — это прикладной уровень: теперь curl говорит по этому соединению на HTTP. 200 OK означает, что сервер нашёл файл и ответил. Строка HTML внизу — ваш файл.
Сервер пишет строку о запросе в свой терминал. У меня это было так:
127.0.0.1 - - [08/Nov/2026 06:07:56] "GET / HTTP/1.1" 200 -Адрес источника — тот же loopback-адрес, значит запрос пришёл с этой машины.
Вернитесь в терминал сервера и нажмите Ctrl+C. Так сервер останавливается чисто. На своей машине я остановил процесс через Stop-Process -Force, что завершает его без возможности корректно закрыться. В любом случае слушающий сокет исчезает, как только процесс завершается.
Убедитесь, что сокета больше нет:
Get-NetTCPConnection -State Listen -LocalPort 8000У меня вывод был пустым. Теперь снова выполните ту же команду curl:
curl -v http://127.0.0.1:8000/Вот вывод из моего запуска:
* Failed to connect to 127.0.0.1 port 8000 after 2048 ms: Could not connect to server
curl: (7) Failed to connect to 127.0.0.1 port 8000 after 2048 ms: Could not connect to serverКод завершения curl был 7: это значит, что подключиться не удалось.
Вот что произошло. Ваш SYN дошёл до 127.0.0.1 на порт 8000. Там никто не слушал, поэтому операционная система ответила сбросом. curl сообщил об отказе сразу, не дожидаясь таймаута. Если вместо этого будет долгое ожидание и таймаут, подозревайте файрвол или маршрут, на котором пакеты отбрасываются.
Один и тот же адрес и порт вели себя по-разному. Со слушателем рукопожатие завершилось и сервер ответил. Без него отказ пришёл за миллисекунды. Разницу между этими случаями определяет список слушающих портов. Когда соединение ведёт себя неожиданно, сначала проверьте этот список.
Случайно привязать сервер ко всем интерфейсам. Многие простые серверы по умолчанию слушают на всех адресах. На ноутбуке в сети кафе так сервер разработки может оказаться доступен другим устройствам в этой сети. Если нужен только локальный доступ, явно укажите адрес привязки.
Считать номер порта доказательством того, что на нём работает. Порт 80 обычно означает HTTP, но программу ничто не заставляет говорить на нём по HTTP. Подключитесь и прочитайте, что присылает сервис, или проверьте, какому процессу принадлежит сокет.
Забывать про UDP. ss -tlnp показывает только слушателей TCP. Для UDP используйте ss -ulnp, а на Windows Get-NetUDPEndpoint. На машине может быть активный UDP-сервис, который никогда не появится в проверке TCP.
Принимать таймаут за «сервиса нет». Таймаут часто означает, что между вами и портом стоит фильтр. Отказ означает, что хост ответил и порт никто не слушает. Дальнейшие шаги в этих случаях разные.
Завершить не тот процесс. Прежде чем останавливать процесс, проверьте имя процесса-владельца и его командную строку. Сервер, оставшийся от прошлой попытки, может по-прежнему держать порт и отдавать файлы из папки, которую вы публиковать не собирались. Проверьте владельца, прежде чем считать порт своим.
Слушающие порты описывают то, что ваша машина предлагает сети, намеренно или нет. Прежде чем тестировать что-то ещё на своём хосте, выведите список слушающих портов, сопоставьте каждую запись с известной вам программой и закройте лишнее. Забытый сервер разработки — хороший пример записи, которая выглядит безобидно и всё равно отвечает.
Те же команды можно применять на машинах, которые вы не контролируете, но для сканирования или проверки чужой системы нужно письменное разрешение её владельца. Тренируйтесь на своих машинах и в своей лаборатории.
Повторите упражнение с UDP. Короткий слушатель на Python на 127.0.0.1, порт 5399, в моём запуске получил датаграмму от отправителя. Ничего особенного слушатель не делал:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("127.0.0.1", 5399))
data, addr = s.recvfrom(1024)
print("got", data, "from", addr)Запустите его в одном терминале, затем отправьте датаграмму из другого через socket.sendto. Я получил вывод got b'hello' from ('127.0.0.1', 63411). Заметьте, что рукопожатия не было. Пакет просто пришёл. Порт 5353 на моей машине уже был занят, привязка к нему завершилась ошибкой доступа, поэтому для теста я выбрал 5399.
Потом прочитайте часть RFC 9293 об установлении соединения и сравните её с описанным выше обменом пакетами. Затем переходите к статье как работает HTTP: там разобрано, как HTTP работает поверх TCP и что содержат запрос и ответ.
RFC 9293, Transmission Control Protocol (TCP), IETF, August 2022. https://www.rfc-editor.org/rfc/rfc9293
RFC 768, User Datagram Protocol, IETF, August 1980. https://www.rfc-editor.org/rfc/rfc768
Service Name and Transport Protocol Port Number Registry, IANA. https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
RFC 6335, Internet Assigned Numbers Authority (IANA) Procedures for the Management of the Service Name and Transport Protocol Port Number Registry, IETF, August 2011. https://www.rfc-editor.org/rfc/rfc6335
Сети и веб
Разбираем настоящий обмен HTTP построчно: методы, URL, заголовки, классы кодов состояния и cookie, с проверкой через curl на локальном сервере.
Сети и веб
TLS 1.3 даёт HTTPS конфиденциальность, целостность и аутентификацию сервера. Разбираем рукопожатие, цепочки сертификатов и то, чего замок в адресной строке не доказывает.
Сети и веб
Как DNS превращает имена в адреса: резолверы, авторитетные серверы, кеширование по TTL, записи A, AAAA, CNAME, MX и TXT и безопасный запрос.