MQTT – eine Einführung

Wer Daten von A nach B übermitteln möchte, hat unzählige Möglichkeiten dies zu tun. Je nach Anforderung und Anwendungsfall sieht die ideale Möglichkeit der Datenübermittlung anders aus. Für die Kommunikation zwischen Sensoren und im IoT-Bereich hat das Protokoll MQTT den Standard gesetzt. Immer dort wo verteilte Systeme miteinander kommunizieren müssen, eignet sich MQTT. MQTT steht für Message Queue Telemetry Transport und ist architektonisch relativ einfach aufgebaut. MQTT ist Nachrichten-orientiert und zentralisiert.

Die MQTT-Architektur

Für MQTT wird ein Broker benötigt, die zentrale Instanz an welche alle Clients ihre Nachrichten senden und sie von diesem Broker empfangen. Im Umkehrschluss bedeutet dies, das sich die Clients untereinander nicht kennen. Die gesamte Kommunikation läuft über dem Broker ab. Die Nachrichten werden nicht einfach wahllos an den Broker geschickt, sondern an ein sogenanntes Topic, ein Thema z.B. bad/lichtsensor1. Diese Topics sind hierarchisch aufgebaut und werden wie ein Pfad, mit einem Slash als Trennzeichen, definiert. Die Topics können von anderen Clients abonniert werden, so das diese bei einer neuen Nachricht betreffend des Topics informiert werden. Durch den hierarchischen Aufbau ist es möglich alle Topics einer bestimmten Hierarchieebene zu abonnieren. So würde der Topic:

bad/#

alle Untertopics von bad abonnieren. Eine weitere Möglichkeit ist es an einer bestimmten Stelle im Pfad das Sonderzeichen Plus zu benutzen:

+/lautsprecher/

In diesem Beispiel würden alle Lautsprecher aller Zimmer abonniert, so wären bad/lautsprecher als auch wohnzimmer/lautsprecher abgedeckt.

Nach der Verbindung mit einer CONNECT-Nachricht antwortet der Broker mit einer CONNACK-Nachricht. Damit ist die Verbindung etabliert. Der Client abonniert in dem Beispiel (siehe Bild) den Topic bad/lichtsensor1. Nun kann der Broker den Client über Nachrichten dieses Topic betreffend informieren. Wenn eine solche Nachricht eingeht, reagiert der Client, indem er auf dem Topic bad/lautsprecher die Nachricht bzw. den Wert on hinterlässt. Soll die Verbindung später wieder beendet werden, so wird eine DISCONNECT-Nachricht gesendet.

Die Kommunikation zwischen Client und Broker

Wenn ein Client keine aktive Verbindung zum Broker hat und eine neue Nachricht auf einem Topic aufläuft, auf welches der Client ein Abonnement hält, so verpasst er diese Nachricht. Anders sieht es aus, wenn der Sender beim Senden der Nachricht das sogenannte Retain-Flag für die Nachricht gesetzt hat. In diesem Fall stellt der Broker die Nachricht später noch zu.

Für den Fall, das die Verbindung vom Client nicht beendet wird bzw. beendet werden kann, existiert in MQTT ein sogenanntes Testament. So kann es bei Sensoren, die über Batterien gespeist sind durchaus vorkommen, dass die Verbindung plötzlich abbricht. Deshalb kann ein Client ein Testament, eine Nachricht auf ein Topic, hinterlegen, welche im Falle des Verbindungsabbruches vom Broker über das Topic versendet wird.

In der Praxis existieren zwei Versionen von MQTT: MQTT 3 welches die größte Basis besitzt und MQTT 5, welches mit einigen Neuerungen aufwarten kann, um das Protokoll fit für die Zukunft zu machen. Spezifiziert wird MQTT von OASIS, der Organization for the Advancement of Structured Information Standards. Die größten Unterschiede zwischen der letzten 3er-Version 3.1.1 und 5 sind Shared Subscriptions, welche Load Balancing auf Clientseite erlauben, Negative Acks eine Art Statuscodes ähnlich den HTTP-Statuscodes und User Properties, bei denen es sich um die Entsprechung zu den HTTP-Headern handelt. Diese können unter anderem für Metadaten genutzt werden. Ergeben haben sich diese Neuerungen hauptsächlich durch die Rückmeldungen aus der Community über die letzten Jahre.

