Tutoriales

MQTT en C con Eclipse Paho C: ejemplos de cliente síncrono y asíncrono

Usa MQTT en C con la librería Eclipse Paho C: instálala o compílala, publisher síncrono, subscriber asíncrono con reconexión, TLS, MQTT 5 y códigos de retorno.

Actualizado · 10 min de lectura

MQTT en C con Eclipse Paho C: ejemplos de cliente síncrono y asíncrono

Eclipse Paho C es la librería cliente MQTT de referencia para C en Linux, macOS y Windows. Ofrece dos APIs: MQTTClient, una API síncrona en la que cada llamada bloquea hasta terminar, y MQTTAsync, una API no bloqueante basada en callbacks con reconexión automática integrada. Ambas soportan MQTT 3.1.1 y MQTT 5, TLS mediante OpenSSL y WebSockets. En este tutorial instalamos la librería, construimos un publisher síncrono y un subscriber asíncrono, y después añadimos TLS y MQTT 5.

Para microcontroladores sin sistema operativo, Paho C suele ser demasiado pesado; mira coreMQTT en FreeRTOS.

Instalar o compilar la librería

Los paquetes son la vía más rápida:

Instalar desde paquetes
# Debian / Ubuntu
sudo apt install libpaho-mqtt-dev

# macOS (Homebrew)
brew install libpaho-mqtt

Para la última versión, o para controlar las opciones de compilación, compílala con CMake:

Compilar desde el código fuente con TLS
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   # solo Linux

Necesitas un compilador de C, CMake y el paquete de desarrollo de OpenSSL (libssl-dev) para PAHO_WITH_SSL=ON. La compilación genera cuatro librerías; elige la que corresponda a tu programa:

LibreríaAPITLS
paho-mqtt3cMQTTClient (síncrona)No
paho-mqtt3csMQTTClient (síncrona)Sí
paho-mqtt3aMQTTAsyncNo
paho-mqtt3asMQTTAsyncSí

Un publisher síncrono

La API síncrona se lee de arriba abajo: crear, conectar, publicar, esperar la entrega, desconectar.

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, "fallo en create: %d\n", rc);
        return EXIT_FAILURE;
    }

    conn_opts.keepAliveInterval = 30;
    conn_opts.cleansession = 1;
    conn_opts.username = getenv("MQTT_USER");   /* NULL vale para brokers abiertos */
    conn_opts.password = getenv("MQTT_PASS");

    if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "fallo al conectar: %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("entrega %s (token %d)\n", rc == MQTTCLIENT_SUCCESS ? "confirmada" : "fallida", token);

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

Antes de ejecutarlo, abre el cliente MQTT online y suscríbete a testmqtt/c-paho/#: el mensaje aparece allí en cuanto pub imprime "entrega confirmada". Con QoS 1, MQTTClient_waitForCompletion solo vuelve tras el PUBACK del broker, así que el éxito significa de verdad que el broker tiene el mensaje.

Un subscriber asíncrono con reconexión automática

Para un proceso de larga duración, MQTTAsync encaja mejor. Nada bloquea; registras callbacks y el hilo en segundo plano de la librería los invoca. Lo importante es el callback connected: se ejecuta tras cada conexión correcta, incluidas las reconexiones automáticas, así que es el lugar adecuado para suscribirse.

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("suscrito a %s\n", TOPIC);
}

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

/* Se ejecuta tras la primera conexión y tras cada reconexión automática. */
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("conectado (%s)\n", cause ? cause : "");
    MQTTAsync_subscribe(client, TOPIC, 1, &opts);
}

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

static void on_connection_lost(void *ctx, char *cause)
{
    printf("conexión perdida: %s\n", cause ? cause : "desconocida");
}

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

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, "fallo en create: %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;    /* segundos, se duplica hasta el máximo */
    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, "no se pudo iniciar la conexión: %d\n", rc);
        return EXIT_FAILURE;
    }

    printf("pulsa Enter para salir\n");
    getchar();

    MQTTAsync_disconnect(client, &disc_opts);
    sleep(1);
    MQTTAsync_destroy(&client);
    return EXIT_SUCCESS;
}
Compilar y ejecutar
gcc sub_async.c -o sub_async -lpaho-mqtt3a
./sub_async

Ejecuta ./pub en otra terminal, o publica desde el cliente del navegador, y el subscriber imprimirá cada mensaje. Tres reglas mantienen sano el código con MQTTAsync:

  • Libera lo que te da la librería. El callback de mensajes debe llamar a MQTTAsync_freeMessage y MQTTAsync_free(topic), o el proceso perderá memoria con cada mensaje.
  • Sal rápido de los callbacks. Se ejecutan en el hilo de la librería; un trabajo largo ahí retrasa la gestión del keep-alive. Pasa los mensajes a tu propia cola o worker.
  • Suscríbete en el callback connected. Con clean session, el broker olvida las suscripciones al desconectar; volver a suscribirse ahí las restaura tras cada reconexión automática.

Compilar con CMake

