9.18. MQTT, byte per byte#

A questo punto la cam dispone di ogni componente necessario per comunicare con un servizio reale su internet aperto: un socket TCP, TLS per incapsularlo, DNS per identificare il peer e asyncio per consentire allo stesso script di svolgere altro lavoro mentre la connessione è aperta. MQTT è il primo protocollo di trasmissione che riunisce tutti questi elementi in qualcosa che un prodotto in produzione utilizza realmente.

Questa pagina tratta il protocollo in sé – il formato sul filo, i ruoli che ciascun partecipante svolge e i compromessi nel suo design – in modo abbastanza onesto da far apparire il client mqtt incluso come un’ovvia incapsulazione di ciò che è già noto, anziché un salto nel vuoto.

9.18.1. Pub/sub vs richiesta/risposta#

HTTP – il protocollo a cui la maggior parte dei progetti cam ricorre per primo – è richiesta/risposta. Un client chiede a un server specifico una risorsa specifica; il server risponde. Ogni scambio è uno-a-uno, ed entrambe le estremità conoscono in anticipo l’indirizzo dell’altra.

MQTT è publish/subscribe. I client si connettono a una terza parte intermedia chiamata broker. Un publisher invia un messaggio a un topic con nome senza sapere o curarsi di chi sta ascoltando. Un subscriber comunica al broker quali topic desidera e riceve in seguito ogni messaggio pubblicato su quei topic. Il broker è il punto di diramazione: una sola pubblicazione su yard-cam/motion raggiunge ogni dispositivo iscritto a yard-cam/motion, che siano zero, uno o cinquanta.

Da questo cambio di modello derivano tre conseguenze:

  • Disaccoppiamento. I publisher non devono sapere che esistono dei subscriber. I subscriber possono comparire e scomparire senza che il publisher se ne accorga. Aggiungere una seconda dashboard è una sola riga di codice sulla nuova dashboard; la cam non cambia.

  • Diramazione. Il broker gestisce ogni duplicato, quindi la cam invia un solo pacchetto indipendentemente da quanti dispositivi lo leggono. È il caso d’uso per cui MQTT è stato creato.

  • Asimmetria. Il broker è ora un componente infrastrutturale necessario – senza di esso, il protocollo non funziona. Per i progetti domestici si tratta di solito di un broker pubblico gratuito (test.mosquitto.org, broker.hivemq.com) o di uno piccolo gestito autonomamente.

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. Topic#

I topic sono stringhe separate da barre. La convenzione è dal più generale a sinistra al più specifico a destra:

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

Due wildcard funzionano nelle sottoscrizioni (non nelle pubblicazioni):

  • + corrisponde a un singolo livello. +/motion si iscrive al topic motion di ogni cam; yard-cam/+ si iscrive a ogni sotto-topic di yard-cam.

  • # corrisponde a uno o più livelli finali. yard-cam/# si iscrive a yard-cam/motion, yard-cam/temperature, yard-cam/temperature/sensor-3 e a qualsiasi altra cosa sotto yard-cam/. Deve comparire alla fine della sottoscrizione.

Le stringhe dei topic sono sensibili alle maiuscole. Secondo la specifica, un $ iniziale contrassegna i topic interni al broker ($SYS/...) sui quali i publisher non dovrebbero scrivere.

9.18.3. Il formato del pacchetto#

MQTT viaggia su TCP. Ogni pacchetto di controllo inizia con un header fisso di un byte seguito da un campo Remaining Length a lunghezza variabile, poi un header variabile specifico per tipo di pacchetto e infine il payload. Lo stesso formato esterno copre ogni comando – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT e gli altri – ed è per questo che un client MQTT può essere scritto in poche centinaia di righe.

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.

L’header fisso è di un byte:

  • I bit 7..4 sono il tipo di pacchetto di controllo. 0x3 è PUBLISH (quindi il primo byte di solito inizia con 0x3?). 0x1 è CONNECT, 0x2 CONNACK, 0x8 SUBSCRIBE, 0xC PINGREQ, 0xE DISCONNECT, ecc.

  • I bit 3..0 sono flag specifici per tipo di pacchetto. Per PUBLISH i flag codificano il flag di ritrasmissione DUP, il livello QoS (2 bit) e il flag RETAIN.

Il Remaining Length è un intero a lunghezza variabile da 1 a 4 byte che conta ogni byte successivo a se stesso. Il bit più alto di ciascun byte è un marcatore di continuazione – 1 significa «segue un altro byte di lunghezza», 0 significa «questo è l’ultimo». Una lunghezza inferiore a 128 sta in un byte; payload più grandi ne usano di più. La lunghezza massima codificata è 256 MiB.