Das OSI-Modell

Technisch basiert das MQTT-Protokoll auf TCP/IP. Dabei werde die Ports 1883 und 8883 genutzt. Der erste Port ist für die unverschlüsselte, der zweite Port für die verschlüsselte Kommunikation reserviert. Im OSI-Schichtenmodell befindet sich MQTT, wie HTTP auf dem Application Layer (OSI Layer 7). Im Gegensatz zu HTTP ist MQTT bleibt die Verbindung bei MQTT auch bestehen, wenn keine Daten übertragen werden.

Gestartet wird die Kommunikation des Clients mit einer CONNECT-Nachricht, woraufhin der Broker das Ganze mit einer CONNACK-Nachricht bestätigt. Nun kann der Client Topics abonnieren und Informationen zu einem Topic senden (PUBLISH).

MQTT beherrscht drei verschiedene Stufen des Quality of Service (QoS). Stufe 0 ist vom Modell her Fire-and-Forgot; die Nachricht wird einmal versendet und danach vom Broker vergessen. Ob sie ankommt, ist auf dieser QoS-Stufe nicht relevant. Bei Stufe 1 garantiert der Broker das die Nachricht mindestens einmal zugestellt wird, sie kann aber durchaus auch mehrfach bei den Clients ankommen. Stufe 2 hingegen garantiert, dass die Nachricht exakt einmal ankommt. Offiziell sind die QoS-Stufen wie folgt benannt:

At most once (0)
At least once (1)
Exactly once (2)

Je nach gewählter QoS-Stufe kommt es zu vermehrter Kommunikation über das MQTT-Protokoll. Bei Stufe 1 würde die Gegenstelle nach einer PUBLISH-Nachricht mit einer PUBACK-Nachricht antworten. Ohne eine solche Nachricht wird bei Stufe 1 der Sendevorgang so lange wiederholt, bis er von der Gegenseite bestätigt wurde. Deshalb ist nicht sichergestellt das die Nachricht nur einmal ankommt.

Wenn dies nicht gewünscht ist, kann stattdessen die QoS-Stufe 2 genutzt werden. Hier wird in der PUBLISH-Nachricht vom Sender eine ID mitgegeben. Nach dem Empfang wird die Nachricht gespeichert, aber noch nicht verarbeitet. Der Empfänger sendet eine PUBREC-Nachricht mit der ID an den Sender. Erhält der Sender diese Nachricht, sendet er eine Release-Nachricht (PUBREL), an den Empfänger. Als letzte Aktion sendet Empfänger ein PUBCOMP-Nachricht an den Sender und verarbeitet anschließend die Nachricht. Der Sender löscht beim Empfang der PUBCOMP-Nachricht die Nachricht.

Hintergrund für die unterschiedlichen Qualitätsstufen ist der Ressourcenverbrauch; je niedriger die QoS-Stufe um so weniger Ressourcen wie Zeit, Speicher und CPU, werden benötigt, um die Nachricht zu verarbeiten.

An Brokern und Client-Bibliotheken mangelt es MQTT nicht. Zu den bekanntesten Brokern gehören Mosquitto, HiveMQ und VerneMQ. Im Produktivbetrieb muss auf eine Absicherung der Topics geachtet werden. So ist es je nach Broker möglich das diese eine Authentifizierung der Clients verlangen und erst dann den Zugriff auf die Topics erlauben. Client-Bibliotheken für MQTT gibt es wie Sand am Meer, für unterschiedlichste Systeme wie Arduino, C, Java, .NET, Go und viele weitere Sprachen und Frameworks.

MQTTBox unter macOS

Daneben existieren grafische Client, welche die Nachrichten auf dem Broker anzeigen und die Interaktion mit einem Broker ermöglichen. Zu diesen Clients gehören unter anderem MQTT.fx, mqtt-spy und MQTTBox.

Maximale URL-Länge bei HTTP

