Метеостанция в гараже жила без приключений два месяца: плата висела в WiFi, адрес 192.168.1.117 был в закладке браузера, а веб-страница из урока про мини-сервер открывалась с полтычка. Потом я перезапустил роутер — и закладка умерла. DHCP раздал адреса заново и выдал плате другой, 192.168.1.134; выяснить это можно было только заглянув в список клиентов роутера. С памятью на цифры у меня туго, а устройств с веб-мордой в квартире к тому моменту было четыре — вести табличку с адресами я отказался принципиально.
Выход, который всё починил, — mDNS: плата сама объявляет в локальной сети своё имя, и открываешь её не по адресу, а по имени вида esp32-garage.local. В этом уроке разберём, как работает этот опрос эфира, что нужно плате и роутеру, чтобы имя отвечало, кто из ваших устройств его поймёт, а кто нет, и почему MDNS.begin() зовут строго после подключения к сети. Плату в WiFi мы уже вводили в первом уроке раздела — здесь этот фундамент понадобится с первой же строки.
Что такое mDNS: ау-опрос вместо справочника
Обычный DNS — это справочник: браузер спрашивает у сервера, какой адрес скрывается за именем. mDNS устроен иначе и потому живёт только внутри локальной сети: запрос не уходит никуда за роутер, а рассылается всем устройствам сразу — multicast-пакетом на адрес 224.0.0.251, UDP-порт 5353. Вопрос звучит как «кто здесь esp32-garage.local?», и отвечает та плата, чьё имя совпало: она шлёт в тот же эфир свой IP. Сервера нет, настройки нет — говорящий сам себя называет, а слушающий запоминает.
Технология пришла из мира Apple, где зовётся Bonjour и встроена в macOS и iOS; на Linux её тянет демон Avahi. В Arduino-среде для ESP32 библиотека называется ESPmDNS и уже сидит в установленном пакете плат — докачивать ничего не нужно. Имя в begin пишут без суффикса: MDNS.begin("esp32-garage") — а .local библиотека допишет сама, потому что суффикс .local зарезервирован под mDNS и в обычном интернете таких адресов не бывает. На само имя есть ограничение: метка до 63 символов и без точек внутри — esp32-garage проходит, esp32.garage нет.
Выглядит обмен тривиально, и это стоит один раз прочувствовать. Плата с именем esp32-garage сама шлёт анонс в multicast-группу: «я здесь, у меня веб-сервер на 80-м». Ноутбук, который захотел открыть esp32-garage.local, спрашивает эфир, и один вопрос слышат все соседи сразу — отвечает тот, кого назвали. Поэтому mDNS не требует ни правки файла hosts, ни записи в роутере: всё держится на честном слове эфира, и ровно поэтому оно не переживает границы сети — пакеты на 224.0.0.251 маршрутизаторы наружу не выпускают. Отсюда же и обратная сторона: спрятать плату за mDNS-именем от соседей по сети нельзя — в вашей локалке её услышат все.
Три условия, при которых имя заработает. Первое: MDNS.begin() зовут после того, как плата подняла WiFi и получила адрес — объявлять имя в эфире до того, как ты вообще в сети, некому и нечем. Второе: все устройства в одной сети и в одном её сегменте: multicast-пакеты роутер за пределы сети не выпускает, поэтому .local из офиса к плате дома не долетит физически. Третье: сеть не гостевая — изоляция клиентов, о которой мы говорили в уроке про подключение к роутеру, режет и multicast: плата подключится, а имени услышать не даст никто, даже при верном коде.
Второй вызов после begin — MDNS.addService("http", "tcp", 80): плата не только отвечает на своё имя, но и рекламирует сервис — веб-сервер на порту 80. В списках сети на macOS появляется иконка с именем платы, клик — и страница открыта. Протокол и порт произвольные: так же объявляют MQTT-брокер на 1883 и другие службы — в разделе про MQTT плата и брокер ходят друг к другу уже без всяких адресов в скетче, если имена вписаны.
| Где открываете | Поймёт ли .local | Что делать |
|---|---|---|
| macOS, iPhone, iPad | Да: Bonjour из коробки | Набрать esp32-garage.local в браузере |
| Linux | Да, если стоит Avahi | sudo apt install avahi-daemon — и всё оживает |
| Windows | Не всегда: системной поддержки долго не было | Bonjour-сервис (идёт с iTunes) или адрес по IP |
| Android | Браузеры имя не резолвят | Приложение с NSD или IP-адрес в строке |
| Гостевая WiFi-сеть | Нет: multicast порезан изоляцией | Перевести телефон в основную сеть |
#include <WiFi.h>#include <ESPmDNS.h>#include <WebServer.h> WebServer server(80); void setup() { Serial.begin(115200); WiFi.begin("DachaNet", "superParol123"); while (WiFi.status() != WL_CONNECTED) { // сперва сеть... delay(500); Serial.print("."); } Serial.println(); Serial.print("ip="); Serial.println(WiFi.localIP()); if (MDNS.begin("esp32-garage")) { // имя без .local — суффикс допишут сами Serial.println("mdns: имя esp32-garage.local в эфире"); MDNS.addService("http", "tcp", 80); // реклама: веб-сервер на 80-м порту } else { Serial.println("mdns не поднялся: имя кривое или сеть не готова"); } server.on("/", []() { // та же страница из урока про сервер server.send(200, "text/plain", "Privet! Ya esp32-garage"); }); server.begin();} void loop() { server.handleClient();}MDNS.begin() возвращает bool: если он false, имя отклонено — чаще всего это точка внутри имени, пустая строка или вызов до подключения к сети. Успешный begin открывает плату по имени, но без addService списки устройств будут молчать: на прямой вопрос по имени плата ответит и так, а в перечне сервисов сети появится только после рекламы.
Два приёма на каждый день. Имена давайте по месту: esp32-garage, esp32-kitchen, esp32-shkal — когда плат больше двух, одинаковые имена начинают спорить за эфир: mDNS допускает конфликт, при котором отвечают то одна плата, то другая, и отладка превращается в ловлю блох. Второй приём — MDNS.queryService("http", "tcp"): плата сама опрашивает сеть и печатает список соседей с их IP и портами. За минуту получаешь реестр всех плат в квартире прямо из монитора порта — без списка клиентов роутера и без таблички в блокноте, от которой я когда-то отказался.
Про кэш. Устройства помнят ответы mDNS порядка двух минут — стеки по умолчанию отдают записи с TTL 120 секунд. Поэтому после смены имени в скетче старое имя может отвечать ещё какое-то время из кэша компьютера, а новое — не подхватиться сразу: перезагрузите плату и дайте две-три минуты, эфир и кэш придут в согласие. Я однажды поменял имя и полчаса перешивал плату, будучи уверенным, что «не работает», — хотя исправно работал кэш моего же ноутбука.
Симулятор: тот же вход в сеть, эфир моделируется
Журнал ниже — модель, и она честно это признаёт: строку «кто здесь esp32-garage.local?» на реальной плате печатает не скетч, а стек mDNS, когда кто-то в сети спрашивает. Смысл модели в другом: вход в сеть, адрес, имя и регулярность объявы — всё, за что отвечает скетч, здесь настоящее, и порядок строк тот же, что вы увидите в мониторе порта на живой плате.
MDNS.queryService("http", "tcp") в реальности печатает табличку вида «esp32-garage: 192.168.1.117:80» — по строке на каждую плату с рекламой http. Я вешаю этот вызов раз в минуту в отладочном скетче и за неделю собрал карту всех устройств в доме: роутер при этом знать ничего не знает, потому что спрашивают не он, а сами платы.
- Смена имени. Замените name на esp32-kitchen и запустите: журнал начнёт объявлять новое имя, а адрес останется тем же — имя и адрес в mDNS живут отдельно.
- Другой сервис. В скетче для платы добавьте рядом с http строку MDNS.addService("osc", "udp", 3333) — реклама второй службы встанет рядом с первой.
- Реестр соседей. На реальном стенде вызовите MDNS.queryService("http", "tcp") после подключения и напечатайте ответ в Serial — соберите список всех плат в сети за один прогон.
- Кэш глазами. Переименуйте плату на стенде и попробуйте старое имя — оно может отвечать пару минут из кэша компьютера: так выглядит TTL 120 секунд изнутри.
- 1. По какому адресу и порту рассылается mDNS-запрос «кто здесь esp32.local»?
- 2. Что нужно написать в MDNS.begin(), чтобы плата отвечала на имя esp32-garage.local?
- 3. Плата подключена к гостевой WiFi-сети с изоляцией клиентов. Что с mDNS?
- 4. С какого момента можно вызывать MDNS.begin()?
- 5. Сменили имя платы в скетче, а старое имя всё ещё открывается с ноутбука. Почему?
- mDNS — ау-опрос эфира: multicast на 224.0.0.251, UDP 5353; сервер не нужен — отвечает та плата, чьё имя спросили
- MDNS.begin("имя-без-.local") строго после WL_CONNECTED; MDNS.addService("http","tcp",80) — реклама веб-сервера в списках сети
- macOS/iOS и Linux с Avahi понимают .local сразу; Windows — через Bonjour-сервис; браузеры Android имена .local не резолвят
- Гостевые сети с изоляцией клиентов режут multicast: имя не услышит никто, каким верным ни был код
- Кэш mDNS живёт порядка двух минут: после смены имени дайте эфиру остыть, прежде чем перешивать всё подряд
С именем вместо адреса раздел открывается заново: сервер из урока про мини-веб-сервер теперь живёт на esp32-garage.local, и адрес перестал быть тайной, которую нужно перепроверять после каждого перезапуска роутера. Если хочется, чтобы плата и сама находила соседей, — MDNS.queryService() из экспериментов собирает реестр устройств прямо в Serial.
Дальше по маршруту раздела — две темы, которым имя тоже пригождается. NTP-клиент: научим плату брать точное время из интернета, чтобы расписание не сбивалось от перезагрузки. А HTTPS: как ходить на настоящие API с сертификатами и заодно понять, что именно отключает setInsecure(), который вы наверняка уже копировали со StackOverflow.
Частые вопросы по теме урока
- По какому адресу и порту рассылается mDNS-запрос «кто здесь esp32.local»?
- mDNS живёт на UDP-порту 5353 в multicast-группе 224.0.0.251. Один вопрос слышат все соседи по сети — отвечает та плата, чьё имя совпало. UDP 5353, multicast-группа 224.0.0.251
- Что нужно написать в MDNS.begin(), чтобы плата отвечала на имя esp32-garage.local?
- В begin пишут голую метку без точек и без .local: суффикс библиотека добавляет сама. Точки внутри имени — повод для отказа begin, и вам покажут false вместо имени. MDNS.begin("esp32-garage") — суффикс .local библиотека допишет сама
- Плата подключена к гостевой WiFi-сети с изоляцией клиентов. Что с mDNS?
- Изоляция клиентов в гостевых сетях режет multicast-трафик: плата в WiFi, но объявить имя некому. Переводите и плату, и телефон в основную сеть — проверено на двух роутерах подряд. Multicast порезан: имя в сети никто не услышит
- С какого момента можно вызывать MDNS.begin()?
- Стек должен знать свой адрес: begin зовут после статусного цикла. Раньше — строка в логе будет, а имени в сети нет. После того как WiFi.status() вернул WL_CONNECTED
- Сменили имя платы в скетче, а старое имя всё ещё открывается с ноутбука. Почему?
- Это кэш устройства: ответы mDNS хранятся порядка 120 секунд. Подождите пару минут — эфир и кэш сойдутся. Браузер держит ответ mDNS в кэше — записи живут порядка двух минут (TTL 120 секунд)