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

К этому моменту у камеры есть всё необходимое, чтобы общаться с реальным сервисом в открытом интернете: TCP-сокет, TLS для его обёртки, DNS для именования узла и asyncio, позволяющий тому же скрипту выполнять другую работу, пока соединение открыто. MQTT – это первый протокол передачи, который объединяет всё это в нечто, что действительно используется в готовом продукте.

Эта страница описывает сам протокол – формат передачи, роли каждого участника и компромиссы в его устройстве – достаточно честно, чтобы входящий в комплект клиент mqtt выглядел как очевидная обёртка над уже известным, а не как шаг в неизвестность.

9.18.1. Pub/sub против request/response

HTTP – протокол, к которому большинство проектов с камерой обращаются в первую очередь, – это request/response. Клиент запрашивает у конкретного сервера конкретный ресурс; сервер отвечает. Каждый обмен происходит один-к-одному, и оба конца заранее знают адрес друг друга.

MQTT – это publish/subscribe. Клиенты подключаются к посреднику, называемому брокером. Издатель (publisher) отправляет сообщение в именованную тему (topic), не зная и не интересуясь, кто его слушает. Подписчик (subscriber) сообщает брокеру, какие темы ему нужны, и затем получает каждое сообщение, опубликованное в эти темы. Брокер выполняет рассылку: одна публикация в yard-cam/motion достигает каждого устройства, подписанного на yard-cam/motion, будь их ноль, одно или пятьдесят.

Из этой смены модели следуют три вещи:

  • Развязка. Издателям не нужно знать о существовании подписчиков. Подписчики могут появляться и исчезать незаметно для издателя. Добавление второй панели мониторинга – это одна строка кода на новой панели; камера при этом не меняется.

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

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

Одна камера публикует в тему yard-cam/motion на брокере, а две панели мониторинга в браузере и один облачный архиватор каждый получают одно и то же сообщение.

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. Каждый управляющий пакет начинается с однобайтового фиксированного заголовка, за которым следует поле Remaining Length переменной длины, затем переменный заголовок, специфичный для типа пакета, и наконец полезная нагрузка. Один и тот же внешний формат охватывает каждую команду – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT и остальные – поэтому клиент MQTT можно написать в несколько сотен строк.

Байтовая раскладка пакета MQTT PUBLISH, показывающая байт типа и флагов фиксированного заголовка, поле Remaining Length переменной длины, имя темы, необязательный идентификатор пакета и байты полезной нагрузки.

Фиксированный заголовок занимает один байт:

  • Биты 7..4 – это тип управляющего пакета. 0x3 – это PUBLISH (поэтому первый байт обычно начинается с 0x3?). 0x1 – это CONNECT, 0x2 – CONNACK, 0x8 – SUBSCRIBE, 0xC – PINGREQ, 0xE – DISCONNECT и т. д.

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

Remaining Length – это целое число переменной длины от 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 для прикладного кода.