Der Uniform Resource Locator, oder in der Kurzform die URL, wird sicherlich jedem schon begegnet sein. Die URL setzt sich aus einem Schema und einem für das Schema spezifischen Part zusammen. Getrennt werden die beiden Bestandteile durch einen Doppelpunkt. Bei HTTP wäre die Angabe des Schemas (http) und dem anschließenden spezifischen Teil mit dem Host, eventuellen Informationen über den Port, Kennwörter und ähnliches und anschließend der Pfad zur gewünschten Ressource. Interessant ist allerdings die Frage, wie lang darf eine solche URL sein? Kurze URLs wie:

http://api.example.com

machen keinerlei Probleme. In der RFC 2616 finden sich erste Anhaltspunkte dazu. Die RFC trägt den Namen Hypertext Transfer Protocol — HTTP/1.1 und definiert eben dieses Protokoll. Im Abschnitt 3.2, der sich um Uniform Resource Identifiers dreht, findet sich im Unterpunkt 3.2.1 folgender Abschnitt:

The HTTP protocol does not place any a priori limit on the length of a URI. Servers MUST be able to handle the URI of any resource they serve, and SHOULD be able to handle URIs of unbounded length if they provide GET-based forms that could generate such URIs. A server SHOULD return 414 (Request-URI Too Long) status if a URI is longer than the server can handle (see section 10.4.15).

Aus diesem Abschnitt ergibt sich das URLs so lang wie benötigt sein dürfen und das HTTP-Protokoll keinerlei Beschränkung vornimmt. Sollte der Server eine lange URL nicht verarbeiten können, so soll dieser mit dem Statuscode 414 (URI Too Long) antworten. Gleich danach finden sich ein weiterer Hinweis in der RFC:

Note: Servers ought to be cautious about depending on URI lengths above 255 bytes, because some older client or proxy implementations might not properly support these lengths.

Dort wird darauf hingewiesen das es Probleme geben kann, wenn die URL länger als 255 Byte ist. Je nach Kodierung und Sprache (wie z.B. Deutsch oder Japanisch), lassen sich eine unterschiedliche Menge an Informationen in diesen 255 Byte kodieren. Allerdings wurde die RFC 2616 durch die RFC 7230 für obsolet erklärt. In dieser RFC findet sich folgender Absatz:

Various ad hoc limitations on request-line length are found in practice. It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets.

Mit diesem Absatz wird definiert das HTTP-Clients und Server eine Mindestlänge von 8000 Byte unterstützen sollen. In der Praxis stellt sich nun die Frage was wirklich funktioniert. Es existieren durchaus längere URLs, z.B. sogenannte Data-URLs, allerdings sind dies keine HTTP-URLs, sondern definierten ein eigenes Schema. Daneben existieren alte Untersuchungen zu dem Thema. Unabhängig von der theoretischen Machbarkeit existieren weitere Kriterien, wie die maximale Länge die von Suchmaschinen indiziert werden. Hier schwanken die Zahlen zwischen knapp 1800 Byte bis zu 2048 Byte.

Damit lässt sich die Frage nach der maximalen URL-Länge nicht pauschal beantworten. Mit aktueller Software auf Server- und Client-Seite, sollten 1024 Byte für die URL kein Problem sein. Längere URLs sollten mit Vorsicht genossen werden und ihre Sinnhaftigkeit noch einmal hinterfragt werden.

Wiki für das Minecraft-Protokoll

Vor einigen Tagen schrieb ich über eine Java-Bibliothek für das Minecraft-Protokoll. Dieses definiert wie der Server mit dem Client und vice versa kommuniziert. Soll mithilfe der Bibliothek ein Client für den Server implementiert werden, so werden wesentlich mehr Informationen benötigt, als die Implementierung der Bibliothek hergibt.

Zur Entwicklung eines Clients werden viele Informationen benötigt

Diese Informationen liefert die wiki.vg. In dieser Wiki sind Informationen rund um das Protokoll und die Implementierung zu finden. Dabei wird nicht nur die Java-Version, sondern auch die Bedrock-Version beleuchtet. Die Inhalte der Wiki sind unter der Creative Commons-Lizenz BY-SA lizenziert. Zu finden ist die Wiki unter wiki.vg.

Java-Bibliothek für das Minecraft-Protokoll

