La biblioteca Paho MQTT C es la elección estándar cuando necesitas un cliente MQTT pequeño y con pocas dependencias en Linux embebido. En este tutorial compilarás Paho MQTT C con OpenSSL, escribirás un publicador y un suscriptor con TLS, harás una compilación cruzada para una placa ARM y ejecutarás el resultado como un servicio systemd resiliente conectado a un broker en la nube.
Por qué Paho MQTT C para Linux embebido
Eclipse Paho C es C99 puro con una única dependencia opcional, OpenSSL, para TLS. Funciona en cualquier cosa, desde una Raspberry Pi hasta una pasarela industrial con unos pocos megabytes de RAM, es compatible con MQTT 3.1, 3.1.1 y 5, y lo empaquetan la mayoría de las distribuciones y capas de Yocto. Si estás eligiendo entre lenguajes y bibliotecas, la panorámica de bibliotecas cliente MQTT lo pone en contexto; si prefieres C++, Paho también ofrece un wrapper de C++ sobre esta biblioteca.
La biblioteca incluye cuatro variantes. Elige la adecuada al enlazar:
| Biblioteca | Estilo de API | TLS | Uso típico |
|---|---|---|---|
paho-mqtt3c |
Síncrona MQTTClient_* |
No | Herramientas sencillas en redes de confianza |
paho-mqtt3cs |
Síncrona MQTTClient_* |
Sí | La mayoría de publicadores y suscriptores embebidos |
paho-mqtt3a |
Asíncrona MQTTAsync_* |
No | Apps orientadas a eventos |
paho-mqtt3as |
Asíncrona MQTTAsync_* |
Sí | Apps orientadas a eventos con TLS |
Este tutorial usa paho-mqtt3cs: la API síncrona con TLS. "Síncrona" significa que las llamadas de conexión y publicación bloquean hasta completarse; los mensajes entrantes siguen llegando en un hilo en segundo plano mediante callbacks.
Instalar o compilar la biblioteca
En Debian, Ubuntu o Raspberry Pi OS la versión empaquetada suele bastar:
sudo apt update
sudo apt install -y build-essential libpaho-mqtt-dev libssl-dev ca-certificates
Para obtener la última versión, o para compilar de forma cruzada, compila desde el código fuente:
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
La documentación oficial de Paho C describe cada estructura y función que se usa a continuación.
Un publicador Paho MQTT C sobre TLS
Este programa se conecta a broker.connectmq.com en el puerto 8883, verifica el certificado del servidor contra el paquete de CA del sistema y publica una lectura cada cinco segundos. Funciona de forma anónima contra el servidor MQTT público gratuito y lee las credenciales de variables de entorno cuando pasas a un broker privado.
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; /* comprobar también el nombre de host */
ssl.sslVersion = MQTT_SSL_VERSION_TLS_1_2;
opts.keepAliveInterval = 30;
opts.cleansession = 1;
opts.ssl = &ssl;
opts.username = getenv("MQTT_USERNAME"); /* NULL = anónimo */
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;
}
Compila y ejecuta:
gcc -Wall -O2 -o publisher publisher.c -lpaho-mqtt3cs
./publisher
Algunas líneas merecen explicación. ssl.verify = 1 activa la verificación del nombre de host, que está desactivada por defecto; sin ella, se aceptaría cualquier certificado válido para cualquier host. Llamar a MQTTClient_setCallbacks antes de conectar pone al cliente en modo multihilo, de modo que la biblioteca ejecuta el keep-alive y el procesamiento de recepción en su propio hilo y connection_lost te avisa cuando cae el enlace. El bucle de publicación comprueba MQTTClient_isConnected y reconecta con su propio reintento, porque la API síncrona no reconecta automáticamente.
Un suscriptor con callbacks
El suscriptor reutiliza la misma lógica de conexión y hace su trabajo en message_arrived. Sustituye main por esto (manteniendo las funciones auxiliares anteriores):
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 = mensaje procesado; 0 pide a la biblioteca que lo vuelva a entregar */
}
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;
}
Liberar tanto el mensaje como la cadena del topic es obligatorio; olvidar cualquiera de los dos es la fuga de memoria más común en los programas Paho C. Mantén el callback corto. Si el procesamiento es lento (escribir en flash, llamar a una API HTTP), copia el payload a una cola y retorna, porque el callback se ejecuta en el hilo de recepción de la biblioteca.
También puedes observar al publicador sin escribir nada de código:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-4821 -t "connectmq/demo/paho-c-4821/#" -v
Compilación cruzada para una placa ARM
Para imágenes Yocto o Buildroot, Paho C tiene recetas listas. Para una compilación cruzada manual rápida, apunta CMake a tu toolchain y a un OpenSSL compilado de forma cruzada:
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
# Copiar el binario y la biblioteca compartida a la placa
scp publisher /opt/paho-arm/lib/libpaho-mqtt3cs.so* root@gateway:/usr/local/lib/
En la placa, mueve publisher a /usr/local/bin y ejecuta ldconfig. Paho también se puede compilar como biblioteca estática (-DPAHO_BUILD_STATIC=TRUE) para no tener que distribuir un .so aparte, pero entonces las actualizaciones de OpenSSL de ese binario pasan a ser cosa tuya. Comprueba que la imagen de destino contiene un paquete de CA; las imágenes mínimas a menudo no lo incluyen, y TLS falla con un error de verificación de certificado aunque el código sea correcto. Si la misma pasarela también gestiona una pantalla táctil local, el tutorial del cliente MQTT en Qt muestra cómo construir la parte de HMI en C++, y pruebas de clientes MQTT explica cómo poner este código bajo CI.
QoS, persistencia y almacenamiento offline
Las pasarelas pierden conectividad, así que decide de antemano qué ocurre con los datos durante un corte. El cliente anterior usa MQTTCLIENT_PERSISTENCE_NONE, que mantiene los mensajes QoS 1 en curso solo en memoria; si el proceso se reinicia a mitad de la entrega, esos mensajes se pierden. Paho puede, en cambio, persistir en disco los mensajes en curso: pasa MQTTCLIENT_PERSISTENCE_DEFAULT y la ruta de un directorio como los dos últimos argumentos de MQTTClient_create, conecta con cleansession = 0, y la biblioteca reenvía todo lo que aún no se haya confirmado tras una reconexión. En almacenamiento flash, valóralo frente al desgaste por escritura, porque persistir cada mensaje en una tarjeta SD barata acorta su vida útil.
La persistencia solo cubre los mensajes ya entregados a la biblioteca. En el ejemplo, las lecturas tomadas mientras el enlace está caído nunca llegan a MQTTClient_publishMessage, porque el bucle se salta la publicación hasta que reconecta. Si necesitas cada lectura, escríbelas en un búfer circular local (un archivo o una base de datos SQLite) y vacíalo tras reconectar, empezando por la más antigua, con la marca de tiempo original en el payload para que el backend pueda situar cada valor correctamente.
La elección de QoS depende de los datos. La telemetría periódica que se sustituye cada pocos segundos está bien con QoS 0 o 1; los comandos y las alarmas merecen QoS 1. QoS 2 añade un viaje de ida y vuelta extra y rara vez hace falta en pasarelas. El plan gratuito Serverless de ConnectMQ admite QoS hasta 1, y los planes de pago añaden QoS 2. La guía de niveles de QoS en MQTT explica en detalle las garantías de entrega.
Ejecutarlo como servicio systemd
/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
Las credenciales anteriores proceden de tu broker privado. Con un broker MQTT gestionado como ConnectMQ, creas el broker en el portal y recibes un usuario, una contraseña y un client ID generados; los client ID están vinculados a esas credenciales, así que da a cada pasarela su propio conjunto. Para cualquier cosa más allá de unos pocos dispositivos, mueve los secretos a un EnvironmentFile legible solo por root.
Códigos de retorno y solución de problemas
| Código de retorno / síntoma | Significado | Qué comprobar |
|---|---|---|
-1 (MQTTCLIENT_FAILURE) al conectar con TLS |
Fallo del handshake o del certificado | Ruta del paquete de CA, reloj del sistema (TLS falla si la fecha es incorrecta), nombre de host |
4 |
Usuario o contraseña incorrectos | Credenciales, salto de línea final en el archivo de entorno |
5 |
No autorizado | El client ID no coincide con las credenciales emitidas |
2 |
Identificador rechazado | Client ID vacío o duplicado |
| La conexión se cuelga en el 8883 | Un firewall bloquea el puerto | Prueba con openssl s_client -connect broker.connectmq.com:8883 |
| La memoria crece con los días | Mensajes o topics sin liberar | MQTTClient_freeMessage y MQTTClient_free en todas las rutas del callback |
Conviene insistir en el problema del reloj en Linux embebido: las placas sin reloj de tiempo real arrancan en 1970, y todos los certificados parecen "aún no válidos" hasta que NTP sincroniza. Ordena tu servicio después de time-sync.target si te afecta. Encontrarás más patrones de fallo de TLS en MQTT TLS handshake failed.
Empieza con ConnectMQ
Tu pasarela en C está lista para un broker real. Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, coloca las credenciales generadas en la unidad systemd y observa cómo llegan los mensajes al explorador de topics en vivo del portal. Cuando la flota crezca, el mismo binario se conecta a brokers de pago, dedicados o BYOC sin cambios en el código.
Preguntas frecuentes
¿Con qué biblioteca Paho C debo enlazar para usar TLS?
Enlaza paho-mqtt3cs para la API síncrona con TLS, o paho-mqtt3as para la API asíncrona con TLS. Las variantes sin la s final no tienen soporte SSL.
¿El cliente Paho MQTT C reconecta automáticamente?
La API síncrona MQTTClient no lo hace, así que comprueba MQTTClient_isConnected y reconecta tú mismo. La API asíncrona MQTTAsync ofrece una opción automaticReconnect.
¿Paho C es compatible con MQTT 5?
Sí. Crea el cliente con MQTTClient_createWithOptions y MQTTVERSION_5, y después usa la familia de funciones MQTTClient_connect5 y MQTTClient_publish5; ConnectMQ acepta tanto MQTT 3.1.1 como 5.0.
¿Cuánta memoria necesita Paho MQTT C?
Es lo bastante pequeña para las pasarelas típicas con Linux embebido, pero el consumo exacto depende de tus opciones de compilación y de OpenSSL. Mídelo en tu dispositivo de destino con la variante que realmente enlaces.



