9.18. MQTT, байт за байтом#

На цьому етапі камера має все необхідне для зв’язку з реальним сервісом в інтернеті: TCP-сокет, TLS для шифрування, DNS для визначення адреси вузла та asyncio, щоб той самий скрипт міг виконувати іншу роботу, поки з’єднання відкрите. MQTT — це перший мережевий протокол, який об’єднує всі ці компоненти в щось, що реально використовується у розгорнутому продукті.

Ця сторінка охоплює сам протокол – формат на рівні дротових даних, ролі кожного учасника та компроміси в його дизайні – достатньо докладно, щоб вбудований клієнт mqtt виглядав як очевидна обгортка вже відомого, а не стрибок у невідоме.

9.18.1. Pub/sub проти запит/відповідь#

HTTP – протокол, до якого більшість проєктів із камерою звертається першочергово – є запит/відповідь. Клієнт запитує у конкретного сервера конкретний ресурс; сервер відповідає. Кожен обмін є «один до одного», і обидва кінці заздалегідь знають адреси один одного.

MQTT є публікація/підписка. Клієнти підключаються до третьої сторони посередині, яка називається брокером. Видавець надсилає повідомлення в іменовану тему, не знаючи і не дбаючи про те, хто слухає. Підписник повідомляє брокеру, які теми він хоче отримувати, і отримує всі повідомлення, опубліковані в цих темах. Брокер здійснює розподіл: одна публікація в yard-cam/motion досягає кожного пристрою, підписаного на yard-cam/motion, навіть якщо їх нуль, один або п’ятдесят.

З цієї зміни моделі випливають три речі:

  • Розв’язання. Видавці не повинні знати про існування підписників. Підписники можуть приходити і йти, не помічаючись видавцем. Додавання другої панелі моніторингу — це один рядок коду на новій панелі; камера не змінюється.

  • Розгалуження. Брокер обробляє всі дублікати, тому камера надсилає один пакет незалежно від кількості пристроїв, які його читають. Саме для цього варіанту використання і був розроблений MQTT.

  • Асиметрія. Брокер є обов’язковим елементом інфраструктури – без нього протокол не працює. Для домашніх проєктів це зазвичай безкоштовний публічний брокер (test.mosquitto.org, broker.hivemq.com) або невеликий, який ви запускаєте самостійно.

One cam publishing to a yard-cam/motion topic on a broker while two browser dashboards and one cloud archiver each receive the same message.

9.18.2. Теми#

Теми — це рядки, розділені косою рискою. Угода передбачає найбільш загальне зліва, найбільш конкретне справа:

yard-cam/motion
yard-cam/temperature
workshop-cam/motion
workshop-cam/temperature/sensor-3

У підписках (але не при публікації) діють два символи підстановки:

  • + відповідає одному рівню. +/motion підписується на тему motion кожної камери; yard-cam/+ підписується на кожну підтему yard-cam.

  • # відповідає одному або декільком кінцевим рівням. yard-cam/# підписується на yard-cam/motion, yard-cam/temperature, yard-cam/temperature/sensor-3 та будь-що інше в yard-cam/. Він повинен знаходитись у кінці підписки.

Рядки тем чутливі до регістру. Відповідно до специфікації, провідний символ $ позначає внутрішні теми брокера ($SYS/...), до яких видавці не повинні записувати.

9.18.3. Формат пакету#

MQTT працює поверх TCP. Кожен керуючий пакет починається з однобайтного фіксованого заголовку, за яким слідує поле змінної довжини Залишкова довжина, потім змінний заголовок конкретного типу пакету, а потім навантаження. Однаковий зовнішній формат охоплює всі команди – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT та інші – саме тому клієнт MQTT можна написати у кількох сотнях рядків.

The byte layout of an MQTT PUBLISH packet showing the fixed-header type and flags byte, the variable-length Remaining Length field, the topic name, the optional packet identifier, and the payload bytes.

Фіксований заголовок складається з одного байта:

  • Біти 7..4 — це тип керуючого пакету. 0x3 — PUBLISH (тому перший байт зазвичай починається з 0x3?). 0x1 — CONNECT, 0x2 — CONNACK, 0x8 — SUBSCRIBE, 0xC — PINGREQ, 0xE — DISCONNECT тощо.

  • Біти 3..0 — це прапорці, специфічні для типу пакету. Для PUBLISH прапорці кодують прапорець повторної передачі DUP, рівень QoS (2 біти) та прапорець RETAIN.

Залишкова довжина — це цілочисельне значення змінної довжини від 1 до 4 байтів, яке підраховує кожен байт після себе. Старший біт кожного байта є маркером продовження – 1 означає «іде ще один байт довжини», 0 означає «це останній». Довжина до 128 вміщується в одному байті; більші навантаження використовують більше. Максимальна закодована довжина становить 256 МіБ.

