Насос в теплице однажды проработал всю ночь, хотя по плану должен был отключиться в девять вечера. Утренний разбор: команда стоп уходила с QoS 0 ровно в тот момент, когда роутер на две секунды потерял плату из виду. Пакет умер молча — QoS 0 не подтверждает доставку и не повторяет попыток: отправил и забыл. Wi-Fi вернулся, плата продолжала публиковать температуру как ни в чём не бывало, но приказ никто не повторил. Счёт за электричество пришёл раньше осознания: два киловатта впустую за ночь — цена одного неподтверждённого пакета, которого я даже не заметил.
Зеркальная грабля случилась с тумблером света в дашборде: я отправлял toggle с QoS 1 при неуверенной связи — и свет иногда щёлкал дважды. QoS 1 гарантирует минимум один раз: подтверждение PUBACK потерялось, клиент шлёт заново, брокер доставляет второй экземпляр. Команда-переключатель от дубликата инвертируется: включить и выключить — и вы снова не там, где хотели. Заметьте, обе истории — про одну и ту же дырку в понимании: я знал про уровни из таблицы, но не знал, как они устроены в пакетах и что именно обещают.
В базовом уроке про устройство протокола уже есть таблица выбора QoS — пересказывать её не буду. Сейчас спускаемся этажом ниже: какие пакеты ходят на каждом уровне, во сколько обходятся гарантии и почему PubSubClient умеет только половину стандарта. Спойлер: вторая половина — та, что стоит в четырёх пакетах.
Три уровня глазами пакетов
QoS 0 — at most once: один пакет PUBLISH, никаких подтверждений. Потерялся — потерялся, ни ретрансляций, ни уведомлений. QoS 1 — at least once: отправитель держит сообщение у себя, пока не получит PUBACK с тем же идентификатором пакета; не дождался — шлёт заново с флагом DUP. Отсюда свойства уровня: потеря невозможна, дубликат возможен — и это не дефект, а цена гарантии.
QoS 2 — exactly once: четырёхтактный обмен PUBLISH, PUBREC, PUBREL, PUBCOMP. Две пары запрос-подтверждение страхуют от двух бед сразу: первая фиксирует приём, вторая разрешает отправителю забыть сообщение; между ними у брокера есть состояние принял, но не завершил, поэтому повтор того же PUBLISH не приводит ко второй доставке. Идентификатор пакета — 16-битное число, до 65536 сообщений в полёте одновременно: для дома за глаза.
Цена в пакетах: QoS 0 — один пакет на сообщение, QoS 1 — два, QoS 2 — четыре. В байтах разница копеечная: PUBACK весит четыре байта. Во времени — зависит от канала: в локальной сети круг меньше пяти миллисекунд и разницы вы не увидите; на GSM один круг — триста — семьсот миллисекунд, и QoS 2 превращает публикацию в секундную паузу, если наглухо ждать подтверждения в loop — а мы уже умеем не блокировать loop из урока про millis-публикацию. Замер на своём канале делается за минуту: напечатайте millis до и после publish с QoS 1 — разница и есть цена подтверждения.
| QoS | Обмен на одно сообщение | Если пакет потерялся | Мой выбор |
|---|---|---|---|
| 0 | PUBLISH | молча теряется навсегда | телеметрия чаще раза в 10 секунд: свежий отсчёт сам исправит пропуск |
| 1 | PUBLISH + PUBACK | ретрансляция с DUP: возможен дубликат | команды реле и конфиги: потеря недопустима, дубликат безвреден при идемпотентной команде |
| 2 | PUBLISH + PUBREC + PUBREL + PUBCOMP | двойной обмен: ни потери, ни дубликата | счётчики энергии и деньги; дома пока не встретил |
#include <PubSubClient.h> WiFiClient net;PubSubClient client(net);unsigned long lastPub = 0;int n = 0; void setup() { Serial.begin(9600); WiFi.begin("HomeNet", "12345678"); while (WiFi.status() != WL_CONNECTED) { delay(300); } client.setServer("192.168.1.50", 1883); client.connect("esp32-garage"); Serial.print("pub qos0 = "); Serial.println(client.publish("garage/temp", "21", false, 0)); Serial.print("pub qos1 = "); Serial.println(client.publish("garage/door/cmd", "check", false, 1)); Serial.print("pub qos2 = "); Serial.println(client.publish("garage/meter/kwh", "1054.7", false, 2)); Serial.println("nol na qos2: v biblioteke tolko 0 i 1"); client.subscribe("garage/cmd", 1);} void loop() { client.loop(); unsigned long now = millis(); if (now - lastPub >= 4000) { lastPub = now; n = n + 1; int t = 15 + random(0, 8); if (client.publish("garage/temp", String(t), false, 0)) { Serial.print("telemetriya qos0 t="); Serial.println(t); } if (n % 5 == 0) { client.publish("garage/meter/kwh", "1054.7", false, 1); Serial.println("schyotchik qos1: poteri net, dublicat bezvreden"); } }}PubSubClient и договорённости уровней
PubSubClient от Nick O'Leary реализует QoS 0 и 1 — PUBREC, PUBREL и PUBCOMP в библиотеке просто нет. Ловушка в том, что компилятор не остановит: publish с четвёртым аргументом 2 собирается нормально и возвращает false — библиотека молча отказывается отправлять уровень, которого не умеет. Нужен настоящий QoS 2 — берите MQTT-стек из ESP-IDF: там все три уровня. Для дома я за годы не встретил задачи, где двойной handshake окупился бы.
Второй нюанс — уровень договаривается дважды: публикатор договаривается с брокером, подписчик — отдельно, и в подписку сообщение уходит с минимумом двух договорённостей. Публикация QoS 1 при подписке QoS 0 — доставка QoS 0: брокер не станет гонять подтверждения туда, где их не ждут. Обратная комбинация — публикация QoS 0 при подписке QoS 2 — тоже ноль: повысить гарантию подпиской невозможно, уровень задаёт отправитель. Комбинаций немного, и их стоит проговорить: один и ноль — ноль, два и один — единица, два и два — двойка. Всё решает слабейшая сторона.
Практическое следствие для устойчивости: QoS 1 плюс постоянная сессия — clean session=false, что это такое разбирали в уроке про clientId — превращает брокера в накопитель: пока плата в офлайне, сообщения для неё копятся и выстреливают пачкой при reconnect. Для редких команд это спасение, для телеметрии — гора устаревших отсчётов, которую надо фильтровать по времени. Я оставляю телеметрию на QoS 0 и чистой сессии, команды — на QoS 1, а счётчик энергии, если дойдут руки, повешу на отдельного клиента с QoS 2.
Ещё одна деталь уровня 1, о которой узнаёшь на практике: подтверждение шлёт и брокер, и клиент — в обе стороны. Плате, публикующей с QoS 1, нужен PUBACK от брокера; подписчику с QoS 1 — свой PUBACK брокеру за каждый доставленный пакет. Поэтому гарантия нагружает слабый канал в обе стороны: GSM-плата, подписанная на всё подряд с QoS 1, при плохой связи утонет в ретрансляциях — я сужаю подписку масками и оставляю QoS 1 только там, где потеря реально ударит.
Про буфер и уровень: подтверждения — не единственное, что едет между платой и брокером, и буфер PubSubClient в 256 байтов должен вместить и пакет, и его служебную обвязку. На больших конфигах с QoS 1 я не раз упирался в молчаливый отказ publish: буфер не влез, библиотека вернула ноль. Диагностика та же, что и с QoS 2: печатать возвращаемое значение каждого сетевого вызова — привычка, которая стоит одной строки кода.
Мой итоговый профиль дома: телеметрия — QoS 0, потому что следующий отсчёт важнее пропавшего прошлого; команды и конфиги — QoS 1 с идемпотентными payload; QoS 2 — запас на счётчики и деньги, которых в квартире пока нет. Уровень выбирается не по важности вообще, а по цене потери конкретного сообщения — именно с этого вопроса я теперь начинаю любую новую тему в брокере.
И последнее: уровень доставки — не замена проверке связи. QoS 1 гарантирует доставку при живом соединении, но не скажет, что плата утонула в пруду вместе с роутером: индикатор онлайн строится на завещании и retained-статусе — той самой паре механизмов, о которой шла речь в уроке про retained. Гарантия доставки и факт жизни устройства — разные вопросы, и у каждого свой ответ.
Если сетевые вызовы и датчики делят один loop, следите, чтобы ожидание подтверждений не съело keepalive: расписание задач на millis мы строили в кооперативном цикле, и MQTT-вызовы там живут на равных с мигалками — без блокировок и без синхронного ожидания: пустил пакет и вернулся к датчикам, PUBACK разберёт loop.
- QoS 0 — отправил и забыл: один пакет, потеря молча; годится там, где следующий отсчёт исправит пропуск
- QoS 1 — PUBLISH плюс PUBACK: потери нет, дубликат возможен; команды делайте идемпотентными, не toggle
- QoS 2 — четыре пакета PUBREC, PUBREL, PUBCOMP: ровно один раз; цена — двойной круг, дома редкость
- PubSubClient умеет только 0 и 1: publish с QoS 2 вернёт false молча, компилятор не предупредит
- Доставка идёт с минимумом уровней публикатора и подписчика: подписка QoS 2 не повышает гарантию
- 1. Насос должен выключиться по команде. Какой минимальный QoS делает молчаливую потерю пакета невозможной?
- 2. Почему после toggle-команды с QoS 1 свет иногда возвращается в исходное состояние?
- 3. Сколько пакетов уходит на одно сообщение с QoS 2 в базовом цикле?
- 4. Что сделает PubSubClient с вызовом publish(topic, payload, false, 2)?
- 5. Публикация идёт с QoS 1, подписка оформлена на QoS 0. С каким уровнем подписчик получит сообщение?
Частые вопросы по теме урока
- Насос должен выключиться по команде. Какой минимальный QoS делает молчаливую потерю пакета невозможной?
- Уже QoS 1 исключает потерю: отправитель держит сообщение до PUBACK и шлёт заново. Дубликат сделайте безвредным: команда задаёт состояние, а не переключает. QoS 1
- Почему после toggle-команды с QoS 1 свет иногда возвращается в исходное состояние?
- At least once допускает повтор с флагом DUP. Toggle от дубликата меняет состояние дважды. Лечение — идемпотентные команды: set 1 или set 0. PUBACK потерялся, клиент шлёт заново, и дубликат инвертирует тумблер
- Сколько пакетов уходит на одно сообщение с QoS 2 в базовом цикле?
- Четырёхтактный обмен: две пары запрос-подтверждение. Ретрансляции при потерях добавляют пакеты сверху, но базовый цикл — ровно четыре. Четыре: PUBLISH, PUBREC, PUBREL, PUBCOMP
- Что сделает PubSubClient с вызовом publish(topic, payload, false, 2)?
- Библиотека поддерживает только QoS 0 и 1, а отказ молчаливый: проверяйте возвращаемое значение publish, иначе не узнаете о не отправленном сообщении. Скомпилирует и вернёт false: уровня 2 в библиотеке нет
- Публикация идёт с QoS 1, подписка оформлена на QoS 0. С каким уровнем подписчик получит сообщение?
- Две договорённости независимы, и доставка идёт с минимумом. Повысить гарантию подпиской нельзя, понизить — можно. С QoS 0: доставке достаётся минимум из двух договорённостей