Tutorials

MQTT in C mit Eclipse Paho C: Beispiele für synchrone und asynchrone Clients

MQTT in C mit der Eclipse-Paho-C-Bibliothek: installieren oder bauen, synchroner Publisher, asynchroner Subscriber mit Reconnect, TLS, MQTT 5 und Return Codes.

Aktualisiert · 10 Min. Lesezeit

MQTT in C mit Eclipse Paho C: Beispiele für synchrone und asynchrone Clients

Eclipse Paho C ist die Referenz-MQTT-Client-Bibliothek für C unter Linux, macOS und Windows. Sie bietet zwei APIs: MQTTClient, eine synchrone API, bei der jeder Aufruf bis zum Abschluss blockiert, und MQTTAsync, eine nicht blockierende, callback-gesteuerte API mit eingebautem automatischem Reconnect. Beide unterstützen MQTT 3.1.1 und MQTT 5, TLS über OpenSSL und WebSockets. Dieses Tutorial installiert die Bibliothek, baut einen synchronen Publisher und einen asynchronen Subscriber und ergänzt dann TLS und MQTT 5.

Für Mikrocontroller ohne Betriebssystem ist Paho C meist zu schwer; schau dir stattdessen coreMQTT unter FreeRTOS an.

Bibliothek installieren oder bauen

Pakete sind der schnellste Weg:

Aus Paketen installieren
# Debian / Ubuntu
sudo apt install libpaho-mqtt-dev

# macOS (Homebrew)
brew install libpaho-mqtt

Für das neueste Release oder volle Kontrolle über die Build-Optionen kompilierst du mit CMake:

Aus dem Quellcode mit TLS bauen
git clone https://github.com/eclipse/paho.mqtt.c.git
cd paho.mqtt.c
cmake -Bbuild -H. -DPAHO_WITH_SSL=ON -DPAHO_ENABLE_TESTING=OFF
sudo cmake --build build/ --target install
sudo ldconfig   # nur Linux

Du brauchst einen C-Compiler, CMake und für PAHO_WITH_SSL=ON das OpenSSL-Entwicklungspaket (libssl-dev). Der Build erzeugt vier Bibliotheken; wähle die passende für dein Programm:

BibliothekAPITLS
paho-mqtt3cMQTTClient (synchron)Nein
paho-mqtt3csMQTTClient (synchron)Ja
paho-mqtt3aMQTTAsyncNein
paho-mqtt3asMQTTAsyncJa

Ein synchroner Publisher

Die synchrone API liest sich von oben nach unten: erzeugen, verbinden, publishen, auf Zustellung warten, trennen.

pub.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "MQTTClient.h"

#define ADDRESS  "tcp://broker.connectmq.com:1883"
#define CLIENTID "testmqtt-c-sync-pub"
#define TOPIC    "testmqtt/c-paho/hello"
#define TIMEOUT  10000L

int main(void)
{
    MQTTClient client;
    MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
    MQTTClient_message msg = MQTTClient_message_initializer;
    MQTTClient_deliveryToken token;
    int rc;

    rc = MQTTClient_create(&client, ADDRESS, CLIENTID,
                           MQTTCLIENT_PERSISTENCE_NONE, NULL);
    if (rc != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "create fehlgeschlagen: %d\n", rc);
        return EXIT_FAILURE;
    }

    conn_opts.keepAliveInterval = 30;
    conn_opts.cleansession = 1;
    conn_opts.username = getenv("MQTT_USER");   /* NULL ist für offene Broker in Ordnung */
    conn_opts.password = getenv("MQTT_PASS");

    if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "Verbindung fehlgeschlagen: %d (%s)\n", rc, MQTTClient_strerror(rc));
        MQTTClient_destroy(&client);
        return EXIT_FAILURE;
    }

    const char *payload = "hello from Paho C";
    msg.payload = (void *)payload;
    msg.payloadlen = (int)strlen(payload);
    msg.qos = 1;
    msg.retained = 0;

    MQTTClient_publishMessage(client, TOPIC, &msg, &token);
    rc = MQTTClient_waitForCompletion(client, token, TIMEOUT);
    printf("Zustellung %s (Token %d)\n", rc == MQTTCLIENT_SUCCESS ? "bestätigt" : "fehlgeschlagen", token);

    MQTTClient_disconnect(client, 1000);
    MQTTClient_destroy(&client);
    return rc == MQTTCLIENT_SUCCESS ? EXIT_SUCCESS : EXIT_FAILURE;
}
Kompilieren und starten
gcc pub.c -o pub -lpaho-mqtt3c
./pub