Per un PUBLISH l’header variabile è il nome del topic – una lunghezza di 2 byte, poi i byte UTF-8 – seguito da un identificatore di pacchetto di 2 byte che esiste solo quando il QoS è 1 o 2. I byte rimanenti sono il payload, trattato come byte opachi dal protocollo.

Un PUBLISH minimo a QoS-0 di ok su a/b è:

30 07 00 03 'a' '/' 'b' 'o' 'k'
  • 30 – PUBLISH, tutti i flag a zero.

  • 07 – seguono 7 byte.

  • 00 03 – lunghezza del topic 3.

  • 'a' '/' 'b' – topic.

  • 'o' 'k' – payload.

Nove byte sul filo e il messaggio arriva a ogni subscriber di a/b sul broker.

9.18.4. Livelli QoS#

Il Quality-of-Service controlla quanto duramente lavorano il broker (e il client) per garantire la consegna. I tre livelli:

QoS 0 – al massimo una volta. Invia e dimentica. Il pacchetto PUBLISH viene inviato e mai confermato. Se TCP consegna, il broker inoltra. Se la connessione cade a metà invio, il messaggio è perso. La maggior parte della telemetria dei sensori va bene a QoS 0 – una singola lettura di temperatura mancata in un flusso che emette ogni 30 secondi non ha importanza.

QoS 1 – almeno una volta. Il publisher include un identificatore di pacchetto e attende un PUBACK. Se nessun PUBACK arriva prima di un timeout, il publisher ritrasmette con il flag DUP impostato. Il broker può finire per consegnare lo stesso messaggio due volte a un subscriber sullo stesso livello; il subscriber deve essere disposto a gestire i duplicati.

QoS 2 – esattamente una volta. Un handshake in quattro passaggi (PUBREC / PUBREL / PUBCOMP) garantisce che il messaggio arrivi esattamente una volta, anche tra riconnessioni. Costoso in termini di round-trip e stato del broker. Poche app cam ne hanno bisogno.

Il client mqtt incluso implementa QoS 0 e QoS 1; QoS 2 solleva un’eccezione se lo richiedi. Per una cam che riporta letture dei sensori, QoS 0 è quasi sempre la risposta giusta.

9.18.5. Messaggi conservati e last will#

Due funzionalità vale la pena conoscerle perché cambiano ciò che il broker ricorda del tuo topic.

RETAIN. Se un PUBLISH ha il flag RETAIN impostato, il broker memorizza il messaggio e lo inoltra a ogni futuro subscriber nel momento in cui si iscrive. È così che MQTT gestisce la domanda «qual è il valore attuale?» – un sensore pubblica la sua lettura più recente con il flag retain, e una dashboard che si iscrive dieci minuti dopo riceve comunque il valore più recente anziché attendere la pubblicazione successiva. Ripubblicare con lo stesso topic sovrascrive il valore conservato; pubblicare un payload vuoto lo cancella.

Last will. Quando un client si connette può fornire al broker un «last will and testament» (ultime volontà): un topic, un payload, un QoS e un flag retain. Se quel client si disconnette in modo non pulito – TCP RESET, perdita di alimentazione, caduta di rete senza pacchetto DISCONNECT – il broker pubblica il will per conto del client. I subscriber lo vedono come la notifica della cam di essere andata offline. La cam stessa non invia mai il will; lo fa il broker, perché a quel punto la cam non c’è più.

9.18.6. Keepalive e riconnessione#

CONNECT trasporta un intervallo di keepalive in secondi. Se il client è rimasto silenzioso per quel periodo, il broker lo considera morto. Per evitarlo, il client invia periodicamente un PINGREQ (un byte: 0xC0) e riceve un PINGRESP (0xD0) – il battito cardiaco più piccolo ed economico che il protocollo possa trasportare. La maggior parte delle app cam imposta il keepalive a 30 o 60 secondi.

Se la connessione TCP cade, entrambe le parti se ne accorgono e si riconnettono da zero. Le sottoscrizioni effettuate prima della caduta vengono perse a meno che il client non abbia usato una sessione persistente alla connessione; per le semplici app cam il pattern di ri-sottoscrizione alla riconnessione è più breve e altrettanto valido.

Questo è sufficiente per leggere la specifica MQTT o per implementare a mano un client su un socket.socket. Il client incluso in mqtt fa esattamente questo, più un’API sensata per il codice applicativo.