9.18. MQTT, octet cu octet

În acest punct camera are toate componentele necesare pentru a comunica cu un serviciu real pe internetul deschis: un socket TCP, TLS pentru a-l încapsula, DNS pentru a numi partenerul și asyncio care permite aceluiași script să facă și alte lucruri cât timp conexiunea este deschisă. MQTT este primul protocol de comunicare care le reunește pe toate acestea într-un mod pe care un produs implementat chiar îl folosește.

Această pagină acoperă protocolul în sine – formatul de transmisie, rolurile pe care le joacă fiecare participant și compromisurile din proiectarea sa – suficient de onest încât clientul mqtt inclus să arate ca o încapsulare evidentă a ceea ce se cunoaște deja, nu ca un salt în necunoscut.

9.18.1. Pub/sub vs cerere/răspuns

HTTP – protocolul la care recurg primele cele mai multe proiecte cu camera – este cerere/răspuns. Un client cere unui anumit server o anumită resursă; serverul răspunde. Fiecare schimb este unu-la-unu, iar ambele capete cunosc dinainte adresa celuilalt.

MQTT este publicare/abonare (publish/subscribe). Clienții se conectează la o terță parte aflată la mijloc, numită broker. Un publisher trimite un mesaj către un topic denumit fără să știe sau să-i pese cine ascultă. Un subscriber îi spune brokerului ce topicuri dorește și primește ulterior fiecare mesaj publicat pe acele topicuri. Brokerul este cel care distribuie: o singură publicare pe yard-cam/motion ajunge la fiecare dispozitiv abonat la yard-cam/motion, fie că sunt zero, unu sau cincizeci dintre ele.

Din această schimbare de model decurg trei lucruri:

  • Decuplare. Publisherii nu trebuie să știe că subscriberii există. Subscriberii pot apărea și dispărea fără ca publisherul să observe. Adăugarea unui al doilea dashboard înseamnă o singură linie de cod pe noul dashboard; camera nu se schimbă.

  • Distribuție (fan-out). Brokerul se ocupă de fiecare duplicat, așa că camera trimite un singur pachet indiferent de câte dispozitive îl citesc. Acesta este cazul de utilizare pentru care a fost creat MQTT.

  • Asimetrie. Brokerul este acum o componentă de infrastructură obligatorie – fără unul, protocolul nu funcționează. Pentru proiecte casnice acesta este de obicei un broker public gratuit (test.mosquitto.org, broker.hivemq.com) sau unul mic pe care îl rulezi tu însuți.

O singură cameră care publică pe un topic yard-cam/motion al unui broker, în timp ce două dashboard-uri de browser și un arhivator în cloud primesc fiecare același mesaj.

9.18.2. Topicuri

Topicurile sunt șiruri separate prin bară oblică. Convenția este: cel mai general în stânga, cel mai specific în dreapta:

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

Două caractere de tip wildcard funcționează în abonamente (nu în publicări):

  • + se potrivește cu un singur nivel. +/motion se abonează la topicul de mișcare al fiecărei camere; yard-cam/+ se abonează la fiecare sub-topic yard-cam.

  • # se potrivește cu unul sau mai multe niveluri finale. yard-cam/# se abonează la yard-cam/motion, yard-cam/temperature, yard-cam/temperature/sensor-3 și la orice altceva aflat sub yard-cam/. Trebuie să apară la sfârșitul abonamentului.

Șirurile de topic sunt sensibile la majuscule. Conform specificației, un $ la început marchează topicuri interne ale brokerului ($SYS/...) în care publisherii nu ar trebui să scrie.

9.18.3. Formatul pachetului

MQTT rulează peste TCP. Fiecare pachet de control începe cu un antet fix de un octet, urmat de un câmp de Remaining Length de lungime variabilă, apoi de un antet variabil specific tipului de pachet și apoi de payload. Același format exterior acoperă fiecare comandă – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT și restul – motiv pentru care un client MQTT poate fi scris în câteva sute de linii.

Dispunerea octeților unui pachet PUBLISH MQTT, arătând octetul de tip și de flaguri al antetului fix, câmpul Remaining Length de lungime variabilă, numele topicului, identificatorul opțional al pachetului și octeții de payload.

Antetul fix are un octet:

  • Biții 7..4 sunt tipul pachetului de control. 0x3 este PUBLISH (deci primul octet începe de obicei cu 0x3?). 0x1 este CONNECT, 0x2 CONNACK, 0x8 SUBSCRIBE, 0xC PINGREQ, 0xE DISCONNECT etc.

  • Biții 3..0 sunt flaguri specifice tipului de pachet. Pentru PUBLISH flagurile codifică flagul de retransmitere DUP, nivelul QoS (2 biți) și flagul RETAIN.