Для PUBLISH змінний заголовок є іменем теми – 2-байтна довжина, потім байти UTF-8 – за яким слідує 2-байтний ідентифікатор пакету, який існує лише тоді, коли QoS дорівнює 1 або 2. Решта байтів є навантаженням, яке протокол трактує як непрозорі байти.

Мінімальний PUBLISH з QoS-0 ok в a/b виглядає так:

30 07 00 03 'a' '/' 'b' 'o' 'k'
  • 30 – PUBLISH, всі прапорці нульові.

  • 07 – далі 7 байтів.

  • 00 03 – довжина теми 3.

  • 'a' '/' 'b' – тема.

  • 'o' 'k' – навантаження.

Дев’ять байтів у мережі, і повідомлення надходить до кожного підписника a/b на брокері.

9.18.4. Рівні QoS#

Quality-of-Service визначає, наскільки старанно брокер (і клієнт) забезпечує доставку. Три рівні:

QoS 0 – не більше одного разу. Надіслати і забути. Пакет PUBLISH надсилається і ніколи не підтверджується. Якщо TCP доставив – брокер пересилає. Якщо з’єднання обривається під час відправки – повідомлення втрачається. Більшість телеметрії датчиків нормально працює з QoS 0 – одиничне пропущене значення температури в потоці, що надсилається кожні 30 секунд, не має значення.

QoS 1 – щонайменше один раз. Видавець включає ідентифікатор пакету і чекає на PUBACK. Якщо PUBACK не надходить до закінчення тайм-ауту, видавець повторно передає пакет з встановленим прапорцем DUP. Брокер може в підсумку двічі доставити те саме повідомлення підписнику на тому ж рівні; підписник повинен бути готовий обробляти дублікати.

QoS 2 – рівно один раз. Чотириетапне рукостискання (PUBREC / PUBREL / PUBCOMP) гарантує, що повідомлення потрапляє рівно один раз, навіть після перепідключення. Дорого з точки зору кількості обходів та стану брокера. Мало застосунків для камери потребують цього.

Вбудований клієнт mqtt реалізує QoS 0 та QoS 1; QoS 2 викликає виняток, якщо ви його запросите. Для камери, яка звітує про показники датчиків, QoS 0 майже завжди є правильним вибором.

9.18.5. Збережені повідомлення та останнє волевиявлення#

Дві функції варто знати, оскільки вони змінюють те, що брокер пам’ятає про вашу тему.

RETAIN. Якщо для PUBLISH встановлено прапорець RETAIN, брокер зберігає повідомлення та пересилає його кожному майбутньому підписнику в момент підписки. Саме так MQTT відповідає на питання «яке поточне значення?» – датчик публікує своє останнє значення з RETAIN, і панель моніторингу, яка підпишеться через десять хвилин, все одно отримає найсвіжіше значення, не чекаючи наступної публікації. Повторна публікація в ту саму тему перезаписує збережене значення; публікація порожнього навантаження очищає його.

Останнє волевиявлення. При підключенні клієнт може надати брокеру «останнє волевиявлення та заповіт»: тему, навантаження, QoS та прапорець RETAIN. Якщо цей клієнт відключається аварійно – TCP RESET, втрата живлення, обрив мережі без пакету DISCONNECT – брокер публікує заповіт від імені клієнта. Підписники бачать це як сповіщення камери про те, що вона перейшла в офлайн. Сама камера ніколи не надсилає заповіт; це робить брокер, тому що на той момент камера вже недоступна.

9.18.6. Keepalive та перепідключення#

CONNECT несе в собі інтервал keepalive у секундах. Якщо клієнт мовчить протягом цього часу, брокер вважає його мертвим. Щоб запобігти цьому, клієнт періодично надсилає PINGREQ (один байт: 0xC0) та отримує у відповідь PINGRESP (0xD0) – найменше та найдешевше підтвердження активності, яке може підтримувати протокол. Більшість застосунків для камери встановлюють keepalive на 30 або 60 секунд.

Якщо TCP-з’єднання обривається, обидві сторони помічають це і перепідключаються з нуля. Підписки, зроблені до обриву, втрачаються, якщо клієнт не використовував постійну сесію при підключенні; для простих застосунків для камери шаблон повторної підписки при перепідключенні є коротшим і не менш ефективним.

Цього достатньо, щоб прочитати специфікацію MQTT або написати клієнт вручну поверх socket.socket. Вбудований клієнт у mqtt робить саме це, плюс зручний API для коду застосунку.