Wenn sich der Minecraft-Client mit dem entsprechenden Server verbindet, so kommunizieren diese über ein festgelegtes Protokoll. Mit einer eigenen Implementation dieses Protokolls ist es möglich sich mit einem Minecraft-Server zu verbinden und entsprechende Aktionen durchzuführen. Zum Beispiel könnte diese Möglichkeit genutzt werden um einen Bot für Minecraft zu schreiben. Eine solche Implementation des Minecraft-Protokolls ist die Java-Bibliothek MCProtocolLib von Steven Smith.

Central City auf meinem eigenen Minecraft-Server

Ein minimales Beispiel, für den Login auf dem Server (basierend auf einem Unit-Test der Bibliothek), mit besagter Bibliothek könnte dabei wie folgt aussehen:

public class SimpleBot {

    private static final String HOST = "example.org";
    private static final int PORT = 25565;
    private static final Proxy PROXY = Proxy.NO_PROXY;
    private static final Proxy AUTH_PROXY = Proxy.NO_PROXY;
    private static final String USERNAME = "user@example.org;
    private static final String PASSWORD = "password";

    public static void main(String[] args) throws FileNotFoundException, RequestException {

        MinecraftProtocol protocol = new MinecraftProtocol(USERNAME, PASSWORD);
        Client client = new Client(HOST, PORT, protocol, new TcpSessionFactory(PROXY));
        client.getSession().setFlag(MinecraftConstants.AUTH_PROXY_KEY, AUTH_PROXY);

        client.getSession().addListener(new SessionAdapter() {
            @Override
            public void packetReceived(PacketReceivedEvent event) {
                if(event.getPacket() instanceof ServerJoinGamePacket) {
                    event.getSession().send(new ClientChatPacket("Hello, World!"));
                } else if(event.getPacket() instanceof ServerChatPacket) {
                    Message message = event.getPacket().getMessage();
                    System.out.println("Received Message: " + message.getFullText());
                    if(message instanceof TranslationMessage) {
                        System.out.println("Received Translation Components: " + Arrays.toString(((TranslationMessage) message).getTranslationParams()));
                    }

                    event.getSession().disconnect("Finished");
                }
            }

            @Override
            public void disconnected(DisconnectedEvent event) {
                System.out.println("Disconnected: " + Message.fromString(event.getReason()).getFullText());
                if(event.getCause() != null) {
                    event.getCause().printStackTrace();
                }
            }
        });

        client.getSession().connect();
    }
}

In diesem Beispiel wird sich mit dem Server verbunden und nach erfolgreicher Verbindung eine Chatnachricht gesendet. Danach loggt sich der Bot wieder aus. Der Quelltext der Bibliothek ist auf GitHub zu finden. Das Projekt ist unter der MIT-Lizenz lizenziert und damit freie Software.

Das Icecast-Protokoll

Bei Icecast handelt es sich um einen freien Streamingserver. Der Server nimmt ein Signal entgegen und streamt es über einen Mountpoint, mit welchem sich die Nutzer des Servers verbinden um den Stream zu hören. Leider ist das Icecast-Protokoll nicht wirklich gut dokumentiert. Stattdessen wird darauf verwiesen, das der Quellcode verfügbar ist. Mit diesem Artikel soll diesem Problem ein wenig Abhilfe geschaffen werden. Das Icecast-Protokoll ist streng genommen einfaches HTTP. Wenn ein Eingangssignal zu einem Icecast-Server gestreamt sendet die Streamingquelle einen HTTP-Request (am Beispiel von butt):

PUT /stream123 HTTP/1.1
Authorization: Basic c291cmNlOmhhY2ttZQ==
User-Agent: butt 0.1.14
Content-Type: audio/mpeg
ice-name: no name
ice-public: 0
ice-audio-info: ice-bitrate=128;ice-channels=2;ice-samplerate=44100

Bei Mixxx würde dieser Request so aussehen:

SOURCE /stream123 HTTP/1.0
Authorization: Basic c291cmNlOmhhY2ttZQ==
User-Agent: libshout/2.0.0
Content-Type: application/ogg
ice-name: 
ice-public: 0
ice-url: http://www.mixxx.org
ice-genre: Live Mix
ice-audio-info: bitrate=128
ice-description:

