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.
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.+/motionsi 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 ayard-cam/motion,yard-cam/temperature,yard-cam/temperature/sensor-3e a qualsiasi altra cosa sottoyard-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.
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 con0x3?).0x1è CONNECT,0x2CONNACK,0x8SUBSCRIBE,0xCPINGREQ,0xEDISCONNECT, 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.