На даче у меня висел уличный градусник на ESP32, и показания его хотелось видеть в трёх местах: на дисплее в доме, в графике на ноутбуке и в маленькой сигнализации, которая пищала при плюсе после заморозков. Я решил по-простому: дисплей ходит к плате HTTP-запросом раз в десять секунд, ноутбук делает то же самое, сигнализация — тоже. Час работы — и схема дышала. А через неделю задохнулась: три клиента по шесть запросов в минуту — это примерно тысяча запросов в час, и в каждом запросе двести-триста байт заголовков ради пяти байт цифры. Плата вместо сонной должности почтальона превратилась в проходной двор.
Дальше — хуже. Каждый клиент знал IP платы и был прибит к нему в скетче. Роутер перезагрузился, DHCP раздал новые адреса, и я перепрошивал три устройства подряд. Потом захотелось четвёртого читателя — дисплей на кухне, — и я снова открыл скетч градусника, чтобы что-то там дописать. Лишний раз трогать работающий код ради чужого желания — плохая примета: один раз я уронил его правкой и день ловил, почему пропала публикация.
Потом я выкинул всю эту схему и поставил посреди сети брокера, а платы перевёл на MQTT. Тот же градусник перестал быть сервером: он раз в десять секунд говорил одну фразу в общий канал, и кто хотел — тот слушал. Дисплей, ноутбук, сигнализация и четвёртый пассажир подключались к каналу сами, без единой правки скетча. В этом уроке разберём эту механику словами, без зауми: кто в pub-sub кто, зачем нужен брокер и почему модель «доски объявлений» экономит и трафик, и нервы.
Доска объявлений вместо курьеров
Представьте доску объявлений в подъезде. Жилец вешает листок с заголовком «Отключение воды 14 мая» и уходит по делам: ему неважно, кто прочитает и прочитает ли вообще. Соседи заглядывают на доску и читают только те заголовки, которые им интересны. Никто ни к кому не приходит домой, никто никого не ждёт у двери. В MQTT жилец — издатель (publisher), соседи — подписчики (subscribers), а доска — брокер (broker). Единственный, кого обязаны знать все участники, — это доска.
Из этой картины следуют три развязки, ради которых модель и придумали. Первая — по адресам: издатель не знает ни одного подписчика, подписчик не знает ни одного издателя; каждый знает только брокера. Вторая — по времени: издатель и подписчик не обязаны быть в сети одновременно; листок висит на стороне брокера, и подписчик, вернувшийся из офлайна, получит то, что опубликовали без него, — по крайней мере последнее значение, если оно помечено retained-флагом, о котором был отдельный разговор в первом уроке раздела. Третья — по темпу: издатель кладёт сообщение и сразу идёт дальше, не дожидаясь, пока его прочитают; ждать в HTTP-запросе ответа не нужно.
Роли при этом съёмные. Плата с DHT22 — издатель: публикует температуру в топик. Плату с реле можно сделать подписчиком: она слушает топик с командами. А та же плата через минуту становится издателем — публикует в свой топик статуса слово done. Мои дачные устройства почти все двусторонние: телеметрия из них, команды в них, и оба потока идут через одного брокера.
#include <PubSubClient.h> WiFiClient net;PubSubClient client(net); void setup() { Serial.begin(9600); if (client.connect("dacha-greenhouse")) { Serial.println("Подключился к брокеру"); client.subscribe("dacha/pump/on"); // роль подписчика }} void loop() { client.loop(); int t = 20 + random(0, 8); String payload = String(t); client.publish("dacha/temp", payload); // роль издателя delay(3000);}Смотрите, что произошло с нагрузкой. В HTTP-схеме тысяча запросов в час шла от клиентов к плате, и каждый запрос плата обязана была встретить, распарсить и ответить. В pub-sub один издатель кладёт сообщение в топик раз в десять секунд — это 360 публикаций в час, — а брокер сам разносит копии всем, кто подписан. Если подписчиков ноль, сообщение всё равно уходит и теряется тихо: брокеру всё равно, слушает ли кто. Если подписчиков пятеро, каждый получает свою копию — и плата об этом даже не узнает: счётчик подписчиков живёт на стороне брокера.
Число с условием: в локальной сети, где пинг между платой и брокером два-пять миллисекунд, публикация доезжает за единицы миллисекунд после вызова publish. Это не значит, что она доедет всем мгновенно: брокер раскладывает копии по своим очередям, и подписчик на плохом Wi-Fi получит свою позже. Но издателю на это плевать — его работа сделана.
Отсюда же понятен ответ на вопрос «а сколько трафика съедает MQTT по сравнению с HTTP». Пакет публикации с коротким топиком и пятибайтным значением — порядка 30–50 байт вместе с обёрткой протокола, против двухсот-трёхсот байт на HTTP-запрос с заголовками. При опросе раз в десять секунд разница за сутки — мегабайты, что для роутера несущественно, а для платы на батарейке с GSM-модулем — уже аргумент.
Правила доски
| Участник | Что делает | Пример в доме | Чего он НЕ знает |
|---|---|---|---|
| Издатель | Кладёт сообщения в топик | ESP32 с DHT22 публикует dacha/temp каждые 10 секунд | Сколько подписчиков и кто именно; их имена ему недоступны |
| Подписчик | Слушает топик или маску топиков | Дисплей подписан на dacha/# и рисует всё подряд | Кто и когда опубликовал значение; адрес источника скрыт |
| Брокер | Принимает публикации, хранит подписки, раздаёт копии | Mosquitto на Raspberry Pi в кладовке | Смысла данных: он гоняет байты, не понимая, что в них |
| Топик | Адрес сообщения: строка через слэш | dacha/pump/on, dacha/temp | Содержания: это ярлык, а не папка на диске |
Средняя строка таблицы — та, про брокера, — чаще всего недооценивается. Брокер не читает ваши данные, не решает, куда их положить кроме заявленных подписок, и не пересылает ничего, чего не попросили. Это делает его безопасным соседом в сети и одновременно единой точкой отказа: упал брокер — упало всё общение. Поэтому мой брокер живёт на маленькой малине в кладовке, которая не выключается месяцами, а не на ноутбуке, который я таскаю с собой.
Отдельно про статус устройств. В HTTP-схеме я узнавал о смерти платы по таймауту запроса — через десять-пятнадцать секунд мучения. В pub-sub это видно раньше и дешевле: плата раз в минуту публикует в dacha/greenhouse/status слово alive, а дашборд, сравнив возраст последнего сообщения с текущим временем, сам пометит устройство серым, когда оно замолчит. Никакой магии: просто ещё один топик и дисциплина публикации.
Бытовая деталь, которую я недооценивал: постоянный адрес нужен только брокеру. Платы могут переезжать по DHCP сколько угодно — они не адресаты, а авторы сообщений, и их IP брокеру безразличен. Я прибил одну лишь малину: в роутере у неё статический адрес за пределами пула DHCP, и с тех пор ни один скетч не пришлось переписывать из-за перезагрузки сети.
Про топики добавлю только то, чего касается эта таблица: топик — это ярлык, а не место в файловой системе. Никакой папки dacha/temp не существует; есть строка-адрес, по которой брокер матчингит подписки. Поэтому топики можно придумывать свободно, но по правилам из первого урока: сверху вниз от общего к частному, без пробелов, в одном регистре — иначе через полгода в собственном доме не найдёшь, где живёт температура подвала.
И последнее правило доски, которое я понял на своей шкуре: подписка по маске — привилегия читателя. Подписаться можно на dacha/# и ловить всё, а опубликовать в маску — нельзя: брокер отказывается угадывать, какой конкретный топик вы имели в виду. В коде это выглядит симметрично, а по сути половины прав у publishing-стороны просто нет.
- Опубликуйте второй топик dacha/humidity со значением random(40, 81): доска получит два потока, и оба уйдут обоим читателям — так выглядит fan-out в миниатюре.
- Поменяйте топик с dacha/temp на greenhouse/temp и запустите заново: подписчики в этой симуляции слушают dacha/# и перестанут видеть поток. Один символ в имени топика — и вся схема молчит.
- Добавьте третью строку читателя после публикации: добавить подписчика в pub-sub стоит ноль правок издателя — сравните с HTTP, где каждого нового клиента пришлось бы прописывать в скетч платы.
- Загляните в Serial-лог и найдите строки, которые печатает сам брокер: они начинаются с MQTT. Это и есть тот самый обмен пакетами, который в реальной сети летит между платой и малиной.
- Pub-sub развязывает участников трижды: по адресам, по времени и по темпу — издатель не ждёт читателей
- Брокер — единственный адрес, который знают все; падение брокера останавливает весь обмен
- Издатель не знает подписчиков, подписчик не знает издателя; счётчики и подписки живут на стороне брокера
- Одна публикация — сколько угодно копий: добавление читателя не требует правок издателя
- Топик — ярлык-адрес, а не папка; маски # и + работают только в подписке
- MQTT для коротких частых событий, HTTP для файлов и прошивок — у каждого своя работа
- 1. Плата публикует температуру в топик, а в доме никто не подписан. Что будет с сообщением?
- 2. Сколько скетчей придётся править, чтобы добавить второго подписчика на существующий топик?
- 3. Кто в схеме MQTT обязан знать адреса всех участников?
- 4. Что можно делать с маской топика вроде home/#?
- 5. В каких случаях я оставляю HTTP вместо MQTT?
Карта протокола у нас теперь есть: доска, роли, топики и правила игры. Дальше по разделу — практика с двух сторон. Сначала поднимем свою доску: в уроке про Mosquitto ставим брокер на Raspberry Pi или ПК и пробуем публикацию из консоли, руками, ещё до всяких скетчей. Потом берём в руки библиотеку: первый скетч на PubSubClient подключается к нашему брокеру и публикует данные по-настоящему, а заодно учит читать коды ошибок. Если хочется сначала вспомнить, как плата вообще попадает в сеть, — вернитесь к подключению ESP32 к Wi-Fi и реле в розетке: именно такие исполнительные устройства мы будем дёргать командами с брокера.
Частые вопросы по теме урока
- Плата публикует температуру в топик, а в доме никто не подписан. Что будет с сообщением?
- Брокер принимает публикацию независимо от числа подписчиков; без подписок сообщение никуда не доставляется. Исключение — retained-флаг: тогда брокер хранит последнее значение. Брокер примет его и выбросит: слушателей нет
- Сколько скетчей придётся править, чтобы добавить второго подписчика на существующий топик?
- В pub-sub подписчики добавляются со своей стороны: издатель не знает читателей и не меняется. Это главное преимущество перед прямой рассылкой. Ни одного: подписчик подключается сам
- Кто в схеме MQTT обязан знать адреса всех участников?
- Вся адресация сведена к одному серверу: издателю и подписчику не нужны чужие IP — только адрес брокера и имена топиков. Никто: каждый знает только брокера
- Что можно делать с маской топика вроде home/#?
- Маски — инструмент читателя: # ловит все вложенные уровни. Публиковать в маску нельзя, брокер не станет угадывать конкретный топик. Только подписываться
- В каких случаях я оставляю HTTP вместо MQTT?
- Короткие частые события — территория MQTT. Объём в сотни килобайт и страницы по-прежнему проще гонять HTTP-запросами. Для прошивок и больших файлов