9.15. Имена и DNS

Каждая страница до сих пор использовала числовые IP-адреса – 192.168.1.20 и подобные. Реальные приложения почти никогда так не делают. Серверы именуются example.com или api.example.com, и приложение во время выполнения находит для имени IP, чтобы отправлять пакеты. Этот поиск – Domain Name System, или DNS.

9.15.1. Во что разрешается имя

Имя – это просто метка. example.com само по себе не несёт никакой IP-информации – его нужно искать, так же как номер телефона ищется в телефонной книге. Инфраструктура DNS – это распределённая телефонная книга интернета, а результат поиска – это один или несколько IP-адресов, к которым камера может подключиться.

example.com  ->  93.184.216.34

Одно имя часто разрешается в несколько адресов (для балансировки нагрузки, географической избыточности, версий одного сервиса по IPv4 и IPv6). Подходит любой из них; приложение выбирает один и пробует его, переходя к следующему в случае неудачи.

9.15.2. Как происходит поиск

Когда камера запрашивает example.com:

  1. Камера отправляет небольшую UDP-датаграмму (да, UDP – см. UDP – отправь пакет и надейся на лучшее) на настроенный DNS-сервер. Адрес DNS-сервера пришёл из того же обмена DHCP, который выдал камере её собственный IP.

  2. У DNS-сервера ответ может уже находиться в кэше (его недавно спрашивали). Если так, он отвечает немедленно.

  3. Если нет, DNS-сервер обходит глобальную иерархию DNS: спрашивает у корневых серверов о .com, спрашивает у тех серверов о example.com, спрашивает у тех серверов об имени. Весь обход дерева скрыт от камеры; камера видит один запрос и один ответ.

  4. DNS-сервер кэширует результат на будущее и отправляет ответ обратно камере в виде ещё одной UDP-датаграммы.

Весь обмен обычно занимает несколько миллисекунд при тёплом кэше и до сотни или около того при холодном.

9.15.3. Интерфейс Python

Функция getaddrinfo() выполняет поиск и возвращает адрес, готовый для передачи конструктору сокета:

import socket

addr = socket.getaddrinfo("example.com", 80)[0][-1]
print(addr)
# ('93.184.216.34', 80)

Сигнатура – getaddrinfo(host, port). Возвращаемое значение – это список кортежей из 5 элементов (по одному на каждый разрешённый адрес); [0] выбирает первый из них, а [-1] выбирает последнее поле, которым является кортеж (ip, port), который можно сразу передать сокету:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(socket.getaddrinfo("example.com", 80)[0][-1])
    s.send(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
    ...

Большинство кода камеры, общающегося с удалённым сервером, начинается с этой одной строки. Помощники asyncio (asyncio.open_connection()) выполняют поиск внутри себя, если вместо числового IP передано имя, поэтому асинхронный код обычно не вызывает getaddrinfo() напрямую.

9.15.4. Что может пойти не так

  • Нет доступного DNS-сервера. Если Wi-Fi только что поднялся и связь нестабильна, первый вызов getaddrinfo() может завершиться по таймауту. Возбуждается OSError; повторите попытку, когда связь стабилизируется.

  • Имя не существует. Опечатка или устаревшее имя возбуждает OSError после того, как DNS-сервер вернёт «нет такого имени». Код ошибки отличает это от «DNS-сервер недоступен», но для большинства приложений с камерой политика повтора/отказа одинакова.

  • Возвращённый адрес не работает. DNS может вернуть адрес, на котором сервис больше не размещён. Решение – перейти к следующей записи в списке результатов getaddrinfo() или выполнить поиск имени снова позже (к тому времени кэш DNS, скорее всего, обновится).

  • Captive-порталы. Некоторые сети Wi-Fi перехватывают DNS и возвращают IP страницы captive-портала для всего. Камера будет казаться подключённой, но получаемые данные не будут соответствовать тому, что отправил бы реальный сервис. Не часто встречается в развёрнутых средах, но случается в Wi-Fi на конференциях и подобных сетях.

9.15.5. Собственное имя камеры

Страницы до сих пор искали имена других устройств. У камеры также есть собственное имя, которое она объявляет в локальной сети, когда запрашивает адрес. По умолчанию это обобщённый идентификатор, различающийся в зависимости от платы; network.hostname() переопределяет его на что-то, что распознает остальная сеть:

import network
network.hostname("kitchen-cam")
# ... then bring the link up as usual ...

Установите имя хоста до поднятия интерфейса, чтобы имя ушло в составе первоначального запроса адреса камеры, а не после.

Теперь происходят две вещи, как только камера присоединилась к сети. Во-первых, большинство домашних маршрутизаторов регистрируют имена хостов, которым они выдают адреса, в своём локальном поиске, так что другие устройства могут обращаться к камере как kitchen-cam – не зная числового адреса, который маршрутизатор ей назначил. (Корпоративные сети могут поддерживать это, а могут и нет; поведение зависит от маршрутизатора.) Во-вторых, камера сама запускает mDNS-ответчик прямо из коробки, поэтому то же имя также доступно как kitchen-cam.local в любой сети, клиенты которой понимают mDNS – а это делают большинство современных настольных операционных систем.

Примечание

Передавайте в network.hostname() голое имя хоста – просто "kitchen-cam", без суффикса .local. Форму .local добавляет mDNS во время поиска; встраивание её в имя хоста заставляет камеру объявлять kitchen-cam.local как обычное имя хоста, что не соответствует ожиданиям ни одной из сторон поиска.

9.15.6. Когда имён недостаточно

Есть несколько ситуаций, в которых DNS не помогает:

  • Обнаружение в локальной сети. Стандартный DNS отвечает на вопросы об именах, зарегистрированных в глобальном каталоге; он ничего не знает об устройствах в локальном сегменте. Multicast DNS (mDNS) – это система, которая заполняет этот пробел. Каждое участвующее устройство присоединяется к специальной multicast-группе в локальной сети и слушает запросы; когда устройство запрашивает имя, оканчивающееся на .local, то устройство, которое владеет этим именем, отвечает напрямую. Никакого центрального сервера, никакой настройки DNS. Bonjour на устройствах Apple, Avahi на Linux и Windows 10+ – все говорят на одном протоколе, поэтому имя kitchen-cam.local, настроенное в предыдущем разделе, разрешается в домашней сети без какой-либо дополнительной настройки.

    Сторона камеры в этом обмене – это ответчик, и он уже работает. Чего у камеры нет, так это резолвера – другой половины, которая позволила бы скрипту спросить у сети «где printer.local?» и получить ответ. Входящий в комплект код mDNS работает только как ответчик (встраиваемые устройства обычно являются тем, что находят, а не тем, кто ищет). Когда обнаружение должно идти в другом направлении, UDP-широковещание (см. UDP – отправь пакет и надейся на лучшее) – более простое решение для случая локального сегмента, либо чисто питоновский модуль, такой как cbrand/micropython-mdns, добавляет полноценный резолвер.

  • Имена IPv6. getaddrinfo() возвращает результаты как IPv4, так и IPv6, если доступны оба. Выберите семейство, которое может использовать сокет приложения.

Для большей части кода на стороне камеры getaddrinfo – это одна строка в начале любой функции, открывающей сетевое соединение. Примеры в других местах этого раздела, работавшие с "192.168.1.20", точно так же работали бы с публичным именем вроде "api.example.com" – нужно просто сначала выполнить разрешение.