9.18. MQTT, bajt po bajcie

Do tego momentu kamera ma wszystkie elementy potrzebne do komunikacji z prawdziwą usługą w otwartym internecie: gniazdo TCP, TLS do jego opakowania, DNS do nazwania węzła docelowego oraz asyncio, które pozwala temu samemu skryptowi wykonywać inne zadania, gdy połączenie jest otwarte. MQTT to pierwszy protokół przewodowy, który łączy wszystkie te elementy w coś, czego rzeczywiście używają wdrożone produkty.

Ta strona opisuje sam protokół – format na przewodzie, role poszczególnych uczestników oraz kompromisy w jego projekcie – na tyle rzetelnie, że dołączony klient mqtt wygląda na oczywiste opakowanie tego, co już znamy, a nie na skok w nieznane.

9.18.1. Pub/sub kontra żądanie/odpowiedź

HTTP – protokół, po który większość projektów z kamerą sięga jako pierwszy – działa w modelu żądanie/odpowiedź. Klient prosi konkretny serwer o konkretny zasób; serwer odpowiada. Każda wymiana jest jeden-do-jednego, a obie strony znają z góry swój adres.

MQTT działa w modelu publikuj/subskrybuj (publish/subscribe). Klienci łączą się z pośrednikiem znajdującym się pomiędzy nimi, zwanym brokerem. Wydawca (publisher) wysyła wiadomość do nazwanego tematu (topic), nie wiedząc i nie dbając o to, kto słucha. Subskrybent informuje brokera, które tematy go interesują, i odtąd otrzymuje każdą wiadomość publikowaną w tych tematach. Broker odpowiada za rozsyłanie (fan-out): jedna publikacja w yard-cam/motion dociera do każdego urządzenia subskrybującego yard-cam/motion, niezależnie od tego, czy jest ich zero, jedno, czy pięćdziesiąt.

Z tej zmiany modelu wynikają trzy rzeczy:

  • Rozłączenie zależności. Wydawcy nie muszą wiedzieć, że subskrybenci istnieją. Subskrybenci mogą pojawiać się i znikać bez wiedzy wydawcy. Dodanie drugiego panelu to jedna linia kodu na nowym panelu; kamera nie zmienia się wcale.

  • Rozsyłanie (fan-out). Broker obsługuje każdy duplikat, więc kamera wysyła jeden pakiet niezależnie od tego, ile urządzeń go odczyta. To właśnie scenariusz, do którego stworzono MQTT.

  • Asymetria. Broker jest teraz wymaganym elementem infrastruktury – bez niego protokół nie działa. W projektach domowych jest to zwykle bezpłatny publiczny broker (test.mosquitto.org, broker.hivemq.com) lub mały broker uruchomiony samodzielnie.

Jedna kamera publikująca w temacie yard-cam/motion na brokerze, podczas gdy dwa przeglądarkowe panele i jeden archiwizator w chmurze otrzymują tę samą wiadomość.

9.18.2. Tematy

Tematy to ciągi rozdzielone ukośnikami. Konwencja zakłada najbardziej ogólny element po lewej, a najbardziej szczegółowy po prawej:

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

W subskrypcjach (nie w publikacjach) działają dwa symbole wieloznaczne:

  • + dopasowuje pojedynczy poziom. +/motion subskrybuje temat ruchu każdej kamery; yard-cam/+ subskrybuje każdy podtemat yard-cam.

  • # dopasowuje jeden lub więcej końcowych poziomów. yard-cam/# subskrybuje yard-cam/motion, yard-cam/temperature, yard-cam/temperature/sensor-3 oraz wszystko inne pod yard-cam/. Musi pojawić się na końcu subskrypcji.

Ciągi tematów rozróżniają wielkość liter. Zgodnie ze specyfikacją wiodący znak $ oznacza tematy wewnętrzne brokera ($SYS/...), do których wydawcy nie powinni pisać.

9.18.3. Format pakietu

MQTT działa na warstwie TCP. Każdy pakiet sterujący zaczyna się od jednobajtowego nagłówka stałego, po którym następuje pole Remaining Length o zmiennej długości, następnie nagłówek zmienny zależny od typu pakietu, a na końcu ładunek. Ten sam zewnętrzny format obejmuje każde polecenie – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT i pozostałe – dlatego klienta MQTT można napisać w kilkuset liniach.

Układ bajtów pakietu PUBLISH protokołu MQTT przedstawiający bajt typu nagłówka stałego i flag, pole Remaining Length o zmiennej długości, nazwę tematu, opcjonalny identyfikator pakietu oraz bajty ładunku.

Nagłówek stały to jeden bajt:

  • Bity 7..4 to typ pakietu sterującego. 0x3 to PUBLISH (więc pierwszy bajt zwykle zaczyna się od 0x3?). 0x1 to CONNECT, 0x2 to CONNACK, 0x8 to SUBSCRIBE, 0xC to PINGREQ, 0xE to DISCONNECT itd.

  • Bity 3..0 to flagi zależne od typu pakietu. W przypadku PUBLISH flagi kodują flagę retransmisji DUP, poziom QoS (2 bity) oraz flagę RETAIN.

Remaining Length to liczba całkowita o zmiennej długości od 1 do 4 bajtów, która zlicza każdy bajt następujący po niej. Najwyższy bit każdego bajtu jest znacznikiem kontynuacji – 1 oznacza „następuje kolejny bajt długości”, 0 oznacza „to jest ostatni”. Długość poniżej 128 mieści się w jednym bajcie; większe ładunki używają więcej. Maksymalna zakodowana długość to 256 MiB.

