9.18. MQTT, Byte für Byte

An diesem Punkt verfügt die Kamera über alle Bausteine, die sie braucht, um mit einem echten Dienst im offenen Internet zu kommunizieren: einen TCP-Socket, TLS zu dessen Absicherung, DNS zur Benennung der Gegenstelle und asyncio, damit dasselbe Skript andere Arbeit erledigen kann, während die Verbindung offen ist. MQTT ist das erste Protokoll auf der Leitung, das all dies zu etwas zusammenführt, das ein ausgeliefertes Produkt tatsächlich verwendet.

Diese Seite behandelt das Protokoll selbst – das Format auf der Leitung, die Rollen, die jeder Teilnehmer spielt, und die Kompromisse in seinem Entwurf – ehrlich genug, dass der mitgelieferte mqtt-Client wie eine naheliegende Kapselung des bereits Bekannten aussieht statt wie ein Sprung ins Ungewisse.

9.18.1. Pub/Sub vs. Request/Response

HTTP – das Protokoll, zu dem die meisten Kameraprojekte zuerst greifen – ist Request/Response. Ein Client fragt einen bestimmten Server nach einer bestimmten Ressource; der Server antwortet. Jeder Austausch ist eins-zu-eins, und beide Enden kennen die Adresse des jeweils anderen im Voraus.

MQTT ist Publish/Subscribe. Clients verbinden sich mit einer dritten Partei in der Mitte, dem sogenannten Broker. Ein Publisher sendet eine Nachricht an ein benanntes Topic, ohne zu wissen oder sich darum zu kümmern, wer zuhört. Ein Subscriber teilt dem Broker mit, welche Topics er möchte, und empfängt anschließend jede Nachricht, die an diese Topics veröffentlicht wird. Der Broker ist der Verteiler: ein einziges Publish auf yard-cam/motion erreicht jedes Gerät, das yard-cam/motion abonniert hat – egal ob es null, eines oder fünfzig davon gibt.

Aus diesem Modellwechsel ergeben sich drei Dinge:

  • Entkopplung. Publisher müssen nicht wissen, dass Subscriber existieren. Subscriber können kommen und gehen, ohne dass der Publisher es bemerkt. Ein zweites Dashboard hinzuzufügen ist eine Zeile Code auf dem neuen Dashboard; die Kamera ändert sich nicht.

  • Verteilung (Fan-out). Der Broker kümmert sich um jede Vervielfältigung, sodass die Kamera ein einziges Paket sendet, unabhängig davon, wie viele Geräte es lesen. Das ist genau der Anwendungsfall, für den MQTT entwickelt wurde.

  • Asymmetrie. Der Broker ist nun ein notwendiger Bestandteil der Infrastruktur – ohne ihn funktioniert das Protokoll nicht. Für Heimprojekte ist dies in der Regel ein kostenloser öffentlicher Broker (test.mosquitto.org, broker.hivemq.com) oder ein kleiner, den man selbst betreibt.

Eine Kamera veröffentlicht auf einem yard-cam/motion-Topic eines Brokers, während zwei Browser-Dashboards und ein Cloud-Archivierer jeweils dieselbe Nachricht empfangen.

9.18.2. Topics

Topics sind durch Schrägstriche getrennte Zeichenketten. Die Konvention lautet: am allgemeinsten links, am spezifischsten rechts:

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

Zwei Platzhalter funktionieren in Subscriptions (nicht in Publishes):

  • + passt auf eine einzelne Ebene. +/motion abonniert das Motion-Topic jeder Kamera; yard-cam/+ abonniert jedes Unter-Topic von yard-cam.

  • # passt auf eine oder mehrere nachfolgende Ebenen. yard-cam/# abonniert yard-cam/motion, yard-cam/temperature, yard-cam/temperature/sensor-3 und alles andere unterhalb von yard-cam/. Es muss am Ende der Subscription stehen.

Topic-Zeichenketten unterscheiden Groß- und Kleinschreibung. Laut Spezifikation kennzeichnet ein vorangestelltes $ broker-interne Topics ($SYS/...), in die Publisher nicht schreiben sollten.

9.18.3. Das Paketformat

MQTT läuft über TCP. Jedes Steuerpaket beginnt mit einem ein Byte großen Fixed Header, gefolgt von einem variabel langen Remaining Length-Feld, dann einem paketartspezifischen Variable Header und schließlich der Payload. Dasselbe äußere Format deckt jeden Befehl ab – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT und den Rest – weshalb sich ein MQTT-Client in wenigen hundert Zeilen schreiben lässt.

Das Byte-Layout eines MQTT-PUBLISH-Pakets, das das Typ- und Flags-Byte des Fixed Headers, das variabel lange Remaining-Length-Feld, den Topic-Namen, den optionalen Paketbezeichner und die Payload-Bytes zeigt.

Der Fixed Header ist ein Byte:

  • Die Bits 7..4 sind der Steuerpakettyp. 0x3 ist PUBLISH (das erste Byte beginnt also meist mit 0x3?). 0x1 ist CONNECT, 0x2 CONNACK, 0x8 SUBSCRIBE, 0xC PINGREQ, 0xE DISCONNECT usw.

  • Die Bits 3..0 sind paketartspezifische Flags. Bei PUBLISH kodieren die Flags das DUP-Retransmit-Flag, die QoS-Stufe (2 Bits) und das RETAIN-Flag.

