Die Bibliothek Paho MQTT C ist die Standardwahl, wenn Sie auf Embedded Linux einen kleinen MQTT-Client mit wenigen Abhängigkeiten brauchen. In diesem Tutorial bauen Sie Paho MQTT C mit OpenSSL, schreiben einen TLS-Publisher und einen Subscriber, cross-kompilieren für ein ARM-Board und betreiben das Ergebnis als robusten systemd-Dienst, der mit einem Cloud-Broker verbunden ist.
Warum Paho MQTT C für Embedded Linux
Eclipse Paho C ist reines C99 mit einer optionalen Abhängigkeit, OpenSSL, für TLS. Es läuft auf allem vom Raspberry Pi bis zum industriellen Gateway mit wenigen Megabyte RAM, unterstützt MQTT 3.1, 3.1.1 und 5 und ist in den meisten Distributionen und Yocto-Layern paketiert. Wenn Sie zwischen Sprachen und Bibliotheken abwägen, ordnet die Übersicht der MQTT-Client-Bibliotheken es ein; bevorzugen Sie C++, bietet Paho zusätzlich einen C++-Wrapper auf Basis dieser Bibliothek.
Die Bibliothek gibt es in vier Varianten. Wählen Sie beim Linken die richtige:
| Bibliothek | API-Stil | TLS | Typischer Einsatz |
|---|---|---|---|
paho-mqtt3c |
Synchron MQTTClient_* |
Nein | Einfache Tools in vertrauenswürdigen Netzen |
paho-mqtt3cs |
Synchron MQTTClient_* |
Ja | Die meisten Embedded-Publisher und -Subscriber |
paho-mqtt3a |
Asynchron MQTTAsync_* |
Nein | Ereignisgesteuerte Anwendungen |
paho-mqtt3as |
Asynchron MQTTAsync_* |
Ja | Ereignisgesteuerte Anwendungen mit TLS |
Dieses Tutorial verwendet paho-mqtt3cs: die synchrone API mit TLS. "Synchron" bedeutet, dass Connect- und Publish-Aufrufe blockieren, bis sie abgeschlossen sind; eingehende Nachrichten kommen trotzdem über Callbacks auf einem Hintergrund-Thread an.
Die Bibliothek installieren oder bauen
Unter Debian, Ubuntu oder Raspberry Pi OS reicht in der Regel die paketierte Version:
sudo apt update
sudo apt install -y build-essential libpaho-mqtt-dev libssl-dev ca-certificates
Für das neueste Release oder zum Cross-Kompilieren bauen Sie aus den Quellen:
git clone https://github.com/eclipse-paho/paho.mqtt.c.git
cd paho.mqtt.c
cmake -B build -DPAHO_WITH_SSL=TRUE -DPAHO_BUILD_SAMPLES=FALSE -DPAHO_BUILD_STATIC=FALSE -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
sudo cmake --install build
sudo ldconfig
Die offizielle Paho-C-Dokumentation beschreibt jede Struktur und jede Funktion, die unten verwendet wird.
Ein Paho MQTT C Publisher über TLS
Dieses Programm verbindet sich mit broker.connectmq.com auf Port 8883, prüft das Serverzertifikat gegen das CA-Bundle des Systems und publiziert alle fünf Sekunden einen Messwert. Es funktioniert anonym mit dem öffentlichen MQTT-Broker, der für Tests und Prototypen gedacht ist, und liest Zugangsdaten aus Umgebungsvariablen, sobald Sie zu einem privaten Broker wechseln.
publisher.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include "MQTTClient.h"
#define ADDRESS "ssl://broker.connectmq.com:8883"
#define CA_BUNDLE "/etc/ssl/certs/ca-certificates.crt"
#define QOS 1
#define TIMEOUT 10000L
static volatile sig_atomic_t running = 1;
static void on_signal(int sig) { (void)sig; running = 0; }
static void connection_lost(void *ctx, char *cause) {
(void)ctx;
fprintf(stderr, "connection lost: %s\n", cause ? cause : "unknown");
}
static int message_arrived(void *ctx, char *topic, int len, MQTTClient_message *msg) {
(void)ctx; (void)len;
MQTTClient_freeMessage(&msg);
MQTTClient_free(topic);
return 1;
}
static int connect_broker(MQTTClient client) {
MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
MQTTClient_SSLOptions ssl = MQTTClient_SSLOptions_initializer;
ssl.trustStore = CA_BUNDLE;
ssl.enableServerCertAuth = 1;
ssl.verify = 1; /* auch den Hostnamen prüfen */
ssl.sslVersion = MQTT_SSL_VERSION_TLS_1_2;
opts.keepAliveInterval = 30;
opts.cleansession = 1;
opts.ssl = &ssl;
opts.username = getenv("MQTT_USERNAME"); /* NULL = anonym */
opts.password = getenv("MQTT_PASSWORD");
int rc = MQTTClient_connect(client, &opts);
if (rc != MQTTCLIENT_SUCCESS)
fprintf(stderr, "connect failed, rc=%d\n", rc);
return rc;
}
int main(void) {
const char *client_id = getenv("MQTT_CLIENT_ID");
const char *prefix = getenv("MQTT_TOPIC_PREFIX");
char id_buf[64], topic[256], payload[128];
if (!client_id) {
snprintf(id_buf, sizeof id_buf, "paho-c-%d", (int)getpid());
client_id = id_buf;
}
if (!prefix) prefix = "connectmq/demo/paho-c-4821";
snprintf(topic, sizeof topic, "%s/gateway1/temperature", prefix);
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
MQTTClient client;
int rc = MQTTClient_create(&client, ADDRESS, client_id,
MQTTCLIENT_PERSISTENCE_NONE, NULL);
if (rc != MQTTCLIENT_SUCCESS) {
fprintf(stderr, "create failed, rc=%d\n", rc);
return EXIT_FAILURE;
}
MQTTClient_setCallbacks(client, NULL, connection_lost, message_arrived, NULL);
int backoff = 1;
while (running && connect_broker(client) != MQTTCLIENT_SUCCESS) {
sleep(backoff);
if (backoff < 60) backoff *= 2;
}
printf("connected as %s, publishing to %s\n", client_id, topic);
srand((unsigned)time(NULL));
while (running) {
if (!MQTTClient_isConnected(client)) {
if (connect_broker(client) != MQTTCLIENT_SUCCESS) { sleep(5); continue; }
}
double temp = 20.0 + (rand() % 100) / 10.0;
int n = snprintf(payload, sizeof payload,
"{\"ts\":%ld,\"temperature\":%.1f}", (long)time(NULL), temp);
MQTTClient_message msg = MQTTClient_message_initializer;
MQTTClient_deliveryToken token;
msg.payload = payload;
msg.payloadlen = n;
msg.qos = QOS;
msg.retained = 0;
rc = MQTTClient_publishMessage(client, topic, &msg, &token);
if (rc == MQTTCLIENT_SUCCESS)
rc = MQTTClient_waitForCompletion(client, token, TIMEOUT);
if (rc != MQTTCLIENT_SUCCESS)
fprintf(stderr, "publish failed, rc=%d\n", rc);
else
printf("sent %s\n", payload);
sleep(5);
}
MQTTClient_disconnect(client, TIMEOUT);
MQTTClient_destroy(&client);
return EXIT_SUCCESS;
}
Bauen und ausführen:
gcc -Wall -O2 -o publisher publisher.c -lpaho-mqtt3cs
./publisher
Einige Zeilen verdienen eine Erklärung. ssl.verify = 1 aktiviert die Prüfung des Hostnamens, die standardmäßig ausgeschaltet ist; ohne sie würde jedes gültige Zertifikat für einen beliebigen Host akzeptiert. Wird MQTTClient_setCallbacks vor dem Verbinden gesetzt, läuft der Client im Multithread-Modus: Die Bibliothek erledigt Keep-Alive und Empfang auf einem eigenen Thread, und connection_lost meldet Ihnen, wenn die Verbindung abreißt. Die Publish-Schleife prüft MQTTClient_isConnected und verbindet sich mit eigener Wiederholungslogik neu, weil die synchrone API nicht automatisch reconnectet.
Ein Subscriber mit Callbacks
Der Subscriber nutzt dieselbe Verbindungslogik und erledigt seine Arbeit in message_arrived. Ersetzen Sie main durch Folgendes (die Hilfsfunktionen oben bleiben erhalten):
static int on_message(void *ctx, char *topic, int len, MQTTClient_message *msg) {
(void)ctx; (void)len;
printf("%s: %.*s\n", topic, msg->payloadlen, (char *)msg->payload);
MQTTClient_freeMessage(&msg);
MQTTClient_free(topic);
return 1; /* 1 = Nachricht verarbeitet; 0 fordert eine erneute Zustellung an */
}
int main(void) {
const char *prefix = getenv("MQTT_TOPIC_PREFIX");
char filter[256];
if (!prefix) prefix = "connectmq/demo/paho-c-4821";
snprintf(filter, sizeof filter, "%s/+/temperature", prefix);
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
MQTTClient client;
MQTTClient_create(&client, ADDRESS, "paho-c-sub-4821",
MQTTCLIENT_PERSISTENCE_NONE, NULL);
MQTTClient_setCallbacks(client, NULL, connection_lost, on_message, NULL);
while (running) {
if (!MQTTClient_isConnected(client)) {
if (connect_broker(client) == MQTTCLIENT_SUCCESS) {
MQTTClient_subscribe(client, filter, QOS);
printf("subscribed to %s\n", filter);
} else {
sleep(5);
continue;
}
}
sleep(1);
}
MQTTClient_disconnect(client, TIMEOUT);
MQTTClient_destroy(&client);
return 0;
}
Sowohl die Nachricht als auch der Topic-String müssen freigegeben werden; wer eines davon vergisst, erzeugt das häufigste Speicherleck in Paho-C-Programmen. Halten Sie den Callback kurz. Ist die Verarbeitung langsam (Schreiben in den Flash, Aufruf einer HTTP-API), kopieren Sie den Payload in eine Queue und kehren Sie zurück, denn der Callback läuft auf dem Empfangs-Thread der Bibliothek.
Sie können den Publisher auch ganz ohne Code beobachten:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-4821 -t "connectmq/demo/paho-c-4821/#" -v
Cross-Kompilieren für ein ARM-Board
Für Yocto- oder Buildroot-Images gibt es fertige Rezepte für Paho C. Für einen schnellen manuellen Cross-Build verweisen Sie CMake auf Ihre Toolchain und ein cross-kompiliertes OpenSSL:
cmake -B build-arm \
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_SYSTEM_NAME=Linux \
-DPAHO_WITH_SSL=TRUE \
-DOPENSSL_ROOT_DIR=/opt/sysroot/usr \
-DPAHO_BUILD_SHARED=TRUE -DPAHO_BUILD_SAMPLES=FALSE \
-DCMAKE_INSTALL_PREFIX=/opt/paho-arm -DCMAKE_INSTALL_LIBDIR=lib
cmake --build build-arm -j"$(nproc)"
cmake --install build-arm
aarch64-linux-gnu-gcc -O2 -o publisher publisher.c \
-I/opt/paho-arm/include -L/opt/paho-arm/lib -lpaho-mqtt3cs -lssl -lcrypto -lpthread
# Binary und Shared Library auf das Board kopieren
scp publisher /opt/paho-arm/lib/libpaho-mqtt3cs.so* root@gateway:/usr/local/lib/
Verschieben Sie publisher auf dem Board nach /usr/local/bin und führen Sie ldconfig aus. Paho lässt sich auch als statische Bibliothek bauen (-DPAHO_BUILD_STATIC=TRUE), um keine separate .so ausliefern zu müssen, dann sind Sie aber selbst für OpenSSL-Updates dieses Binaries verantwortlich. Prüfen Sie, ob das Ziel-Image ein CA-Bundle enthält; minimale Images haben oft keines, und TLS scheitert dann mit einem Zertifikatsprüfungsfehler, obwohl der Code korrekt ist. Steuert dasselbe Gateway zusätzlich einen lokalen Touchscreen, zeigt das Qt MQTT Client Tutorial, wie Sie die HMI-Seite in C++ bauen, und MQTT-Clients testen behandelt, wie Sie diesen Code in die CI bringen.
QoS, Persistenz und Offline-Pufferung
Gateways verlieren Verbindungen, also legen Sie vorab fest, was während eines Ausfalls mit den Daten passiert. Der Client oben verwendet MQTTCLIENT_PERSISTENCE_NONE und hält laufende QoS-1-Nachrichten nur im Speicher; startet der Prozess mitten in der Zustellung neu, sind diese Nachrichten verloren. Paho kann laufende Nachrichten stattdessen auf der Festplatte persistieren: Übergeben Sie MQTTCLIENT_PERSISTENCE_DEFAULT und einen Verzeichnispfad als letzte beiden Argumente von MQTTClient_create, verbinden Sie sich mit cleansession = 0, und die Bibliothek sendet nach einem Reconnect alles erneut, was noch nicht bestätigt wurde. Auf Flash-Speicher sollten Sie das gegen den Verschleiß durch Schreibvorgänge abwägen, denn wer jede Nachricht auf einer billigen SD-Karte persistiert, verkürzt deren Lebensdauer.
Persistenz deckt nur Nachrichten ab, die bereits an die Bibliothek übergeben wurden. Messwerte, die während eines Verbindungsausfalls entstehen, erreichen im Beispiel nie MQTTClient_publishMessage, weil die Schleife das Publizieren bis zum Reconnect überspringt. Brauchen Sie jeden Messwert, schreiben Sie ihn in einen lokalen Ringpuffer (eine Datei oder eine SQLite-Datenbank) und leeren Sie ihn nach dem Reconnect, die ältesten zuerst, mit dem ursprünglichen Zeitstempel im Payload, damit das Backend jeden Wert korrekt einordnen kann.
Die Wahl des QoS richtet sich nach den Daten. Periodische Telemetrie, die alle paar Sekunden ersetzt wird, ist mit QoS 0 oder 1 gut bedient; Befehle und Alarme verdienen QoS 1. QoS 2 fügt einen zusätzlichen Round Trip hinzu und wird auf Gateways selten gebraucht. Der kostenlose ConnectMQ Serverless-Plan unterstützt QoS bis 1, kostenpflichtige Pläne ergänzen QoS 2. Der Leitfaden zu MQTT QoS-Stufen erklärt die Zustellgarantien im Detail.
Als systemd-Dienst betreiben
/etc/systemd/system/mqtt-publisher.service:
[Unit]
Description=Paho MQTT C publisher
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/publisher
Environment=MQTT_CLIENT_ID=YOUR_CLIENT_ID
Environment=MQTT_USERNAME=YOUR_USERNAME
Environment=MQTT_PASSWORD=YOUR_PASSWORD
Environment=MQTT_TOPIC_PREFIX=acme/plant1/gateway1
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-publisher
journalctl -u mqtt-publisher -f
Die Zugangsdaten oben stammen von Ihrem privaten Broker. Bei einem verwalteten MQTT-Broker wie ConnectMQ erstellen Sie den Broker im Portal und erhalten einen generierten Benutzernamen, ein Passwort und eine Client ID; Client IDs sind an diese Zugangsdaten gebunden, geben Sie also jedem Gateway einen eigenen Satz. Sobald es um mehr als eine Handvoll Geräte geht, verschieben Sie Secrets in ein EnvironmentFile, das nur root lesen kann. Läuft das Gateway in einer Fabrik, lohnt es sich, MQTT_TOPIC_PREFIX an der ISA-95-Hierarchie eines Unified Namespace (UNS) auszurichten, etwa acme/werk-stuttgart/montage/linie-1, damit MES, Historian und Dashboards die Werte ohne Zuordnungstabelle finden.
Rückgabecodes und Fehlerbehebung
| Rückgabecode / Symptom | Bedeutung | Was zu prüfen ist |
|---|---|---|
-1 (MQTTCLIENT_FAILURE) beim Verbinden mit TLS |
Handshake- oder Zertifikatsfehler | Pfad zum CA-Bundle, Systemuhr (TLS scheitert bei falschem Datum), Hostname |
4 |
Falscher Benutzername oder falsches Passwort | Zugangsdaten, abschließender Zeilenumbruch in der Env-Datei |
5 |
Nicht autorisiert | Client ID passt nicht zu den ausgegebenen Zugangsdaten |
2 |
Identifier abgelehnt | Leere oder doppelte Client ID |
| Verbindung hängt auf 8883 | Firewall blockiert den Port | Mit openssl s_client -connect broker.connectmq.com:8883 testen |
| Speicher wächst über Tage | Nachrichten oder Topics nicht freigegeben | MQTTClient_freeMessage und MQTTClient_free in jedem Callback-Pfad |
Das Uhrenproblem verdient bei Embedded Linux besondere Betonung: Boards ohne Echtzeituhr booten im Jahr 1970, und jedes Zertifikat wirkt "noch nicht gültig", bis NTP synchronisiert hat. Ordnen Sie Ihren Dienst nach time-sync.target ein, wenn Sie das trifft. Weitere TLS-Fehlermuster behandelt der Artikel MQTT TLS Handshake fehlgeschlagen.
Starten Sie mit ConnectMQ
Ihr C-Gateway ist bereit für einen echten Broker. Erstellen Sie unter app.connectmq.com einen kostenlosen Serverless Broker, ganz ohne Kreditkarte, tragen Sie die generierten Zugangsdaten in die systemd-Unit ein und beobachten Sie im Live-Topic-Explorer des Portals, wie Nachrichten ankommen. Wächst die Flotte, verbindet sich dasselbe Binary ohne Codeänderungen mit kostenpflichtigen, Dedicated- oder BYOC-Brokern.
Häufig gestellte Fragen
Gegen welche Paho-C-Bibliothek sollte ich für TLS linken?
Linken Sie paho-mqtt3cs für die synchrone API mit TLS oder paho-mqtt3as für die asynchrone API mit TLS. Die Varianten ohne das abschließende s haben keine SSL-Unterstützung.
Verbindet sich der Paho MQTT C Client automatisch neu?
Die synchrone API MQTTClient tut das nicht, prüfen Sie also MQTTClient_isConnected und verbinden Sie sich selbst neu. Die asynchrone API MQTTAsync bietet die Option automaticReconnect.
Unterstützt Paho C MQTT 5?
Ja. Erstellen Sie den Client mit MQTTClient_createWithOptions und MQTTVERSION_5 und verwenden Sie dann die Funktionsfamilie MQTTClient_connect5 und MQTTClient_publish5; ConnectMQ akzeptiert sowohl MQTT 3.1.1 als auch 5.0.
Wie viel Speicher braucht Paho MQTT C?
Die Bibliothek ist klein genug für typische Embedded-Linux-Gateways, der genaue Speicherbedarf hängt aber von Ihren Build-Optionen und von OpenSSL ab. Messen Sie auf Ihrem Zielsystem mit der Variante, die Sie tatsächlich linken.