Öffne vor dem Start den Online-MQTT-Client und abonniere testmqtt/c-paho/# — die Nachricht erscheint dort, sobald pub "Zustellung bestätigt" ausgibt. Mit QoS 1 kehrt MQTTClient_waitForCompletion erst nach dem PUBACK des Brokers zurück; Erfolg heißt also wirklich, dass der Broker die Nachricht hat.

Ein asynchroner Subscriber mit Auto-Reconnect

Für einen langlaufenden Prozess passt MQTTAsync besser. Nichts blockiert; du registrierst Callbacks, und der Hintergrund-Thread der Bibliothek ruft sie auf. Entscheidend ist der Connected-Callback: Er läuft nach jeder erfolgreichen Verbindung, auch nach automatischen Reconnects, und ist damit der richtige Ort zum Abonnieren.

sub_async.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "MQTTAsync.h"

#define ADDRESS  "tcp://broker.connectmq.com:1883"
#define CLIENTID "testmqtt-c-async-sub"
#define TOPIC    "testmqtt/c-paho/#"

static void on_subscribe(void *ctx, MQTTAsync_successData *resp)
{
    printf("abonniert: %s\n", TOPIC);
}

static void on_subscribe_failure(void *ctx, MQTTAsync_failureData *resp)
{
    printf("Subscribe fehlgeschlagen: %d\n", resp ? resp->code : 0);
}

/* Läuft nach dem ersten Connect und nach jedem automatischen Reconnect. */
static void on_connected(void *ctx, char *cause)
{
    MQTTAsync client = (MQTTAsync)ctx;
    MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer;
    opts.onSuccess = on_subscribe;
    opts.onFailure = on_subscribe_failure;
    opts.context = client;
    printf("verbunden (%s)\n", cause ? cause : "");
    MQTTAsync_subscribe(client, TOPIC, 1, &opts);
}

static void on_connect_failure(void *ctx, MQTTAsync_failureData *resp)
{
    printf("Verbindung fehlgeschlagen: %d %s\n", resp ? resp->code : 0,
           resp && resp->message ? resp->message : "");
}

static void on_connection_lost(void *ctx, char *cause)
{
    printf("Verbindung verloren: %s\n", cause ? cause : "unbekannt");
}

static int on_message(void *ctx, char *topic, int topic_len, MQTTAsync_message *m)
{
    printf("empfangen %s: %.*s\n", topic, m->payloadlen, (char *)m->payload);
    MQTTAsync_freeMessage(&m);
    MQTTAsync_free(topic);
    return 1;   /* 1 = Nachricht verarbeitet */
}

int main(void)
{
    MQTTAsync client;
    MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer;
    MQTTAsync_disconnectOptions disc_opts = MQTTAsync_disconnectOptions_initializer;
    int rc;

    if ((rc = MQTTAsync_create(&client, ADDRESS, CLIENTID,
                               MQTTCLIENT_PERSISTENCE_NONE, NULL)) != MQTTASYNC_SUCCESS) {
        fprintf(stderr, "create fehlgeschlagen: %d\n", rc);
        return EXIT_FAILURE;
    }
    MQTTAsync_setCallbacks(client, client, on_connection_lost, on_message, NULL);
    MQTTAsync_setConnected(client, client, on_connected);

    conn_opts.keepAliveInterval = 30;
    conn_opts.cleansession = 1;
    conn_opts.automaticReconnect = 1;
    conn_opts.minRetryInterval = 1;    /* Sekunden, verdoppelt sich bis zum Maximum */
    conn_opts.maxRetryInterval = 30;
    conn_opts.onFailure = on_connect_failure;
    conn_opts.context = client;
    conn_opts.username = getenv("MQTT_USER");
    conn_opts.password = getenv("MQTT_PASS");

    if ((rc = MQTTAsync_connect(client, &conn_opts)) != MQTTASYNC_SUCCESS) {
        fprintf(stderr, "Verbindungsstart fehlgeschlagen: %d\n", rc);
        return EXIT_FAILURE;
    }

    printf("Enter zum Beenden\n");
    getchar();

    MQTTAsync_disconnect(client, &disc_opts);
    sleep(1);
    MQTTAsync_destroy(&client);
    return EXIT_SUCCESS;
}
Kompilieren und starten
gcc sub_async.c -o sub_async -lpaho-mqtt3a
./sub_async