W przypadku PUBLISH nagłówek zmienny to nazwa tematu – 2-bajtowa długość, a następnie bajty UTF-8 – po której następuje 2-bajtowy identyfikator pakietu, który istnieje tylko wtedy, gdy QoS wynosi 1 lub 2. Pozostałe bajty to ładunek, traktowany przez protokół jako nieprzejrzyste bajty.

Minimalny PUBLISH na poziomie QoS 0 z wartością ok do a/b wygląda tak:

30 07 00 03 'a' '/' 'b' 'o' 'k'
  • 30 – PUBLISH, wszystkie flagi zerowe.

  • 07 – następuje 7 bajtów.

  • 00 03 – długość tematu 3.

  • 'a' '/' 'b' – temat.

  • 'o' 'k' – ładunek.

Dziewięć bajtów na przewodzie, a wiadomość trafia do każdego subskrybenta tematu a/b na brokerze.

9.18.4. Poziomy QoS

Jakość usługi (Quality-of-Service) określa, jak bardzo broker (i klient) starają się zapewnić dostarczenie. Trzy poziomy:

QoS 0 – co najwyżej raz. Wyślij i zapomnij. Pakiet PUBLISH jest wysyłany i nigdy nie potwierdzany. Jeśli TCP dostarczy, broker przekazuje dalej. Jeśli połączenie zerwie się w trakcie wysyłki, wiadomość przepada. Większość telemetrii z sensorów sprawdza się przy QoS 0 – pojedynczy pominięty odczyt temperatury w strumieniu emitującym co 30 sekund nie ma znaczenia.

QoS 1 – co najmniej raz. Wydawca dołącza identyfikator pakietu i czeka na PUBACK. Jeśli PUBACK nie dotrze przed upływem limitu czasu, wydawca retransmituje pakiet z ustawioną flagą DUP. Broker może w efekcie dostarczyć subskrybentowi tę samą wiadomość dwukrotnie na tym samym poziomie; subskrybent musi być gotów obsługiwać duplikaty.

QoS 2 – dokładnie raz. Czterostopniowy uścisk dłoni (PUBREC / PUBREL / PUBCOMP) gwarantuje, że wiadomość trafi dokładnie raz, nawet pomiędzy ponownymi połączeniami. Kosztowny pod względem liczby cykli komunikacji i stanu przechowywanego przez brokera. Niewiele aplikacji z kamerą go potrzebuje.

Dołączony klient mqtt implementuje QoS 0 i QoS 1; QoS 2 zgłasza wyjątek, jeśli o niego poprosisz. Dla kamery raportującej odczyty z sensorów QoS 0 jest niemal zawsze właściwym wyborem.

9.18.5. Wiadomości zachowywane (retained) i ostatnia wola (last will)

Dwie funkcje warto poznać, ponieważ zmieniają one to, co broker zapamiętuje o twoim temacie.

RETAIN. Jeśli PUBLISH ma ustawioną flagę RETAIN, broker przechowuje wiadomość i przekazuje ją każdemu przyszłemu subskrybentowi w chwili, gdy ten dokona subskrypcji. W ten sposób MQTT obsługuje pytanie „jaka jest bieżąca wartość?” – sensor publikuje swój najnowszy odczyt jako zachowany, a panel, który subskrybuje dziesięć minut później, nadal otrzymuje najnowszą wartość, zamiast czekać na kolejną publikację. Ponowna publikacja w tym samym temacie nadpisuje zachowaną wartość; opublikowanie pustego ładunku ją czyści.

Ostatnia wola. Łącząc się, klient może przekazać brokerowi „ostatnią wolę i testament”: temat, ładunek, QoS oraz flagę retain. Jeśli ten klient rozłączy się w sposób nieczysty – TCP RESET, utrata zasilania, zerwanie sieci bez pakietu DISCONNECT – broker publikuje wolę w imieniu klienta. Subskrybenci odbierają to jako powiadomienie kamery o przejściu w tryb offline. Sama kamera nigdy nie wysyła woli; robi to broker, ponieważ do tego czasu kamery już nie ma.

9.18.6. Keepalive i ponowne łączenie

CONNECT przenosi interwał keepalive w sekundach. Jeśli klient milczy przez ten czas, broker uznaje go za martwego. Aby temu zapobiec, klient okresowo wysyła PINGREQ (jeden bajt: 0xC0) i otrzymuje w odpowiedzi PINGRESP (0xD0) – najmniejsze i najtańsze tętno, jakie protokół może przenosić. Większość aplikacji z kamerą ustawia keepalive na 30 lub 60 sekund.

Jeśli połączenie TCP zostanie zerwane, obie strony to zauważają i łączą się ponownie od zera. Subskrypcje wykonane przed zerwaniem są tracone, chyba że klient użył przy łączeniu trwałej sesji; w prostych aplikacjach z kamerą wzorzec ponownej subskrypcji po ponownym połączeniu jest krótszy i równie dobry.

To wystarczy, aby przeczytać specyfikację MQTT lub samodzielnie napisać klienta na bazie socket.socket. Dołączony klient w mqtt robi dokładnie to, a do tego oferuje sensowne API dla kodu aplikacji.