Die Remaining Length ist eine 1 bis 4 Byte große variabel lange Ganzzahl, die jedes Byte nach sich selbst zählt. Das oberste Bit jedes Bytes ist eine Fortsetzungsmarkierung – 1 bedeutet „es folgt ein weiteres Längenbyte“, 0 bedeutet „dies ist das letzte“. Eine Länge unter 128 passt in ein Byte; größere Payloads benötigen mehr. Die maximal kodierbare Länge beträgt 256 MiB.

Bei einem PUBLISH ist der Variable Header der Topic-Name – eine 2 Byte große Länge, dann UTF-8-Bytes – gefolgt von einem 2 Byte großen Paketbezeichner, der nur existiert, wenn QoS 1 oder 2 ist. Die verbleibenden Bytes sind die Payload, die vom Protokoll als undurchsichtige Bytes behandelt wird.

Ein minimales QoS-0-PUBLISH von ok an a/b ist:

30 07 00 03 'a' '/' 'b' 'o' 'k'
  • 30 – PUBLISH, alle Flags null.

  • 07 – 7 Bytes folgen.

  • 00 03 – Topic-Länge 3.

  • 'a' '/' 'b' – Topic.

  • 'o' 'k' – Payload.

Neun Bytes auf der Leitung, und die Nachricht erreicht jeden Subscriber von a/b auf dem Broker.

9.18.4. QoS-Stufen

Quality of Service steuert, wie hart der Broker (und der Client) arbeiten, um die Zustellung sicherzustellen. Die drei Stufen:

QoS 0 – höchstens einmal. Senden und vergessen. Das PUBLISH-Paket wird gesendet und nie bestätigt. Stellt TCP zu, leitet der Broker weiter. Bricht die Verbindung mitten im Senden ab, ist die Nachricht verloren. Die meisten Sensor-Telemetriedaten sind mit QoS 0 in Ordnung – eine einzelne verpasste Temperaturmessung in einem Strom, der alle 30 Sekunden sendet, spielt keine Rolle.

QoS 1 – mindestens einmal. Der Publisher fügt einen Paketbezeichner hinzu und wartet auf ein PUBACK. Trifft vor einem Timeout kein PUBACK ein, sendet der Publisher mit gesetztem DUP-Flag erneut. Der Broker stellt dieselbe Nachricht möglicherweise zweimal an einen Subscriber auf derselben Stufe zu; der Subscriber muss bereit sein, mit Duplikaten umzugehen.

QoS 2 – genau einmal. Ein vierstufiger Handshake (PUBREC / PUBREL / PUBCOMP) stellt sicher, dass die Nachricht genau einmal ankommt, selbst über Wiederverbindungen hinweg. Teuer in Bezug auf Roundtrips und Broker-Zustand. Nur wenige Kamera-Apps benötigen es.

Der mitgelieferte mqtt-Client implementiert QoS 0 und QoS 1; QoS 2 löst eine Ausnahme aus, wenn man danach fragt. Für eine Kamera, die Sensormesswerte meldet, ist QoS 0 fast immer die richtige Antwort.

9.18.5. Retained Messages und Last Will

Zwei Funktionen sind wissenswert, weil sie ändern, was sich der Broker über Ihr Topic merkt.

RETAIN. Wenn ein PUBLISH das RETAIN-Flag gesetzt hat, speichert der Broker die Nachricht und leitet sie an jeden zukünftigen Subscriber weiter, sobald dieser abonniert. So handhabt MQTT die Frage „Was ist der aktuelle Wert?“ – ein Sensor veröffentlicht seinen letzten Messwert mit Retain, und ein Dashboard, das zehn Minuten später abonniert, empfängt dennoch den aktuellsten Wert, anstatt auf das nächste Publish zu warten. Erneutes Veröffentlichen mit demselben Topic überschreibt den gespeicherten Wert; das Veröffentlichen einer leeren Payload löscht ihn.

Last Will. Wenn sich ein Client verbindet, kann er dem Broker einen „letzten Willen“ mitgeben: ein Topic, eine Payload, ein QoS und ein Retain-Flag. Trennt sich dieser Client unsauber – TCP RESET, Stromausfall, Netzwerkabbruch ohne DISCONNECT-Paket –, veröffentlicht der Broker den Willen im Namen des Clients. Subscriber sehen ihn als Benachrichtigung der Kamera, dass sie offline gegangen ist. Die Kamera selbst sendet den Willen nie; das tut der Broker, denn zu diesem Zeitpunkt ist die Kamera bereits weg.

9.18.6. Keepalive und Wiederverbindung

CONNECT trägt ein Keepalive-Intervall in Sekunden. War der Client so lange stumm, betrachtet ihn der Broker als tot. Um das zu verhindern, sendet der Client periodisch ein PINGREQ (ein Byte: 0xC0) und erhält ein PINGRESP (0xD0) zurück – der kleinste, günstigste Herzschlag, den das Protokoll tragen kann. Die meisten Kamera-Apps setzen Keepalive auf 30 oder 60 Sekunden.

Bricht die TCP-Verbindung ab, bemerken es beide Seiten und verbinden sich von Grund auf neu. Vor dem Abbruch getätigte Subscriptions gehen verloren, es sei denn, der Client hat beim Verbinden eine persistente Sitzung verwendet; für einfache Kamera-Apps ist das Muster des erneuten Abonnierens bei der Wiederverbindung kürzer und genauso gut.

Das genügt, um die MQTT-Spezifikation zu lesen oder einen Client von Hand über einen socket.socket zu bauen. Der mitgelieferte Client in mqtt tut genau das, plus eine sinnvolle API für Anwendungscode.