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:
# Debian / Ubuntu
sudo apt install libpaho-mqtt-dev
# macOS (Homebrew)
brew install libpaho-mqttPara la última versión, o para controlar las opciones de compilación, compílala con CMake:
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 LinuxNecesitas 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ía | API | TLS |
|---|---|---|
paho-mqtt3c | MQTTClient (síncrona) | No |
paho-mqtt3cs | MQTTClient (síncrona) | Sí |
paho-mqtt3a | MQTTAsync | No |
paho-mqtt3as | MQTTAsync | Sí |
Un publisher síncrono
La API síncrona se lee de arriba abajo: crear, conectar, publicar, esperar la entrega, desconectar.
#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;
}gcc pub.c -o pub -lpaho-mqtt3c
./pubAntes 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.
#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;
}gcc sub_async.c -o sub_async -lpaho-mqtt3a
./sub_asyncEjecuta ./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_freeMessageyMQTTAsync_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:
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:
#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;gcc pub_tls.c -o pub_tls -lpaho-mqtt3csSin 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:
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ódigo | Significado | Solución típica |
|---|---|---|
-1 (MQTTCLIENT_FAILURE) | Fallo genérico, a menudo host inalcanzable o handshake TLS fallido | Revisa host, puerto, firewall y certificados |
-10 (MQTTCLIENT_SSL_NOT_SUPPORTED) | URI ssl:// con una librería sin TLS | Enlaza paho-mqtt3cs / paho-mqtt3as |
-14 (MQTTCLIENT_BAD_PROTOCOL) | Esquema de URI desconocido | Usa tcp://, ssl://, ws:// o wss:// |
4 | CONNACK: usuario o contraseña incorrectos | Revisa las credenciales |
5 | CONNACK: no autorizado | Revisa 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
- Ejecuta
sub_async, luegopub, y confirma la ida y vuelta; repítelo con el cliente online en uno de los lados. - 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.
- Pon un valor erróneo en
MQTT_PASSy confirma que el callback de fallo lo informa. - 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.


