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.