La librería instalada incluye un paquete de CMake, así que los proyectos grandes no necesitan flags de enlazado escritos a mano:

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)

Usa los targets paho-mqtt3cs / paho-mqtt3as en cuanto añadas TLS.

TLS y credenciales

Cambia el esquema de la URI a ssl:// y el puerto a 8883, rellena las opciones SSL y enlaza la variante TLS de la librería:

Opciones TLS (API síncrona)
#define ADDRESS "ssl://broker.connectmq.com:8883"

MQTTClient_SSLOptions ssl_opts = MQTTClient_SSLOptions_initializer;
ssl_opts.enableServerCertAuth = 1;
ssl_opts.verify = 1;                 /* comprobar el nombre de host del certificado */
/* ssl_opts.trustStore = "ca.pem";      solo para una CA privada */
/* ssl_opts.keyStore = "client.pem";    certificado de cliente (TLS mutuo) */
conn_opts.ssl = &ssl_opts;
Enlazar la librería TLS
gcc pub_tls.c -o pub_tls -lpaho-mqtt3cs

Sin trustStore, se usan las ubicaciones de certificados por defecto de OpenSSL, lo que funciona con brokers que tienen certificados de confianza pública. La API asíncrona tiene los mismos campos en MQTTAsync_SSLOptions. Si el handshake falla, descarta primero la cadena de certificados con los pasos de probar una conexión MQTT TLS.

MQTT 5 con Paho C

MQTT 5 necesita una opción de creación, un inicializador v5 para las opciones de conexión y las variantes ...5 de las llamadas, que devuelven reason codes:

Conexión MQTT 5 (API síncrona)
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);

Publicar y suscribirse funciona igual mediante MQTTClient_publishMessage5 y MQTTClient_subscribe5. Consulta cualquier reason code en la referencia de reason codes MQTT.

Códigos de retorno que verás

CódigoSignificadoSolución típica
-1 (MQTTCLIENT_FAILURE)Fallo genérico, a menudo host inalcanzable o handshake TLS fallidoRevisa host, puerto, firewall y certificados
-10 (MQTTCLIENT_SSL_NOT_SUPPORTED)URI ssl:// con una librería sin TLSEnlaza paho-mqtt3cs / paho-mqtt3as
-14 (MQTTCLIENT_BAD_PROTOCOL)Esquema de URI desconocidoUsa tcp://, ssl://, ws:// o wss://
4CONNACK: usuario o contraseña incorrectosRevisa las credenciales
5CONNACK: no autorizadoRevisa las ACLs del broker para este cliente

Los códigos positivos 1–5 son los códigos de retorno CONNACK de MQTT 3.1.1 del broker, explicados en detalle en reason codes de CONNACK. Imprímelos con MQTTClient_strerror para que los logs sean legibles.

Probar tu cliente

  1. Ejecuta sub_async, luego pub, y confirma la ida y vuelta; repítelo con el cliente online en uno de los lados.
  2. Desconecta la red durante más tiempo que el keep-alive y vuelve a conectarla: deberías ver "conexión perdida" y luego otra vez "conectado" y "suscrito", la reconexión automática en acción. Buenas prácticas de reconexión cubre el backoff para flotas enteras.
  3. Pon un valor erróneo en MQTT_PASS y confirma que el callback de fallo lo informa.
  4. Ejecuta con Valgrind o AddressSanitizer durante unos miles de mensajes para demostrar que el callback de mensajes lo libera todo.

Suscríbete al topic de la demo en tu navegador

Paho C es pequeño, portable y lleva años en producción. Empieza con MQTTClient para herramientas y pruebas, pasa a MQTTAsync con reconexión automática para servicios, enlaza las librerías s cuando añadas TLS y libera cada mensaje que recibas. Si necesitas C++ o Rust por encima, Paho C++ en MQTT en C++ y paho-mqtt para Rust envuelven esta librería.

Preguntas frecuentes

¿Qué diferencia hay entre MQTTClient y MQTTAsync en Paho C?

MQTTClient es la API síncrona: llamadas como MQTTClient_connect bloquean hasta completarse, lo que mantiene cortos los programas sencillos. MQTTAsync nunca bloquea; cada operación recibe callbacks de éxito y fallo, y añade funciones como reconexión automática y buffer offline. Usa MQTTAsync para servicios de larga duración.

¿Qué librería de Paho C debo enlazar?

paho-mqtt3c para la API síncrona sin TLS, paho-mqtt3cs para síncrona con TLS, paho-mqtt3a para asíncrona sin TLS y paho-mqtt3as para asíncrona con TLS. Enlazar una librería sin TLS y conectar a una URI ssl:// falla con el código de retorno -10 (SSL no soportado).

¿Qué significa el código de retorno 5 de MQTTClient_connect?

Los códigos positivos del 1 al 5 son los códigos de retorno CONNACK de MQTT 3.1.1 enviados por el broker. 5 significa no autorizado y 4 usuario o contraseña incorrectos. Los códigos negativos vienen de la propia librería, por ejemplo -1 para un fallo genérico como un host inalcanzable.