Remaining Length este un întreg de lungime variabilă, de 1 până la 4 octeți, care numără fiecare octet de după el însuși. Bitul cel mai semnificativ al fiecărui octet este un marcator de continuare – 1 înseamnă „urmează încă un octet de lungime”, 0 înseamnă „acesta este ultimul”. O lungime sub 128 încape într-un singur octet; payload-urile mai mari folosesc mai mulți. Lungimea maximă codificată este 256 MiB.

Pentru un PUBLISH, antetul variabil este numele topicului – o lungime de 2 octeți, apoi octeții UTF-8 – urmat de un identificator de pachet de 2 octeți care există doar când QoS este 1 sau 2. Octeții rămași reprezintă payload-ul, tratat de protocol ca octeți opaci.

Un PUBLISH minimal cu QoS 0 al lui ok către a/b este:

30 07 00 03 'a' '/' 'b' 'o' 'k'
  • 30 – PUBLISH, toate flagurile zero.

  • 07 – urmează 7 octeți.

  • 00 03 – lungimea topicului 3.

  • 'a' '/' 'b' – topicul.

  • 'o' 'k' – payload-ul.

Nouă octeți pe fir, iar mesajul ajunge la fiecare subscriber al lui a/b de pe broker.

9.18.4. Niveluri QoS

Quality-of-Service controlează cât de mult se străduiesc brokerul (și clientul) să asigure livrarea. Cele trei niveluri:

QoS 0 – cel mult o dată. Trimite și uită. Pachetul PUBLISH este trimis și niciodată confirmat. Dacă TCP livrează, brokerul retransmite. Dacă conexiunea cade în timpul trimiterii, mesajul se pierde. Majoritatea telemetriei de la senzori este în regulă la QoS 0 – o singură citire de temperatură ratată într-un flux care emite la fiecare 30 de secunde nu contează.

QoS 1 – cel puțin o dată. Publisherul include un identificator de pachet și așteaptă un PUBACK. Dacă niciun PUBACK nu sosește înainte de expirarea unui timeout, publisherul retransmite cu flagul DUP setat. Brokerul poate ajunge să livreze același mesaj de două ori unui subscriber de pe același nivel; subscriberul trebuie să fie pregătit să gestioneze duplicate.

QoS 2 – exact o dată. Un handshake în patru pași (PUBREC / PUBREL / PUBCOMP) asigură că mesajul ajunge exact o dată, chiar și peste reconectări. Costisitor în dus-întorsuri și în stare a brokerului. Puține aplicații cu camera au nevoie de el.

Clientul mqtt inclus implementează QoS 0 și QoS 1; QoS 2 generează o eroare dacă îl ceri. Pentru o cameră care raportează citiri de la senzori, QoS 0 este aproape întotdeauna răspunsul corect.

9.18.5. Mesaje reținute și ultima dorință

Două funcționalități merită cunoscute deoarece schimbă ce reține brokerul despre topicul tău.

RETAIN. Dacă un PUBLISH are flagul RETAIN setat, brokerul stochează mesajul și îl retransmite fiecărui subscriber viitor în momentul în care se abonează. Așa gestionează MQTT „care este valoarea curentă?” – un senzor își publică ultima citire ca reținută, iar un dashboard care se abonează zece minute mai târziu primește totuși cea mai recentă valoare în loc să aștepte următoarea publicare. Republicarea cu același topic suprascrie valoarea reținută; publicarea unui payload gol o șterge.

Ultima dorință (last will). Când un client se conectează, poate da brokerului o „ultimă dorință și testament”: un topic, un payload, un QoS și un flag de reținere. Dacă acel client se deconectează necurat – TCP RESET, pierdere de alimentare, cădere de rețea fără pachet DISCONNECT – brokerul publică dorința în numele clientului. Subscriberii o văd ca notificare a camerei că a ieșit din rețea. Camera însăși nu trimite niciodată dorința; brokerul o face, pentru că până atunci camera a dispărut.

9.18.6. Keepalive și reconectare

CONNECT poartă un interval de keepalive în secunde. Dacă clientul a tăcut atât de mult timp, brokerul îl consideră mort. Pentru a preveni asta, clientul trimite periodic un PINGREQ (un octet: 0xC0) și primește înapoi un PINGRESP (0xD0) – cel mai mic și mai ieftin semnal de viață pe care îl poate purta protocolul. Majoritatea aplicațiilor cu camera setează keepalive la 30 sau 60 de secunde.

Dacă conexiunea TCP cade, ambele părți observă și se reconectează de la zero. Abonamentele făcute înainte de cădere se pierd, cu excepția cazului în care clientul a folosit o sesiune persistentă la conectare; pentru aplicațiile simple cu camera, modelul de reabonare-la-reconectare este mai scurt și la fel de bun.

Atât este suficient pentru a citi specificația MQTT sau pentru a scrie de la zero un client peste un socket.socket. Clientul inclus în mqtt face exact asta, plus un API rezonabil pentru codul de aplicație.