Starte ./pub in einem zweiten Terminal oder publishe aus dem Browser-Client, und der Subscriber gibt jede Nachricht aus. Drei Regeln halten MQTTAsync-Code gesund:

  • Gib frei, was die Bibliothek dir übergibt. Der Nachrichten-Callback muss MQTTAsync_freeMessage und MQTTAsync_free(topic) aufrufen, sonst verliert der Prozess mit jeder Nachricht Speicher.
  • Kehre schnell aus Callbacks zurück. Sie laufen im Thread der Bibliothek; lange Arbeit dort verzögert das Keep-Alive-Handling. Übergib Nachrichten an deine eigene Queue oder einen Worker.
  • Abonniere im Connected-Callback. Bei Clean Session vergisst der Broker die Subscriptions beim Trennen; dort neu zu abonnieren stellt sie nach jedem automatischen Reconnect wieder her.

Bauen mit CMake

Die installierte Bibliothek bringt ein CMake-Paket mit, größere Projekte brauchen also keine handgeschriebenen Linker-Flags:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(mqtt_c_demo C)

find_package(eclipse-paho-mqtt-c REQUIRED)

add_executable(pub pub.c)
target_link_libraries(pub eclipse-paho-mqtt-c::paho-mqtt3c)

add_executable(sub_async sub_async.c)
target_link_libraries(sub_async eclipse-paho-mqtt-c::paho-mqtt3a)

Sobald TLS dazukommt, nutzt du die Targets paho-mqtt3cs / paho-mqtt3as.

TLS und Zugangsdaten

Stelle das URI-Schema auf ssl:// und den Port auf 8883 um, fülle die SSL-Optionen aus und linke die TLS-Variante der Bibliothek:

TLS-Optionen (synchrone API)
#define ADDRESS "ssl://broker.connectmq.com:8883"

MQTTClient_SSLOptions ssl_opts = MQTTClient_SSLOptions_initializer;
ssl_opts.enableServerCertAuth = 1;
ssl_opts.verify = 1;                 /* Hostnamen im Zertifikat prüfen */
/* ssl_opts.trustStore = "ca.pem";      nur für eine private CA */
/* ssl_opts.keyStore = "client.pem";    Client-Zertifikat (Mutual TLS) */
conn_opts.ssl = &ssl_opts;
TLS-Bibliothek linken
gcc pub_tls.c -o pub_tls -lpaho-mqtt3cs

Ohne trustStore werden die Standard-Zertifikatspfade von OpenSSL genutzt, was bei Brokern mit öffentlich vertrauenswürdigen Zertifikaten funktioniert. Die asynchrone API hat dieselben Felder in MQTTAsync_SSLOptions. Schlägt der Handshake fehl, schließe zuerst die Zertifikatskette mit den Schritten aus MQTT-TLS-Verbindung testen aus.

MQTT 5 mit Paho C

MQTT 5 braucht eine Create-Option, einen v5-Initializer für die Connect-Optionen und die ...5-Varianten der Aufrufe, die Reason Codes liefern:

MQTT-5-Connect (synchrone API)
MQTTClient_createOptions create_opts = MQTTClient_createOptions_initializer;
create_opts.MQTTVersion = MQTTVERSION_5;
MQTTClient_createWithOptions(&client, ADDRESS, CLIENTID,
                             MQTTCLIENT_PERSISTENCE_NONE, NULL, &create_opts);

MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer5;
conn_opts.cleanstart = 1;
MQTTProperties props = MQTTProperties_initializer;