Den SOURCE-Request den Mixxx hier anwendet ist seit der Version 2.4 des Icecast-Server veraltet und sollte nicht mehr genutzt werden. Stattdessen soll der PUT-Request verwendet werden. Wie man hier sehen kann erfolgt die Authentifizierung über das Basic Authentication Scheme (auch als HTTP Basic Authentication bekannt). In dem Request sind eine Reihe von ice-* Parametern zu finden:

ice-audio-info
Der Parameter ice-audio-info übermittelt eine Schlüssel-Wert-Liste von Audioinformationen. Die Parameter werden dabei durch ein Semikolon getrennt, das ganze könnte z.B. so aussehen:

channels=2;samplerate=48000;

Die Parameter müssen dabei mit der URL-Kodierung kodiert werden.

ice-bitrate
Der Parameter ice-bitrate setzt die Bitrate des Streams.

ice-description
Ist die Beschreibung des Streams nicht mit dem Tag stream-description definiert, so kann die Beschreibung über den Parameter ice-description eingestellt werden.

ice-genre
Ist das Genre des Streams nicht mit dem Tag genre definiert, so kann dies über den Parameter ice-genre eingestellt werden.

ice-name
Ist der Streamname nicht mit dem Tag stream-name definiert, so kann dies über den Parameter ice-name eingestellt werden.

ice-public
In der Konfiguration gibt es das Attribut public, welches angibt ob der Stream in einem öffentlichen Verzeichnis auftauchen soll. Ist der Public-Tag für den Mountpoint nicht definiert, kann dies über den Parameter ice-public gesetzt werden.

ice-url
Ist die URL des Streams nicht mit dem Tag stream-url definiert, so kann dies über den Parameter ice-url eingestellt werden.

Daneben gibt es noch den Content-Type welcher die Art des Streams definiert. Auf diesen Request antwortet der Icecast-Server (wenn die Authentifizierung korrekt ist) mit einem:

HTTP/1.0 200 OK

Statt dem Statuscode 200 sind noch eine Reihe weiteres Codes als Antwort möglich:

401 You need to authenticate
Die Authentifizierungsdaten sind nicht korrekt.

403 Content-type not supported
Streamformat wird von Icecast nicht unterstützt.

403 No Content-type given
Der Quellclient sendete kein Content-Type mit, dies wird allerdings zwingend benötigt.

403 internal format allocation problem
Internes Problem von Icecast mit dem gewählten Formatplugin.

403 too many sources connected
Das in der Konfiguration definierte Limit an gleichzeitig verbundenen Clients wurde überschritten.

403 Mountpoint in use
Der gewählte Mountpoint ist bereits in Benutzung.

500 Internal Server Error
Ein interner Fehler im Icecastserver, auf Clientseite gibt es für diesen Fehler keine Lösung.

Eine Besonderheit ist der Statuscode 100 (Continue) welcher gesendet wird, wenn der Client einen Request mit folgendem Inhalt gesendet hat:

Request: 100-continue

Mit dem Statuscode bestätigt der Server das der Client weiter Daten senden kann. Danach beginnt der Quellclient mit dem häppchenweisen senden des Ogg Vorbis- oder MP3-Stream – das bedeutet das eine MP3 nicht am Stück gesendet wird, sondern in Häppchen welche das Fortschreiten des Streams widerspiegeln. Beim Empfangen eines Streams passiert effektiv das selbe. Der Client (z.B. VLC) sendet dem Server einen GET-Request:

GET /stream123 HTTP/1.1
Host: 10.63.48.119:8000
User-Agent: VLC/2.2.1 LibVLC/2.2.1
Range: bytes=0-
Connection: close
Icy-MetaData: 1

Darauf antwortet der Icecast-Server mit mit:

HTTP/1.0 200 OK
Server: Icecast 2.4.0
Date: Thu, 02 Jul 2015 10:13:50 GMT
Content-Type: audio/ogg
Cache-Control: no-cache
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Pragma: no-cache
ice-audio-info: samplerate=44%2e100;channels=1;quality=0
icy-pub:1

Anschließend sendet der Icecast-Server die Audiodaten an den Client, welche dieser wiedergibt.