Введение в веб
Сеть снизу вверх: адреса и маршруты, TCP и UDP, DNS, HTTP и TLS, клиент и сервис на Python
Вячеслав Федоров
Лекция 12
Разработка и применение ПО
в физических исследованиях
Изложение. Двенадцатая лекция посвящена сети и вебу. Почти любые данные, нужные физику, сегодня приходят по сети: установка отдаёт телеметрию по TCP, каталог наблюдений доступен через HTTP-интерфейс, расчёт запускается на кластере по SSH, результаты отправляются на сервер группы. В рамках лекции путь запроса от программы до сервера рассматривается снизу вверх: слои и адреса, маршрутизация, транспортные протоколы TCP и UDP, DNS, HTTP и TLS, клиент на Python и, в конце, собственный веб-сервис на FastAPI. Надпись на титуле — GET, метод HTTP, которым браузер запрашивает страницу.
Демонстрации. Слайды с розовым выделением слова «Демонстрация» отмечают места, где изложение прерывается для работы в терминале виртуальной машины lab с Python 3.12, requests 2.34, httpx 0.28, FastAPI 0.142, uvicorn 0.54, curl 8.5 и nginx 1.31, далее называемой стендом: на слайде проигрывается запись тех же команд. Учебные сети строятся внутри стенда из сетевых пространств имён и получают адреса из диапазонов, отведённых для документации, а HTTP и TLS показываются на петлевом интерфейсе 127.0.0.1. Такой выбор обеспечивает воспроизводимость: на другой машине числа в выводе те же, кроме времени, случайных номеров портов, адресов MAC и номеров последовательности, а также ответов DNS, приходящих из интернета.
Содержание
От провода до собственного веб-сервиса
Изложение. Лекция состоит из шести частей. Первая рассматривает слои сети, интерфейсы и адреса. Вторая — маршрутизацию: как узел выбирает, куда отправить пакет. Третья — транспортный уровень: порты, протоколы UDP и TCP, установление соединения и контроль перегрузок. Четвёртая — прикладной уровень: DNS, URL, HTTP и TLS. Пятая — клиентскую сторону на Python: сокеты, requests, работу с API, скрапинг и асинхронные запросы. Шестая — собственный веб-сервис на FastAPI и nginx перед ним.
Цель лекции
Понимать путь запроса от программы до сервера и находить место, где он оборвался
Каждый уровень сети решает свою задачу и проверяется своей командой. Тот, кто представляет путь запроса целиком, отличает отсутствие маршрута от неразрешённого имени, закрытый порт от ошибки сервера и медленный канал от медленной программы — и знает, как получить данные из чужого сервиса и как открыть доступ к своим.
Изложение. Цель лекции — понимать путь запроса от программы до сервера и уметь находить место, где он оборвался. Когда скрипт не может получить данные, причина может лежать на любом уровне: интерфейс не включён, нет маршрута до сети, имя не разрешается, порт закрыт, сервер отвечает ошибкой или канал теряет пакеты. Каждый уровень проверяется своей командой, и порядок проверки экономит время. Вторая часть цели — прикладная: получать данные из чужих сервисов на Python и открывать доступ к своим, например к журналу запусков установки, через небольшой веб-сервис.
Путь
одного запроса
import requests r = requests.get( 'https://api.example.org/runs', timeout=5) print(r.status_code, r.json())
Пояснение
Одна строка кода запускает пять шагов на разных уровнях сети.
Шаг Протокол Чем проверить
имя → адрес DNS dig, curl к DoH
адрес → маршрут IP ip route get, tracepath
соединение с портом TCP ss, tcpdump
шифрование TLS openssl, curl -v
запрос и ответ HTTP curl -v, requests
Изложение. Рассмотрим вызов requests.get: он выглядит как одна операция, но за ним стоят пять шагов. Сначала имя api.example.org превращается в IP-адрес через DNS. Затем ядро выбирает маршрут к этому адресу по своей таблице. Далее открывается TCP-соединение с портом 443 на сервере, поверх него устанавливается шифрованный канал TLS, и только после этого по каналу уходит HTTP-запрос и приходит ответ. Каждый шаг может завершиться ошибкой, и у каждого есть своя команда проверки. Лекция проходит эти шаги снизу вверх, от адресов к HTTP, а затем переходит к клиентам и серверам на Python.
Слои и адреса
Модели OSI и TCP/IP, инкапсуляция, интерфейсы, MAC, IPv4 и IPv6
01
Слои и адреса
Маршрутизация
Транспорт
DNS, HTTP и TLS
Клиент на Python
Свой сервис
Изложение. Первая часть посвящена устройству сети слоями, инкапсуляции данных, интерфейсам и адресам канального и сетевого уровней.
Сеть
слоями
Уровни OSI Уровень TCP/IP Протоколы Единица данных
7–5 прикладной, представления, сеансовый прикладной HTTP, DNS, TLS, SSH сообщение
4 транспортный транспортный TCP, UDP, QUIC сегмент, датаграмма
3 сетевой межсетевой IP, ICMP пакет
2–1 канальный, физический сетевого доступа Ethernet, Wi-Fi кадр
Пояснение
Слой пользуется услугами нижнего и не знает его устройства: Wi-Fi появился без переписывания браузеров.
Для физика
Диагностику начинают с самой простой проверки: интерфейс, имя, маршрут, порт, ответ.
Изложение. Сеть устроена слоями: каждый решает свою узкую задачу, пользуется услугами нижнего и ничего не знает о его устройстве. Эталонная модель OSI насчитывает семь уровней, а интернет построен по более компактной модели TCP/IP из четырёх: сетевого доступа, межсетевого, транспортного и прикладного. Физический и канальный уровни OSI в ней объединены, а три верхних слиты в один прикладной. У каждого уровня своя единица данных: кадр на канальном, пакет на сетевом, сегмент TCP или датаграмма UDP на транспортном и сообщение на прикладном. Разделение на слои позволяет менять один уровень, не затрагивая остальные: Wi-Fi был изобретён без переписывания браузеров и протокола IP. При поиске неисправности уровни проверяют не строго снизу вверх, а в порядке возрастания трудоёмкости проверки: состояние интерфейса, разрешение имени, маршрут, открытый порт, содержимое ответа.
Инкапсуляция
заголовков
данные приложения
сегмент TCP: порты, номера, флаги
пакет IP: адреса отправителя и получателя, TTL
кадр Ethernet: MAC-адреса соседей и контрольная сумма FCS
Получатель
снимает заголовки в обратном порядке
Маршрутизатор
меняет кадр, уменьшает TTL, порты сохраняет
Изложение. Каждый уровень добавляет к данным свой заголовок. HTTP-запрос приложения становится полезной нагрузкой сегмента TCP, заголовок которого содержит порты, номера последовательности и флаги. Сегмент вкладывается в пакет IP с адресами отправителя и получателя и счётчиком TTL, а пакет — в кадр Ethernet с MAC-адресами соседей по сети и контрольной суммой FCS в конце. Получатель снимает заголовки в обратном порядке. Маршрутизатор на пути разбирает только кадр и пакет: он отбрасывает старый кадр, строит новый для следующего участка и уменьшает TTL на единицу, а заголовки TCP и данные не изменяет. Таким образом, MAC-адреса меняются на каждом участке пути, а IP-адреса отправителя и получателя сохраняются, пока их вместе с портами TCP не подменит трансляция адресов NAT.
Адреса
IPv4 и IPv6
Диапазон Назначение
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 частные сети (RFC 1918)
127.0.0.0/8, ::1 петлевой интерфейс, сама машина
169.254.0.0/16, fe80::/10 локальные адреса канала
192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 документация (RFC 5737)
2001:db8::/32 документация IPv6 (RFC 3849)
>>> import ipaddress >>> net = ipaddress.ip_network('192.0.2.0/24') >>> net.num_addresses, net.netmask (256, IPv4Address('255.255.255.0'))
01
IPv4
32 бита, четыре октета: 192.0.2.10
02
Префикс /24
24 старших бита — номер сети, 8 — номер узла
03
IPv6
128 бит; 2001:db8:0:0:0:0:0:1 = 2001:db8::1
Для физика
Адреса учебных сетей лекции взяты из диапазонов документации.
Изложение. IPv4 задаёт 32-битный адрес, записываемый четырьмя десятичными октетами. Запись с префиксом, например 192.0.2.0/24, означает, что 24 старших бита — номер сети, а оставшиеся 8 — номер узла; отсюда получим 2⁸ = 256 адресов в сети. Около четырёх миллиардов адресов IPv4 давно распределены, поэтому домашние и институтские сети используют частные диапазоны RFC 1918, которые не маршрутизируются в интернете, а наружу выходят через трансляцию адресов NAT. Петлевой адрес 127.0.0.1 указывает на саму машину, локальные адреса канала 169.254.0.0/16 узел назначает себе сам, когда не получил адрес. Для примеров в документации отведены диапазоны 192.0.2.0/24, 198.51.100.0/24 и 203.0.113.0/24, а в IPv6 — 2001:db8::/32; ими пользуются учебные сети этой лекции. Адрес IPv6 занимает 128 бит и записывается восемью группами шестнадцатеричных цифр; ведущие нули в группе опускаются, а одну непрерывную цепочку нулевых групп заменяет двойное двоеточие. Модуль ipaddress стандартной библиотеки выполняет арифметику адресов и префиксов.
Канальный уровень:
MAC и ARP
$ ip -br link va@if2 UP f2:82:ac:e0:e0:79 <BROADCAST,UP,…> $ ip neigh 192.0.2.20 dev va lladdr f2:80:58:c1:92:87 REACHABLE
Пояснение
Программа знает IP-адрес соседа, а кадр отправляется на MAC-адрес: соответствие выясняет ARP.
01
MAC-адрес
48 бит; у сетевой карты задан производителем, у виртуальных интерфейсов случаен
02
ARP
широковещательный вопрос «у кого 192.0.2.20?» и ответ с MAC
03
IPv6
вместо ARP — обнаружение соседей NDP
04
VLAN
логические сети на одном оборудовании
Изложение. Канальный уровень доставляет кадры между соседями в одной сети, например через коммутатор Ethernet или точку доступа Wi-Fi. Каждый интерфейс имеет 48-битный MAC-адрес: у физической сетевой карты его задаёт производитель, у виртуальных интерфейсов он выбирается случайно, а современные ОС случайно меняют его и при подключении к Wi-Fi. Программа знает IP-адрес получателя, а кадр отправляется на MAC-адрес, поэтому соответствие выясняет протокол ARP: узел рассылает широковещательный вопрос «у кого адрес 192.0.2.20?» и получает ответ с MAC-адресом, который запоминается в таблице соседей. В IPv6 ту же задачу решает обнаружение соседей NDP. Команда ip -br link показывает интерфейсы и их MAC-адреса, ip neigh — таблицу соседей. VLAN позволяет разделить одно оборудование на независимые логические сети, например чтобы сеть управления установкой не пересекалась с офисной.
Демонстрация:
сеть из двух узлов
01
Пространства имён a и b и пара veth между ними
02
Интерфейсы и адреса узла a
03
ping соседа и таблица ARP
Что наблюдается
В пространстве a только петлевой lo и конец пары va; после ping в таблице соседей появляется MAC-адрес узла b.
root@lab — запись со стенда
Демонстрация. Скрипт lan.sh строит внутри стенда сеть из двух узлов. Сетевое пространство имён является механизмом ядра Linux, который даёт процессам собственный набор интерфейсов, адресов и маршрутов; каждое пространство играет роль отдельной машины, а пространства a и b соединены парой виртуальных интерфейсов veth, которая ведёт себя как кабель. Узлы получают адреса 192.0.2.10 и 192.0.2.20 из диапазона документации. Команда ip -n a -br link показывает, что в пространстве a есть только петлевой интерфейс lo и интерфейс va со случайным MAC-адресом; ip -n a -br addr — адрес 192.0.2.10/24 и локальный адрес IPv6. Эхо-запрос ping до 192.0.2.20 проходит за десятую долю миллисекунды, а после него в таблице соседей ip -n a neigh появляется MAC-адрес узла b, полученный по ARP.
Маршрутизация
Таблица маршрутов, самый конкретный префикс, правила, диагностика пути
02
Слои и адреса
Маршрутизация
Транспорт
DNS, HTTP и TLS
Клиент на Python
Свой сервис
Изложение. Вторая часть посвящена маршрутизации: как узел выбирает, куда отправить пакет, и как проверить путь до адресата.
Таблица
маршрутов
$ ip route default via 192.0.2.1 dev va 192.0.2.0/24 dev va proto kernel scope link 198.51.100.128/25 via 192.0.2.254 dev va $ ip route get 198.51.100.200 198.51.100.200 via 192.0.2.254 dev va
Важно
Сервер соседнего корпуса не отвечает, а машины своей комнаты доступны: чаще всего нет маршрута до сети.
01
Самый конкретный
из подходящих строк выбирается префикс наибольшей длины
02
default
0.0.0.0/0 — всё, что не подошло под другие строки
03
ip route get
спрашивает ядро, какой маршрут оно выберет
Изложение. Каждый узел, включая рабочий компьютер, хранит таблицу маршрутов и для каждого пакета выбирает подходящую строку. Строка сопоставляет префикс сети со способом доставки: напрямую через интерфейс, если сеть подключена к нему, или через шлюз, которым является соседний маршрутизатор. Из всех строк, под которые подходит адрес, выбирается самая конкретная, то есть с наибольшей длиной префикса; маршрут default, он же 0.0.0.0/0, подходит под любой адрес и срабатывает последним. В примере пакет в 198.51.100.200 попадает под /25 и уходит через шлюз 192.0.2.254, хотя default тоже подходит. Команда ip route get показывает, какой маршрут ядро выберет для конкретного адреса. Когда сервер соседнего корпуса не отвечает, а машина в той же комнате доступна, в первую очередь необходимо проверить таблицу маршрутов: чаще всего маршрута до нужной сети нет.
Задача:
выбор маршрута
default via 192.0.2.1 64.0.0.0/4 via 192.0.2.2 128.0.0.0/1 via 192.0.2.19 192.0.2.0/25 via 192.0.2.9 192.0.2.48 via 192.0.2.17
Пояснение
Префикс /n задаёт число старших битов, которые должны совпасть с адресом.
Адрес Выбранная строка Почему
192.0.2.48 192.0.2.48 via 192.0.2.17 /32 — точный адрес
192.0.2.100 192.0.2.0/25 via 192.0.2.9 /25 конкретнее /1 и default
200.1.2.3 128.0.0.0/1 via 192.0.2.19 /1 конкретнее /0
10.1.1.1 default via 192.0.2.1 ничего конкретнее
Изложение. Задача взята из лекции «Как починить Интернет»: для таблицы слева определим маршрут каждого адреса. Адрес 192.0.2.48 совпадает со строкой /32, которая является маршрутом к одному узлу и конкретнее любой другой. Адрес 192.0.2.100 подходит и под 128.0.0.0/1, и под 192.0.2.0/25, охватывающую адреса от 0 до 127 в последнем октете; выбирается более длинный префикс /25, и пакет уходит через 192.0.2.9. Адрес 200.1.2.3 начинается с единичного бита, поэтому подходит под 128.0.0.0/1, которая конкретнее default. Адрес 10.1.1.1 не подходит ни под одну строку, кроме default. Префикс 64.0.0.0/4 охватывает адреса от 64.0.0.0 до 79.255.255.255: четыре старших бита адреса должны совпасть с 0100. Проверить ответы на любой машине позволяет ip route get после добавления тех же строк в таблицу учебного узла.
Несколько
таблиц и правила
$ ip rule 0: from all lookup local 32766: from all lookup main 32767: from all lookup default # ответы сети установки — через её шлюз ip route add default via 10.20.0.1 table 100 ip rule add from 10.20.0.0/16 lookup 100
01
Правила
по порядку приоритетов выбирают таблицу для пакета
02
Признаки
адрес отправителя, интерфейс, метка пакета
03
Зачем
машина в двух сетях отвечает каждой через её шлюз
Для физика
Компьютер на установке: институтская сеть и сеть управления на разных интерфейсах.
Изложение. Таблица маршрутов в Linux не одна. Какую использовать для конкретного пакета, решают правила маршрутизации, которые показывает ip rule: они просматриваются по возрастанию номера приоритета — меньший номер означает более высокий приоритет, — и по умолчанию пакет сначала ищется в таблице local с адресами самой машины, затем в main — обычной таблице, которую показывает ip route, — и в пустой таблице default. Правило может выбирать таблицу по адресу отправителя, входящему интерфейсу или метке пакета. Такую маршрутизацию по политикам необходимо настраивать на машине, подключённой одновременно к двум сетям, например к институтской и к сети управления установкой: ответы с адреса сети установки должны уходить через её шлюз, а не через маршрут по умолчанию в институтскую сеть, иначе удалённая сторона отбросит их как пришедшие с неожиданного направления.
Диагностика
пути
Команда Что показывает
ping доступность и время туда и обратно
tracepath, traceroute узлы пути по истечению TTL
mtr ping и traceroute непрерывно, потери на каждом узле
ip route get выбор маршрута самим ядром
01
TTL
каждый маршрутизатор уменьшает его на 1; на нуле пакет отбрасывается
02
MTU
наибольший размер пакета на участке; tracepath находит его по пути
03
Потери
mtr показывает, на каком узле они начинаются
Изложение. Когда маршрут есть, а пакеты не доходят, необходимо выяснить, где они теряются. Команда ping проверяет доступность адресата и измеряет время туда и обратно. Каждый маршрутизатор уменьшает поле TTL пакета на единицу и отбрасывает пакет, когда оно доходит до нуля, сообщая об этом отправителю; на этом основаны traceroute и tracepath, отправляющие пакеты с растущим TTL и перечисляющие узлы пути. Утилита tracepath также определяет MTU — наибольший размер пакета, который проходит участок без фрагментации. Утилита mtr делает то же непрерывно и подсчитывает потери на каждом узле, что позволяет показать сетевым администраторам, на каком переходе начинается разрыв. Значение TTL в ответе ping тоже информативно: Linux отправляет пакеты с TTL 64, и ответ с ttl=63 означает один маршрутизатор на пути.
Демонстрация:
маршрутизатор
01
Сети 192.0.2.0/24 и 198.51.100.0/24, узел r между ними
02
ping без маршрута: сеть недоступна
03
Маршрут по умолчанию через r: ping и tracepath
04
Более конкретный /25 и ip route get
Что наблюдается
Без маршрута — Network is unreachable; через r ответ приходит с ttl=63, а /25 перехватывает адреса .128–.255.
root@lab — запись со стенда
Демонстрация. Скрипт net.sh строит три пространства имён: узел a в сети 192.0.2.0/24, узел b в сети 198.51.100.0/24 и маршрутизатор r с интерфейсами в обеих сетях; пересылка пакетов в r включается параметром net.ipv4.ip_forward. В таблице узла a есть только подключённая сеть 192.0.2.0/24, поэтому ping до 198.51.100.10 сразу завершается сообщением Network is unreachable: ядро не знает, куда отправить пакет. После добавления маршрута по умолчанию через 192.0.2.1 ping проходит, и ответ приходит с ttl=63 — на один маршрутизатор меньше начальных 64. Утилита tracepath показывает путь: первый узел 192.0.2.1, второй — сам адресат, MTU 1500. Затем добавляется более конкретный маршрут 198.51.100.128/25 через 192.0.2.254, и ip route get показывает, что адрес 198.51.100.200 теперь уходит через него, а 198.51.100.10 по-прежнему через маршрут по умолчанию.
Транспорт
Порты и сокеты, UDP и TCP, рукопожатие, контроль перегрузок, tcpdump
03
Слои и адреса
Маршрутизация
Транспорт
DNS, HTTP и TLS
Клиент на Python
Свой сервис
Изложение. Третья часть посвящена транспортному уровню: портам и сокетам, протоколам UDP и TCP, установлению соединения, контролю перегрузок и перехвату пакетов.
Порты
и сокеты
$ ss -tln State Local Address:Port Peer Address:Port LISTEN 127.0.0.1:8000 0.0.0.0:* LISTEN 0.0.0.0:22 0.0.0.0:*
Порт Служба
22 SSH
53 DNS
80, 443 HTTP, HTTPS
5432 PostgreSQL
01
Порт
16-битный номер программы на машине
02
Соединение
четвёрка: адрес и порт с каждой стороны
03
Порты ниже 1024
привилегированные: по умолчанию занять их может только root
04
Временные порты
клиенту ядро выдаёт номер из 32768–60999
Изложение. IP доставляет пакет до машины, а на ней работают десятки программ; какой из них предназначены данные, указывает 16-битный номер порта. Сервер слушает известный порт: 22 — SSH, 53 — DNS, 80 и 443 — HTTP и HTTPS, 5432 — PostgreSQL. Порты ниже 1024 считаются привилегированными, и в Linux слушать их по умолчанию может только root. Клиенту ядро выдаёт временный порт, в Linux по умолчанию из диапазона 32768–60999. Соединение однозначно задаётся четвёркой: адрес и порт клиента, адрес и порт сервера; это позволяет тысячам клиентов одновременно подключаться к одному порту сервера. Команда ss -tln показывает слушающие TCP-сокеты, с ключом -p — и процессы, которым они принадлежат; адрес 127.0.0.1 означает, что сервер доступен только с самой машины, 0.0.0.0 — со всех интерфейсов. Если порт не слушает ни один процесс, ядро адресата сразу отвечает сегментом RST, и клиент получает Connection refused; если же пакеты отбрасывает межсетевой экран, попытка соединения завершается по тайм-ауту. Таким образом, вид ошибки указывает, где искать причину.
UDP
и
TCP
Свойство UDP TCP
соединение нет рукопожатие перед данными
доставка без гарантий подтверждения и повторы
порядок не сохраняется сохраняется
единица датаграмма поток байтов
применение DNS, видео, QUIC HTTP/1.1–2, SSH, почта
Пояснение
UDP: для DNS проще повторить запрос, чем поддерживать соединение; потерянный кадр видеосвязи уже не нужен.
Пояснение
HTTP/3 работает поверх QUIC на UDP: надёжность реализована над UDP заново.
Для физика
О потере пакетов телеметрии UDP не сообщает: пропуски выявляет счётчик в данных.
Изложение. На транспортном уровне выбирают из двух протоколов в зависимости от того, что дороже: потеря данных или задержка. UDP не устанавливает соединения, не подтверждает доставку и не сохраняет порядок: датаграмма может потеряться, прийти дважды или обогнать предыдущую, и протокол об этом не узнает. Зато накладные расходы и задержка минимальны, поэтому на UDP работают DNS-запросы, голосовая связь и видео, где потерянный фрагмент уже не нужен. TCP обеспечивает доставку потока байтов целиком, в правильном порядке и без дубликатов ценой установления соединения, подтверждений, повторных передач и контроля перегрузок. HTTP/3 работает поверх протокола QUIC, который реализует надёжную доставку и шифрование заново поверх UDP. Для физика существенно, что о потере пакетов телеметрии UDP не сообщает: номер пакета в самих данных позволяет обнаружить пропуски.
Рукопожатие
TCP
SYN, seq x
SYN-ACK, seq y, ack x+1
ACK — соединение установлено
GET / HTTP/1.1 … (данные, флаг P)
HTTP/1.0 200 OK … (данные)
FIN — сервер закрывает соединение
Изложение. Соединение TCP открывается тройным рукопожатием. Клиент отправляет сегмент с флагом SYN и случайным начальным номером последовательности x; сервер отвечает SYN-ACK со своим номером y и подтверждением x+1; клиент подтверждает его сегментом ACK, и соединение установлено. Три сегмента до первого байта полезных данных составляют задержку, которой избегает UDP. Оценим её: при времени туда и обратно 50 мс запрос не может начаться раньше чем через 50 мс. Далее данные идут сегментами, каждый принятый байт подтверждается номером ack, а размер окна сообщает отправителю, сколько данных можно передать без ожидания подтверждений. Закрывается соединение сегментами FIN с каждой стороны; сторона, закрывшая его первой, некоторое время сохраняет соединение в состоянии TIME_WAIT, чтобы запоздавшие сегменты не попали в новое соединение с той же четвёркой адресов и портов.
Демонстрация:
рукопожатие в tcpdump
01
http.server на 127.0.0.1:8000 и ss
02
tcpdump на петлевом интерфейсе
03
curl и разбор перехваченных сегментов
Что наблюдается
Флаги [S], [S.], [.] — рукопожатие; [P.] — запрос 77 байт и ответ; [F.] первым отправляет сервер HTTP/1.0.
root@lab — запись со стенда
Демонстрация. На стенде запускается встроенный веб-сервер Python на 127.0.0.1:8000, и ss -tln показывает слушающий сокет. Утилита tcpdump перехватывает на петлевом интерфейсе lo сегменты с портом 8000 и пишет их в файл, curl запрашивает корневую страницу и получает код 200. Весь обмен из файла рассмотрим по порядку: [S] — SYN клиента со случайного временного порта, [S.] — SYN-ACK сервера, [.] — ACK клиента; затем [P.] с номерами 1:78 — запрос curl длиной 77 байт, подтверждение сервера, два сегмента ответа — заголовки и тело списка файлов — и их подтверждения. Последними идут [F.] — сервер закрывает соединение первым, поскольку http.server отвечает по HTTP/1.0 без поддержания соединения, — затем FIN клиента, подтверждающий FIN сервера, и последнее подтверждение сервера. Номера seq и ack после рукопожатия tcpdump показывает относительными, отсчитанными от начальных номеров сторон.
Контроль
перегрузок
Медленный старт
окно растёт вдвое за каждый RTT до порога
Reno, CUBIC
потеря пакета — признак перегрузки: окно резко уменьшается
BBR
модель канала: измеряет полосу и RTT, потеря — не признак перегрузки
Оценка скорости TCP Reno при доле потерянных пакетов p (формула Матиса):
скорость ≈ MSS / RTT × 1,22 / √p → 1448 байт / 50 мс × 1,22 / 0,1 ≈ 350 КБ/с
Linux по умолчанию использует CUBIC; BBR включается параметром net.ipv4.tcp_congestion_control.
Изложение. TCP не знает пропускной способности пути заранее и определяет её опытным путём, регулируя окно перегрузки, которое представляет собой объём данных, отправленных без подтверждения. В начале соединения окно растёт вдвое за каждое время туда и обратно RTT; это медленный старт. Классические алгоритмы Tahoe и Reno и принятый в Linux по умолчанию CUBIC считают потерю пакета признаком перегрузки и резко уменьшают окно. На канале со случайными потерями, не связанными с перегрузкой, например на длинной линии между корпусами или по радиоканалу, это ограничивает скорость. Для Reno оценку даёт формула Матиса: скорость примерно равна размеру сегмента MSS, делённому на RTT и умноженному на 1,22/√p, где p — доля потерь. Подставив MSS 1448 байт, RTT 50 мс и потери 1 %, получим около 350 КБ/с независимо от полосы канала. Алгоритм BBR, предложенный Google в 2016 году, строит модель канала: измеряет доступную полосу и минимальный RTT и не принимает случайные потери за перегрузку. Алгоритм выбирает отправитель данных параметром net.ipv4.tcp_congestion_control.
Демонстрация:
CUBIC и BBR при потерях
01
Сеть a—r—b: на r задержка 50 мс и 1 % потерь (tc netem)
02
Сервер в b с файлом 8 МиБ, RTT по ping
03
Скачивание трижды при CUBIC
04
BBR у отправителя и снова трижды
Что наблюдается
CUBIC: 23,0–25,4 с на файл, как по формуле Матиса; BBR: 0,90–0,99 с — примерно в 25 раз быстрее.
root@lab — запись со стенда
Демонстрация. Скрипт netem.sh строит цепочку из трёх пространств имён: клиент a в сети 192.0.2.0/24, сервер b в сети 198.51.100.0/24 и маршрутизатор r между ними. На интерфейсе r, обращённом к клиенту, дисциплина очереди netem задерживает на 50 мс каждый пакет, уходящий к клиенту, и случайно теряет 1 % таких пакетов, а в обратную сторону пакеты идут без задержки; так в стенде воспроизводится длинная линия с потерями. Задержка и потери заданы на промежуточном узле, а не на самом отправителе, и укрупнение пакетов TSO, GSO и GRO на виртуальных интерфейсах выключено, чтобы потеря касалась одного пакета, а не группы пакетов. В пространстве b запускается http.server с файлом случайных данных 8 МиБ, ping показывает время туда и обратно 50 мс; первый пакет ждал ещё и ответа ARP от r, который netem тоже задержал, поэтому максимум — 101 мс, а среднее — 64 мс. При алгоритме CUBIC, который в Linux выбран по умолчанию, curl скачивает файл за 23,7, 23,0 и 25,4 с, то есть со скоростью около 0,35 МБ/с, что почти точно совпадает с полученной нами оценкой по формуле Матиса. После переключения отправителя — сервера в b — на BBR тот же файл скачивается за 0,94, 0,90 и 0,99 с, в среднем в 25 раз быстрее: потери прежние, но BBR не уменьшает окно из-за каждой из них. Параметр меняется в пространстве имён отправителя, потому что скорость определяет сторона, которая отправляет данные, а новое соединение получает алгоритм, действующий в момент его установления.
Перехват
пакетов
tcpdump -i any -nn 'tcp port 443' tcpdump -i eth0 -nn host 192.0.2.20 tcpdump -i lo -w dump.pcap 'port 8000' tcpdump -r dump.pcap -nn -A ss -tnp # чьи соединения
Пояснение
Файл .pcap открывает Wireshark: графики окна, повторы, задержки.
01
-i
интерфейс; any — все сразу
02
-nn
без разрешения имён и портов
03
фильтр
выражение BPF: host, port, tcp, udp
04
-w и -r
запись в файл и чтение
Важно
Перехват на чужой сети без разрешения недопустим: пакеты содержат чужие данные.
Изложение. Когда соединение устанавливается, а данные идут не так, как ожидается, последним средством диагностики является перехват пакетов. Утилита tcpdump слушает интерфейс, указанный ключом -i, или все сразу с any; ключ -nn отключает разрешение адресов и портов в имена, чтобы перехват сам не порождал DNS-запросов. Фильтр на языке BPF отбирает нужное: host, port, tcp, udp и их сочетания. Ключ -w позволяет записать перехват в файл и разобрать позже, в том числе в Wireshark, который строит графики окна TCP, отмечает повторные передачи и считает задержки. Команда ss -tnp показывает установленные соединения и процессы. Перехват выполняется от root; в чужой сети для него необходимо разрешение её владельца, поскольку пакеты содержат чужие данные.
DNS, HTTP и TLS
Имена и записи, URL, запрос и ответ, коды состояния, сертификаты
04
Слои и адреса
Маршрутизация
Транспорт
DNS, HTTP и TLS
Клиент на Python
Свой сервис
Изложение. Четвёртая часть посвящена прикладному уровню: системе доменных имён, адресу ресурса URL, протоколу HTTP и шифрованию TLS.
DNS:
имена в адреса
куда спрашивать про com
куда про example.com
A 104.20.23.154
кеш на время TTL
Запись Содержит Пример
A, AAAA адрес IPv4, IPv6 example.com → 104.20.23.154
CNAME псевдоним другого имени www.github.com → github.com
MX почтовые серверы с приоритетом gmail.com → 5 gmail-smtp-in…
TXT текст: проверка владения, SPF «v=spf1 …»
Изложение. Компьютеры адресуют друг друга числами, а люди — именами; связывает их система доменных имён DNS, которая представляет собой распределённую базу данных с иерархией зон. Программа обращается к рекурсивному резолверу — провайдера, институтскому или публичному, например 1.1.1.1, — а тот при необходимости опрашивает корневые серверы, серверы зоны com и, наконец, авторитетный сервер домена. Ответ кешируется на время TTL, указанное в записи: кеширование обеспечивает быстрые повторные запросы, но изменение записи распространяется не мгновенно. Чаще всего встречаются записи A и AAAA с адресами IPv4 и IPv6, CNAME — псевдоним, отсылающий к другому имени, MX — почтовые серверы с приоритетами и TXT — произвольный текст, которым, например, подтверждают владение доменом. DNS — самый частый источник жалоб «ничего не работает», поэтому разрешение имени проверяют одним из первых: dig example.com или nslookup example.com.
Демонстрация:
DNS поверх HTTPS
01
Скрипт doh.sh: запрос к 1.1.1.1 по HTTPS, ответ в JSON
02
Записи A и AAAA для example.com
03
CNAME для www.github.com
04
Почтовые серверы gmail.com
Что наблюдается
www.github.com — псевдоним github.com; у gmail.com пять MX с приоритетами 5–40; второе число в строке — оставшийся TTL в секундах.
root@lab — запись со стенда
Демонстрация. Скрипт doh.sh задаёт вопрос резолверу 1.1.1.1 не обычным пакетом DNS, а HTTP-запросом по HTTPS: Cloudflare принимает имя и тип записи в строке запроса и отвечает объектом JSON, который разбирает jq. Для example.com резолвер возвращает два адреса IPv4 и два адреса IPv6 с оставшимся временем жизни в кеше. Имя www.github.com оказывается псевдонимом: запись CNAME указывает на github.com, и резолвер сразу добавляет его адрес. Для gmail.com возвращаются пять почтовых серверов с приоритетами от 5 до 40: отправитель почты пробует сначала сервер с наименьшим числом. Тот же приём — DNS поверх HTTPS — используют браузеры, чтобы запросы имён не читались и не подменялись по пути, но в стандартном виде RFC 8484: запрос и ответ передаются двоичным сообщением DNS с типом application/dns-message, а ответ в JSON является собственным форматом Cloudflare и Google.
URL:
адрес ресурса
https://api.example.org:443/v1/runs?limit=10#top
https
://
api.example.org
:443
/v1/runs
?limit=10
#top
Часть Кому нужна
https способ взаимодействия: HTTP поверх TLS
api.example.org DNS: имя превращается в адрес
:443 транспорт: порт программы на сервере; по умолчанию 443 или 80
/v1/runs?limit=10 сервер: путь и параметры запроса
#top только браузер: на сервер не отправляется
Изложение. Адрес ресурса URL представляет собой строку единой структуры: протокол, хост, порт, путь, параметры запроса и якорь, причём каждая часть предназначена своему уровню. Протокол https означает HTTP поверх TLS. Хост разрешается через DNS в IP-адрес. Порт указывает транспортному уровню программу на сервере и обычно опускается: для https подразумевается 443, для http — 80. Путь и параметры запроса адресуют конкретный ресурс и передаются серверу; специальные символы в параметрах кодируются процентами, например пробел — как %20. Якорь после решётки на сервер не отправляется и нужен только браузеру, чтобы прокрутить страницу к нужному месту.
Запрос
и ответ
GET /hello.txt HTTP/1.1 Host: localhost HTTP/1.0 200 OK Server: SimpleHTTP/0.6 Content-type: text/plain Content-Length: 13 pulse 4.2 mA
Метод Действие Безопасный Идемпотентный
GET получить ресурс да да
HEAD только заголовки да да
POST создать, отправить данные нет нет
PUT заменить целиком нет да
PATCH изменить часть нет не обязательно
DELETE удалить нет да
Пояснение
Идемпотентный запрос можно повторить при обрыве: состояние сервера останется тем же.
Изложение. HTTP является текстовым протоколом поверх TCP. Запрос состоит из стартовой строки с методом, путём и версией, заголовков вида «имя: значение», пустой строки и необязательного тела; в HTTP/1.1 обязателен заголовок Host, потому что на одном адресе работает много сайтов. Ответ устроен так же: статусная строка с кодом, заголовки, пустая строка и тело, длину которого задаёт Content-Length. Строки разделяются парой символов CR LF. Метод сообщает, что сделать с ресурсом. Безопасные методы GET и HEAD ничего не меняют на сервере, поэтому их результат разрешено кешировать. Идемпотентные методы — GET, HEAD, PUT и DELETE — при повторе оставляют сервер в том же состоянии, что и один запрос, хотя ответ может отличаться: повторный DELETE в сервисе лекции получает 404. Это позволяет клиенту повторять их после обрыва связи; POST при повторе создаст вторую запись, а PATCH идемпотентен, только если так устроен сервер.
Коды
состояния
Класс Смысл Примеры Что делать клиенту
2xx успех 200 OK, 201 Created, 204 No Content —
3xx перенаправление 301, 302, 304 Not Modified клиент идёт по Location; 304 — копия из кеша
4xx ошибка клиента 400, 401, 403, 404, 422, 429 исправить запрос; 429 — подождать
5xx ошибка сервера 500, 502, 503, 504 повторить позже с паузой
Пояснение
401 — нет аутентификации, 403 — доступ запрещён, 404 — ресурса нет.
Для физика
Ночной сбор данных повторяет запрос только при 5xx и 429, при 4xx — останавливается.
Изложение. Самой информативной частью ответа является трёхзначный код состояния, и достаточно помнить значение первой цифры. Коды 2xx означают успех: 200 — данные в теле, 201 — ресурс создан, 204 — успех без тела. Коды 3xx — перенаправление: клиент идёт по адресу из заголовка Location, а 304 сообщает, что копия в кеше ещё действительна. Коды 4xx — ошибка клиента: 400 — некорректный запрос, 401 — нужна аутентификация, 403 — доступ запрещён, 404 — ресурса нет, 422 — запрос понят, но данные не прошли проверку, 429 — превышена частота запросов. Коды 5xx — ошибка сервера: 500 — внутренняя, 502 и 504 — ошибка или тайм-аут за промежуточным сервером, 503 — сервис временно недоступен. При 4xx повторять запрос бессмысленно, пока клиент его не исправит; при 5xx и 429 имеет смысл подождать и повторить.
Демонстрация:
HTTP вручную
01
http.server на 127.0.0.1:8000
02
Запрос из файла: строки с CR LF
03
Отправка через nc и ответ сервера
04
curl -v для отсутствующего пути
Что наблюдается
Сервер отвечает HTTP/1.0 200 OK, Content-Length: 13 и телом; несуществующий путь — 404 File not found.
root@lab — запись со стенда
Демонстрация. На стенде снова запускается http.server, а запрос записан в файл req.txt. Команда cat -A показывает, что каждая строка оканчивается парой символов CR LF, обозначенных ^M$, и что заголовки завершает пустая строка. Утилита nc открывает TCP-соединение с портом 8000 и отправляет файл как есть: в ответ приходят статусная строка HTTP/1.0 200 OK, заголовки Server, Date, Content-type, Content-Length со значением 13 и Last-Modified, пустая строка и тело — 13 байт текста файла. Сервер отвечает версией HTTP/1.0 на запрос HTTP/1.1: так устроен встроенный сервер Python. Затем curl -v запрашивает отсутствующий путь /nope: строки со знаком «>» — отправленный запрос с заголовками Host, User-Agent и Accept, строки со знаком «<» — ответ 404 File not found с HTML-страницей ошибки длиной 335 байт.
TLS:
буква S в HTTPS
01
Шифрование
посторонние на пути не читают и не меняют данные
02
Подлинность
сертификат доказывает, что сервер — тот, за кого себя выдаёт
03
Цепочка доверия
сертификат подписан центром, которому доверяет клиент
04
TLS 1.3
рукопожатие за один RTT; SSL, TLS 1.0 и 1.1 запрещены
Пояснение
Let's Encrypt выдаёт сертификаты бесплатно и автоматически по протоколу ACME.
Важно
verify=False отключает проверку подлинности: шифрование остаётся, защиты от подмены — нет.
Пояснение
HTTP/2 — много запросов одновременно в одном соединении; HTTP/3 — поверх QUIC на UDP.
Изложение. Обычный HTTP передаёт всё открытым текстом, включая пароли и токены, и любой узел на пути видит и может изменить содержимое. TLS, прежнее название которого — SSL, обеспечивает шифрование соединения и проверку подлинности сервера по сертификату: сертификат связывает имя сайта с открытым ключом и подписан удостоверяющим центром, которому доверяет клиент, через цепочку промежуточных сертификатов. Современная версия TLS 1.3 устанавливает защищённый канал за одно время туда и обратно; SSL и версии TLS 1.0 и 1.1 признаны небезопасными и запрещены. Let's Encrypt выдаёт сертификаты бесплатно и автоматически по протоколу ACME, поэтому HTTPS стал нормой и для небольших сервисов. Отключение проверки сертификата, verify=False в requests, оставляет шифрование, но снимает защиту от подмены сервера, и в рабочем коде недопустимо. HTTP/2 передаёт много запросов параллельно в одном соединении, а HTTP/3 работает поверх QUIC на UDP.
Демонстрация:
свой сертификат
01
Сертификат и ключ для localhost на сутки
02
openssl s_server на порту 8443
03
curl без доверия к сертификату
04
curl с --cacert: TLS 1.3
Что наблюдается
Без --cacert curl отказывается с кодом 60: самоподписанного сертификата нет среди доверенных; с ним — TLSv1.3 и ответ 200.
root@lab — запись со стенда
Демонстрация. Скрипт mkcert.sh создаёт ключ на эллиптической кривой P-256 и самоподписанный сертификат для имени localhost сроком на сутки; openssl x509 показывает, что владелец и издатель сертификата совпадают, CN = localhost. Команда openssl s_server запускает TLS-сервер на порту 8443 с простой страницей состояния. Запрос curl без дополнительных параметров завершается с кодом выхода 60: сертификат не подписан ни одним доверенным центром, и curl отказывается продолжать. С ключом --cacert cert.pem клиент доверяет именно этому сертификату: в журнале curl видно соединение TLSv1.3 с шифром TLS_AES_256_GCM_SHA384 и обменом ключами X25519, владельца CN=localhost и ответ HTTP/1.0 200 ok. Так проверяют внутренние сервисы с собственным центром сертификации, не отключая проверку целиком; в requests тот же приём даёт аргумент verify='cert.pem' или переменная окружения REQUESTS_CA_BUNDLE.
Клиент на Python
Сокет, requests, повторы, API и JSON, скрапинг, асинхронные запросы
05
Слои и адреса
Маршрутизация
Транспорт
DNS, HTTP и TLS
Клиент на Python
Свой сервис
Изложение. Пятая часть посвящена клиентской стороне на Python: сокетам, библиотеке requests, работе с API, скрапингу страниц и асинхронным запросам.
HTTP
через сокет
import socket with socket.create_connection(('127.0.0.1', 8000)) as s: s.sendall(b'GET /hello.txt HTTP/1.1\r\n' b'Host: localhost\r\n\r\n') reply = b'' while chunk := s.recv(4096): # до закрытия reply += chunk print(reply.split(b'\r\n\r\n')[1])
Пояснение
Протокол — текст в открытом TCP-соединении; далее необходимо найти конец заголовков, дочитать тело, учесть кодировку и перенаправления.
Для физика
Сокеты нужны для своих протоколов: телеметрия установки, SCPI-команды приборам.
Изложение. Рассмотрим HTTP-запрос, собранный вручную: он показывает, что протокол представляет собой текст, записанный в открытое TCP-соединение. Функция socket.create_connection открывает соединение, sendall отправляет строки запроса, а recv в цикле читает ответ, пока сервер не закроет соединение: http.server отвечает по HTTP/1.0 и закрывает его сам. Одного вызова recv недостаточно: он возвращает только то, что уже пришло, а сервер отправляет заголовки и тело отдельными сегментами. Тело отделено от заголовков пустой строкой. Однако полноценному клиенту необходимо найти конец заголовков, дочитать тело по Content-Length или по блокам при Transfer-Encoding: chunked, учесть кодировку, обработать перенаправления, сжатие и шифрование. Таким образом, в работе пользуются готовыми библиотеками, а сокеты остаются для собственных протоколов, например для телеметрии установки или текстовых команд SCPI, которыми управляют осциллографами и генераторами.
requests
с тайм-аутом и повторами
r = requests.get(url, params={'limit': 10}, timeout=5) r.raise_for_status() # 4xx, 5xx — исключение data = r.json() s = requests.Session() retry = Retry(total=4, backoff_factor=0.5, status_forcelist=[429, 503]) s.mount('https://', HTTPAdapter(max_retries=retry))
01
timeout
без него зависший сервер останавливает программу навсегда
02
raise_for_status
ошибка HTTP становится исключением
03
Session
общие заголовки и переиспользуемое соединение
04
Retry
повтор с растущей паузой; учитывает Retry-After
Изложение. Библиотека requests берёт на себя соединения, перенаправления, кодировки и разбор JSON. Параметры запроса передаются словарём params, тело в JSON — аргументом json, а метод raise_for_status превращает ответ с кодом 4xx или 5xx в исключение. Аргумент timeout необходимо указывать всегда: по умолчанию requests ждёт ответа сколь угодно долго, и зависший сервер останавливает ночной сбор данных навсегда. Объект Session хранит общие заголовки, например ключ доступа, и переиспользует TCP-соединение, что позволяет быстрее выполнять серию запросов к одному серверу. Повтор с растущей паузой подключается адаптером с объектом Retry из urllib3: он повторяет запрос при перечисленных кодах, удваивает паузу и учитывает заголовок Retry-After, которым сервер сообщает, сколько ждать. По умолчанию Retry повторяет только идемпотентные методы — GET, HEAD, PUT, DELETE, OPTIONS и TRACE, — а POST не повторяет.
Демонстрация:
тайм-аут и повтор
01
Сервер flaky.py: два ответа 503 на /data, /slow — через 3 с
02
requests с timeout=1 к /slow
03
Session с Retry к /data
Что наблюдается
Запрос к /slow прерывается ReadTimeout через 1 с; к /data — 200 с третьей попытки за 1,0 с, в журнале 503, 503, 200.
root@lab — запись со стенда
Демонстрация. Скрипт flaky.py изображает ненадёжный сервис на основе http.server: путь /data на первые два запроса отвечает 503 Service Unavailable, а /slow отвечает только через 3 с. Клиент retry.py сначала запрашивает /slow с тайм-аутом 1 с и получает исключение ReadTimeout, вместо того чтобы ждать. Затем сессия с адаптером Retry, допускающим четыре повтора при коде 503 с коэффициентом паузы 0,5, запрашивает /data: первый повтор выполняется сразу, второй — через секунду, и третий запрос получает ответ 200 с данными JSON; всё занимает около одной секунды. Журнал сервера подтверждает два ответа 503 и один 200. Без повтора та же программа завершилась бы ошибкой на первом же отказе.
API
и JSON
import os token = os.environ['ARCHIVE_TOKEN'] s = requests.Session() s.headers['Authorization'] = f'Bearer {token}' r = s.get(f'{API}/runs', params={'page': 2}, timeout=10)
Важно
Ключ доступа — пароль: хранится в переменной окружения, не в репозитории.
01
JSON
объекты и списки, строки, числа, true, false, null
02
NaN
в JSON нет: пропуск кодируют null
03
Квота
429 Too Many Requests — сделать паузу
04
OAuth
токен с ограниченными правами вместо пароля
Изложение. Если у источника есть API, взаимодействие ведётся на языке данных: запрос с параметрами, ответ в JSON с оговорённым набором полей вместо вёрстки, меняющейся при каждой смене дизайна. JSON почти взаимно однозначно соответствует словарям и спискам Python, но в стандарте нет значений NaN и бесконечности: json.dumps по умолчанию записывает их как NaN и Infinity, однако такая строка уже не JSON, и чужой разборщик завершится на ней ошибкой, поэтому пропуски в измерениях кодируют явным null. Ключ доступа к API является паролем, и хранить его необходимо в переменной окружения или конфигурации вне репозитория. Сервис ограничивает частоту запросов квотой: превысивший её клиент получает 429 Too Many Requests, а при повторных превышениях — блокировку. Для доступа к личным данным пользователя применяется OAuth: сервис выдаёт программе токен, ограниченный по правам и сроку и отзываемый в любой момент, и токен передаётся в заголовке Authorization.
Скрапинг
страниц
soup = BeautifulSoup(page.content, 'html.parser') for tr in soup.select('table#magnets tr')[1:]: cells = [td.get_text(strip=True) for td in tr.find_all('td')]
Важно
Без charset в заголовке requests декодирует text/html как ISO-8859-1: разбирают байты page.content.
01
robots.txt
правила обхода, которые просит соблюдать владелец
02
User-Agent
имя программы и контакт автора
03
Паузы и кеш
не больше запроса в секунду, без повторных загрузок
04
API лучше
вёрстка меняется без предупреждения
Изложение. Не у всякого источника есть интерфейс для программ: таблицы параметров установок, архивы наблюдений и расписания часто существуют только как веб-страницы. Разбирать HTML регулярными выражениями бесполезно, поскольку разметка вложенная и часто некорректная; библиотека BeautifulSoup строит из страницы дерево и позволяет искать в нём по тегам, атрибутам и селекторам CSS. Отдельная трудность связана с кодировкой: если сервер не указал charset в заголовке Content-Type, requests декодирует текстовый ответ как ISO-8859-1, и русский текст превращается в набор знаков; передавая BeautifulSoup байты page.content, кодировку берут из тега meta самой страницы. Правила корректного скрапинга: читать robots.txt, где владелец сайта указывает, что разрешено обходить, представляться в заголовке User-Agent с контактом автора, делать паузы между запросами и кешировать скачанное, обрабатывать ошибки. Если у сайта есть API, лучше пользоваться им.
Демонстрация:
таблица со страницы
01
Каталог site через http.server
02
robots.txt: разрешённые и запрещённые пути
03
Кодировка ответа и разбор байтов
04
Строки таблицы магнитов
Что наблюдается
/private/ запрещён в robots.txt; без charset requests выбрал ISO-8859-1 — заголовок испорчен, из байтов — «Магнитная система канала».
root@lab — запись со стенда
Демонстрация. Встроенный сервер раздаёт каталог site со страницей magnets.html — таблицей токов и полей магнитов канала — и файлом robots.txt, запрещающим обход каталога /private/. Скрипт scrape.py сначала читает robots.txt модулем urllib.robotparser: страница таблицы разрешена, журнал смены — запрещён. Затем requests загружает страницу с заголовком User-Agent. Сервер не указал charset, поэтому requests выбрал кодировку ISO-8859-1, и заголовок страницы, разобранный из page.text, превращается в набор знаков; разбор байтов page.content даёт правильный заголовок «Магнитная система канала», поскольку BeautifulSoup берёт кодировку из тега meta. Строки таблицы печатаются с токами в амперах и полями в теслах.
Асинхронные
запросы
async def main(): limit = asyncio.Semaphore(10) async with httpx.AsyncClient() as client: async def one(url): async with limit: return await client.get(url) return await asyncio.gather(*map(one, urls))
Пояснение
Время уходит на ожидание сети: пока один ответ в пути, отправляются другие запросы.
Важно
Тысяча одновременных запросов для чужого сервера неотличима от атаки: параллелизм ограничивают.
Пояснение
httpx по умолчанию ждёт ответа 5 с, requests — без ограничения.
Изложение. Скачать тысячу файлов из архива по одному означает тысячу раз дождаться ответа сервера: процессор простаивает, канал загружен на несколько процентов. Асинхронный код позволяет вести много запросов одновременно и разбирать ответы по мере поступления. Библиотека httpx повторяет интерфейс requests и поддерживает оба режима: синхронный Client и асинхронный AsyncClient, поэтому при переходе код почти не переписывается. Параллелизм необходимо ограничивать, например семафором на десять запросов: сто одновременных запросов ускоряют сбор данных в десятки раз, а тысяча выглядит для чужого сервера как атака. В отличие от requests, httpx по умолчанию ограничивает ожидание пятью секундами, а пул клиента — сотней соединений: запрос, ожидавший свободного соединения дольше тех же пяти секунд, завершается исключением PoolTimeout, и при сотнях одновременных запросов без семафора к медленному сервису многие из них завершаются этим исключением. Последний выпуск httpx — 0.28.1 от декабря 2024 года; с 2026 года развитие проекта с тем же интерфейсом продолжает компания Pydantic под именем httpx2, и Starlette 1.7 уже рекомендует его для тестового клиента. Устройство async и await рассматривается в главе об асинхронности.
Демонстрация:
асинхронный httpx
01
Сервис slow.py на FastAPI: ответ через 0,2 с
02
20 запросов по очереди через httpx.Client
03
20 запросов асинхронно, не больше 10 одновременно
Что наблюдается
По очереди — 4,13 с, асинхронно — 0,47 с: две волны по 10 запросов вместо двадцати ожиданий подряд.
root@lab — запись со стенда
Демонстрация. Сервис slow.py на FastAPI отвечает на каждый запрос через 0,2 с, ожидая в asyncio.sleep, и поэтому обслуживает много запросов одновременно. Скрипт fetch.py отправляет ему 20 запросов двумя способами. По очереди через синхронный httpx.Client это занимает 4,13 с, то есть двадцать ожиданий по 0,2 с подряд и немного накладных расходов. Асинхронно через AsyncClient с семафором на десять запросов — 0,47 с: запросы идут двумя волнами по десять, и каждая волна занимает около 0,2 с. Все ответы в обоих случаях имеют код 200. Таким образом, выигрыш определяется отношением времени ожидания к времени обработки и ограничен семафором.
Свой сервис
REST, FastAPI и проверка данных, документация, nginx перед приложением
06
Слои и адреса
Маршрутизация
Транспорт
DNS, HTTP и TLS
Клиент на Python
Свой сервис
Изложение. Шестая часть посвящена серверной стороне: принципам REST, собственному сервису на FastAPI и nginx перед приложением.
REST:
ресурсы и методы
Запрос Действие Ответ
POST /runs создать запуск 201 Created
GET /runs?min_energy=3 список с фильтром 200 OK
GET /runs/2 один запуск 200 или 404
PUT /runs/2 заменить целиком 200 или 404
DELETE /runs/2 удалить 204 или 404
Для физика
Журнал запусков установки: коллеги читают его из своих скриптов, а не просят выгрузку.
Пояснение
Ресурс — существительное в пути, действие — метод HTTP, результат — код состояния.
Изложение. Собственный веб-сервис нужен, когда данные или расчёт должны быть доступны другим программам: журнал запусков установки, архив измерений, расчёт по запросу. Распространённым стилем таких интерфейсов является REST: ресурс задаётся существительным в пути, например /runs и /runs/2, действие — методом HTTP, а результат — кодом состояния. Создание выполняется методом POST и возвращает 201 Created, чтение — GET с параметрами фильтра, замена — PUT, удаление — DELETE с ответом 204 No Content; обращение к несуществующему запуску возвращает 404. Такой интерфейс понятен без документации и одинаково вызывается из Python, curl и браузера. В главе «Работа с базами данных» те же четыре операции — чтение, добавление, замена и удаление — реализованы для учебного списка записей поверх четырёх хранилищ; журнал запусков в этой лекции хранится в SQLite.
FastAPI
и проверка данных
class Run(BaseModel): energy: float = Field(gt=0) # МэВ current: float = Field(ge=0) # мА @app.post('/runs', status_code=201) def create(run: Run): _, new_id, _ = sql('INSERT INTO runs …', (run.energy, run.current)) return {'id': new_id, **run.model_dump()}
01
Модель Pydantic
типы и ограничения полей; ошибка — ответ 422
02
Параметры
из пути и строки запроса по аннотациям функции
03
Документация
/docs и /redoc строятся из кода автоматически
04
uvicorn
сервер ASGI: uvicorn app:app --port 8000
Изложение. FastAPI представляет собой веб-фреймворк, строящий сервис из обычных функций Python с аннотациями типов. Декоратор связывает функцию с методом и путём, параметры пути и строки запроса извлекаются по аннотациям, а тело запроса описывается моделью Pydantic с типами и ограничениями полей: энергия пучка положительна, ток неотрицателен. Если пришедшие данные не проходят проверку, FastAPI сам отвечает кодом 422 со списком ошибок, и функция не вызывается. Из тех же аннотаций автоматически строится описание интерфейса в формате OpenAPI и интерактивная документация по адресам /docs и /redoc. Для запуска приложения сервером ASGI uvicorn достаточно команды uvicorn app:app --port 8000. Каждая операция с SQLite в примере открывает своё соединение и выполняет запрос в отдельной транзакции, что обеспечивает безопасность при параллельных запросах.
Демонстрация:
сервис на FastAPI
01
uvicorn с приложением app.py
02
POST двух запусков, GET с фильтром
03
DELETE и GET удалённого запуска
04
Некорректные данные и журнал запросов
Что наблюдается
201 с id, фильтр min_energy=3 оставляет один запуск, DELETE — 204, затем 404; энергия −1 и ток «много» — 422 с двумя ошибками.
root@lab — запись со стенда
Демонстрация. Сервис app.py запускается uvicorn на порту 8000, а журнал запросов пишется в файл. Два запроса POST с данными JSON создают запуски с энергией 2,0 и 5,5 МэВ и получают ответы с идентификаторами 1 и 2. Запрос GET с параметром min_energy=3 возвращает только второй запуск. DELETE первого запуска отвечает кодом 204 без тела, а GET удалённого запуска — кодом 404 с сообщением «нет такого запуска». Запрос с отрицательной энергией и током «много» до функции не доходит: FastAPI отвечает 422 и перечисляет две ошибки — энергия должна быть больше нуля, ток должен быть числом. Журнал uvicorn показывает каждый запрос с методом, путём и кодом состояния.
Результат:
документация /docs
Что наблюдается
Пять операций и схема Run построены из кода; кнопки Try it out и Execute отправляют запрос прямо из браузера.
Демонстрация. Та же программа, запущенная на стенде для доступа извне, открывается в браузере по адресу /docs. Страница Swagger UI построена FastAPI из аннотаций и описаний полей в коде, без отдельно написанной документации: пять операций с методами и путями, модель Run с полями energy и current, схемы ошибок проверки. Каждую операцию можно развернуть, заполнить параметры и отправить запрос: кнопка Try it out открывает поля для ввода, а Execute отправляет запрос и показывает ответ и эквивалентную команду curl. По адресу /openapi.json тот же сервис отдаёт машиночитаемое описание интерфейса, по которому строятся клиенты на других языках.
nginx
перед приложением
location /api/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header X-Forwarded-For $remote_addr; }
Пояснение
nginx завершает TLS, раздаёт статику, ограничивает частоту, распределяет нагрузку между копиями.
Изложение. Перед приложением обычно размещают обратный посредник, reverse proxy, чаще всего nginx. Он принимает соединения клиентов, завершает TLS, раздаёт статические файлы, ограничивает частоту запросов и передаёт запросы к API приложению, а при нескольких копиях приложения распределяет между ними нагрузку. Если в адресе директивы proxy_pass указан путь, здесь — одна косая черта, nginx заменяет им часть пути запроса, совпавшую с location: запрос /api/runs уходит приложению как /runs, а без пути он ушёл бы с префиксом /api/. Заголовок X-Forwarded-For передаёт приложению адрес настоящего клиента, иначе в журнале все запросы будут от nginx. Такая схема — nginx снаружи, приложение на внутреннем порту — является основой домашнего задания «Деплой стартапа „Котики в мир“» и подробнее разбирается в лекции о системном дизайне.
Демонстрация:
nginx в контейнере
01
uvicorn с приложением на :8000
02
nginx.conf и запуск контейнера nginx:alpine
03
Запросы через :8080: статика и /api/
04
Заголовки и журнал nginx
Что наблюдается
Ответ /api/runs приходит от приложения, но с заголовком Server: nginx/1.31.6; в журнале nginx — все три запроса.
root@lab — запись со стенда
Демонстрация. Приложение из предыдущей демонстрации запускается на порту 8000, а перед ним — nginx в контейнере nginx:alpine с сетью хоста и конфигурацией из файла nginx.conf. Корневой путь nginx обслуживает сам и отвечает строкой «nginx: статика, /api/ — приложению». Запрос POST к /api/runs через порт 8080 доходит до приложения и создаёт запуск, а GET того же пути возвращает ответ 200 с заголовком Server: nginx/1.31.6 — клиент обменивается данными только с nginx, а приложение ему не видно. Журнал контейнера показывает все три запроса с кодами 200, 201 и 200. После показа контейнер удаляется.
Итоги
лекции
01
Сеть устроена слоями: каждый проверяется своей командой — ip, ss, tcpdump, dig, curl.
02
Маршрут — самый конкретный подходящий префикс; ip route get показывает выбор ядра.
03
TCP надёжен ценой рукопожатия и контроля перегрузок; при случайных потерях скорость сохраняет BBR.
04
HTTP — текст: метод, путь, заголовки, код состояния; TLS добавляет шифрование и подлинность.
05
Клиент — requests или httpx с тайм-аутом и повтором; свой сервис — FastAPI за nginx.
Изложение. Сеть устроена слоями, и каждый уровень проверяется своей командой: ip link и ip addr — интерфейсы, ip route — маршруты, ss — сокеты, tcpdump — пакеты, dig — имена, curl -v — ответ сервера. Узел выбирает самый конкретный подходящий маршрут, а ip route get показывает выбор ядра. TCP гарантирует доставку ценой рукопожатия и контроля перегрузок, и на канале со случайными потерями алгоритм BBR сохраняет скорость, которую теряет CUBIC. HTTP — текстовый протокол из метода, пути, заголовков и кода состояния, а TLS добавляет шифрование и проверку подлинности сервера. Клиент на Python — requests или httpx с обязательным тайм-аутом и повтором при 5xx и 429, собственный сервис — FastAPI с проверкой данных за nginx.
Упражнения
и задание
01
Задача о маршрутах
таблица со слайда в пространстве имён: проверить ответы через ip route get
02
Свой tcpdump
перехватить рукопожатие HTTPS-запроса и найти в нём ClientHello
03
Сбор данных
клиент к публичному API с тайм-аутом, повтором и паузой между запросами
04
Журнал запусков
добавить к сервису PATCH и фильтр по току, проверить через /docs
Пояснение
Задание курса: «Деплой стартапа „Котики в мир“» — nginx перед приложением в Docker.
Для физика
Поиск неисправности в сети начинается с проверки имени, маршрута и порта.
Изложение. Упражнения служат самопроверке. Первое — собрать в сетевом пространстве имён таблицу маршрутов из задачи о выборе маршрута и проверить ответы командой ip route get. Второе — перехватить tcpdump рукопожатие HTTPS-запроса и найти в нём сообщение ClientHello с именем сервера. Третье — написать клиент к публичному API, например к архиву препринтов, с тайм-аутом, повтором при 5xx и 429 и паузой между запросами. Четвёртое — добавить к журналу запусков метод PATCH и фильтр по току и проверить их через страницу /docs. Заданием курса к этой лекции служит «Деплой стартапа „Котики в мир“»: приложение в контейнере Docker, а перед ним nginx.
Источники
6
Mathis M. et al. The Macroscopic Behavior of the TCP Congestion Avoidance Algorithm. – ACM SIGCOMM CCR, 1997. – Vol. 27, No. 3
doi.org/10.1145/263932.264023
Изложение. Основой лекции служат главы книги курса «Компьютерные сети» и «Веб-технологии», а также раздел «Одна задача в четырёх хранилищах» из главы «Работа с базами данных»; порядок сетевой части следует лекции «Как починить Интернет» Курсов информационных технологий 2024 года. Для самостоятельного изучения рекомендуются учебник Куроуза и Росса о компьютерных сетях, справочник MDN по HTTP, статьи Матиса и соавторов о скорости TCP при потерях и о BBR, документация библиотек и стандарты HTTP, TLS 1.3 и TCP.
Спасибо за внимание
Вопросы
phys-dev.github.io/soft-dev-book
Изложение. Надпись на последнем слайде — 200, код успешного ответа HTTP, парный к методу GET на титуле. Лекция завершается ответами на вопросы.