MQTTResponse resp = MQTTClient_connect5(client, &conn_opts, &props, NULL);
if (resp.reasonCode != MQTTREASONCODE_SUCCESS)
    fprintf(stderr, "connect: %s\n", MQTTReasonCode_toString(resp.reasonCode));
MQTTResponse_free(resp);

Publishen und Abonnieren funktionieren genauso über MQTTClient_publishMessage5 und MQTTClient_subscribe5. Jeden Reason Code findest du in der MQTT-Reason-Code-Referenz.

Return Codes, die dir begegnen

CodeBedeutungTypische Lösung
-1 (MQTTCLIENT_FAILURE)Allgemeiner Fehler, oft Host nicht erreichbar oder TLS-Handshake fehlgeschlagenHost, Port, Firewall und Zertifikate prüfen
-10 (MQTTCLIENT_SSL_NOT_SUPPORTED)ssl://-URI mit einer Bibliothek ohne TLSpaho-mqtt3cs / paho-mqtt3as linken
-14 (MQTTCLIENT_BAD_PROTOCOL)Unbekanntes URI-Schematcp://, ssl://, ws:// oder wss:// verwenden
4CONNACK: falscher Benutzername oder PasswortZugangsdaten prüfen
5CONNACK: nicht autorisiertACLs des Brokers für diesen Client prüfen

Die positiven Codes 1–5 sind die MQTT-3.1.1-CONNACK-Return-Codes des Brokers, ausführlich erklärt unter CONNACK-Reason-Codes. Gib sie mit MQTTClient_strerror aus, damit die Logs lesbar sind.

Deinen Client testen

  1. sub_async starten, dann pub, und den Roundtrip bestätigen; anschließend mit dem Online-Client auf einer Seite wiederholen.
  2. Das Netz länger als das Keep-Alive trennen und wieder verbinden: Du solltest "Verbindung verloren" sehen, danach erneut "verbunden" und "abonniert" — das automatische Reconnect bei der Arbeit. Reconnect Best Practices behandelt Backoff für ganze Flotten.
  3. MQTT_PASS auf einen falschen Wert setzen und prüfen, dass der Fehler-Callback das meldet.
  4. Unter Valgrind oder AddressSanitizer ein paar tausend Nachrichten laufen lassen, um zu beweisen, dass der Nachrichten-Callback alles freigibt.

Demo-Topic im Browser abonnieren

Paho C ist klein, portabel und seit Jahren in Produktion. Starte mit MQTTClient für Tools und Tests, wechsle für Dienste zu MQTTAsync mit automatischem Reconnect, linke die s-Bibliotheken, sobald TLS dazukommt, und gib jede empfangene Nachricht frei. Brauchst du C++ oder Rust obendrauf: Paho C++ aus MQTT in C++ und paho-mqtt für Rust kapseln beide diese Bibliothek.

Häufig gestellte Fragen

Was ist der Unterschied zwischen MQTTClient und MQTTAsync in Paho C?

MQTTClient ist die synchrone API: Aufrufe wie MQTTClient_connect blockieren bis zum Abschluss, das hält einfache Programme kurz. MQTTAsync blockiert nie; jede Operation bekommt Erfolgs- und Fehler-Callbacks, dazu kommen Funktionen wie automatisches Reconnect und Offline-Pufferung. Für langlaufende Dienste nimmst du MQTTAsync.

Welche Paho-C-Bibliothek muss ich linken?

paho-mqtt3c für die synchrone API ohne TLS, paho-mqtt3cs für synchron mit TLS, paho-mqtt3a für asynchron ohne TLS und paho-mqtt3as für asynchron mit TLS. Wer eine Bibliothek ohne TLS linkt und sich mit einer ssl://-URI verbindet, bekommt Return Code -10 (SSL nicht unterstützt).

Was bedeutet Return Code 5 von MQTTClient_connect?

Die positiven Codes 1 bis 5 sind die CONNACK-Return-Codes von MQTT 3.1.1, die der Broker sendet. 5 bedeutet nicht autorisiert, 4 falscher Benutzername oder falsches Passwort. Negative Codes stammen von der Bibliothek selbst, etwa -1 für einen allgemeinen Fehler wie einen nicht erreichbaren Host.