A biblioteca Paho MQTT C é a escolha padrão quando você precisa de um cliente MQTT pequeno e com poucas dependências no Linux embarcado. Neste tutorial você vai compilar o Paho MQTT C com OpenSSL, escrever um publicador TLS e um assinante, fazer cross-compile para uma placa ARM e rodar o resultado como um serviço systemd resiliente, conectado a um broker na nuvem.
Por que Paho MQTT C para Linux embarcado
O Eclipse Paho C é C99 puro, com uma única dependência opcional, o OpenSSL, para TLS. Ele roda em tudo, de um Raspberry Pi a um gateway industrial com poucos megabytes de RAM, suporta MQTT 3.1, 3.1.1 e 5, e está empacotado na maioria das distribuições e camadas Yocto. Se você está escolhendo entre linguagens e bibliotecas, a visão geral de bibliotecas cliente MQTT coloca a biblioteca em contexto; se preferir C++, o Paho também oferece um wrapper C++ sobre esta biblioteca.
A biblioteca vem em quatro variantes. Escolha a certa na hora do link:
| Biblioteca | Estilo de API | TLS | Uso típico |
|---|---|---|---|
paho-mqtt3c |
Síncrona MQTTClient_* |
Não | Ferramentas simples em redes confiáveis |
paho-mqtt3cs |
Síncrona MQTTClient_* |
Sim | A maioria dos publicadores e assinantes embarcados |
paho-mqtt3a |
Assíncrona MQTTAsync_* |
Não | Apps orientados a eventos |
paho-mqtt3as |
Assíncrona MQTTAsync_* |
Sim | Apps orientados a eventos com TLS |
Este tutorial usa paho-mqtt3cs: a API síncrona com TLS. "Síncrona" significa que as chamadas de conexão e publicação bloqueiam até terminar; as mensagens recebidas continuam chegando em uma thread em segundo plano, por meio de callbacks.
Instalando ou compilando a biblioteca
No Debian, Ubuntu ou Raspberry Pi OS, a versão empacotada geralmente basta:
sudo apt update
sudo apt install -y build-essential libpaho-mqtt-dev libssl-dev ca-certificates
Para obter a versão mais recente, ou para fazer cross-compile, compile a partir do código-fonte:
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
A documentação oficial do Paho C descreve cada estrutura e função usada a seguir.
Um publicador Paho MQTT C via TLS
Este programa se conecta a broker.connectmq.com na porta 8883, verifica o certificado do servidor contra o bundle de CAs do sistema e publica uma leitura a cada cinco segundos. Ele funciona de forma anônima no servidor MQTT público (apenas para testes) e lê as credenciais de variáveis de ambiente quando você migra para um 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; /* verifica também o hostname */
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;
}
Compile e execute:
gcc -Wall -O2 -o publisher publisher.c -lpaho-mqtt3cs
./publisher
Algumas linhas merecem explicação. ssl.verify = 1 ativa a verificação do hostname, que vem desligada por padrão; sem ela, qualquer certificado válido para qualquer host seria aceito. Chamar MQTTClient_setCallbacks antes de conectar coloca o cliente em modo multithread, de modo que a biblioteca executa o keep-alive e o processamento de recebimento na própria thread, e connection_lost avisa quando o link cai. O loop de publicação verifica MQTTClient_isConnected e reconecta com uma tentativa própria, porque a API síncrona não reconecta automaticamente.
Um assinante com callbacks
O assinante reutiliza a mesma lógica de conexão e faz o seu trabalho em message_arrived. Substitua o main por este (mantendo as funções auxiliares acima):
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 = mensagem tratada; 0 pede à biblioteca que reentregue */
}
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 a mensagem quanto a string do topic é obrigatório; esquecer qualquer um dos dois é o vazamento de memória mais comum em programas Paho C. Mantenha o callback curto. Se o processamento for lento (gravar em flash, chamar uma API HTTP), copie o payload para uma fila e retorne, porque o callback roda na thread de recebimento da biblioteca.
Você também pode acompanhar o publicador sem escrever código nenhum:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-4821 -t "connectmq/demo/paho-c-4821/#" -v
Cross-compile para uma placa ARM
Para imagens Yocto ou Buildroot, o Paho C tem receitas prontas. Para um cross build manual e rápido, aponte o CMake para o seu toolchain e para um 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
# Copie o binário e a biblioteca compartilhada para a placa
scp publisher /opt/paho-arm/lib/libpaho-mqtt3cs.so* root@gateway:/usr/local/lib/
Na placa, mova publisher para /usr/local/bin e execute ldconfig. O Paho também pode ser compilado como biblioteca estática (-DPAHO_BUILD_STATIC=TRUE) para evitar distribuir um .so separado, mas aí as atualizações do OpenSSL desse binário ficam por sua conta. Verifique se a imagem de destino contém um bundle de CAs; imagens mínimas muitas vezes não têm, e o TLS falha com um erro de verificação de certificado mesmo com o código correto. Se o mesmo gateway também controla uma tela touch local, o tutorial de cliente MQTT com Qt mostra como construir o lado da IHM em C++, e como testar clientes MQTT explica como colocar este código sob CI.
QoS, persistência e buffer offline
Gateways perdem conectividade, então decida desde o início o que acontece com os dados durante uma queda. O cliente acima usa MQTTCLIENT_PERSISTENCE_NONE, que mantém as mensagens QoS 1 em trânsito apenas na memória; se o processo reiniciar no meio da entrega, essas mensagens se perdem. Em vez disso, o Paho pode persistir as mensagens em trânsito em disco: passe MQTTCLIENT_PERSISTENCE_DEFAULT e um caminho de diretório como os dois últimos argumentos de MQTTClient_create, conecte com cleansession = 0, e a biblioteca reenvia tudo o que ainda não foi confirmado após uma reconexão. Em armazenamento flash, pese isso contra o desgaste por escrita, porque persistir cada mensagem em um cartão SD barato encurta a vida útil dele.
A persistência só cobre mensagens já entregues à biblioteca. Leituras feitas enquanto o link está fora do ar nunca chegam a MQTTClient_publishMessage no exemplo, porque o loop pula a publicação até reconectar. Se você precisa de todas as leituras, grave-as em um buffer circular local (um arquivo ou um banco SQLite) e esvazie-o depois de reconectar, das mais antigas para as mais novas, com o timestamp original no payload para que o back end posicione cada valor corretamente.
A escolha do QoS acompanha os dados. Telemetria periódica que é substituída a cada poucos segundos funciona bem com QoS 0 ou 1; comandos e alarmes merecem QoS 1. O QoS 2 acrescenta uma ida e volta extra e raramente é necessário em gateways. O plano gratuito ConnectMQ Serverless suporta QoS até 1, e os planos pagos acrescentam o QoS 2. O guia dos níveis de QoS do MQTT explica as garantias de entrega em detalhes.
Rodando como serviço 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
As credenciais acima vêm do seu broker privado. Com um broker MQTT gerenciado como o ConnectMQ, você cria o broker no portal e recebe usuário, senha e client ID gerados; os client IDs ficam vinculados a essas credenciais, então dê a cada gateway o seu próprio conjunto. Para qualquer coisa além de um punhado de dispositivos, mova os segredos para um EnvironmentFile legível apenas pelo root.
Códigos de retorno e solução de problemas
| Código de retorno / sintoma | Significado | O que verificar |
|---|---|---|
-1 (MQTTCLIENT_FAILURE) ao conectar com TLS |
Falha no handshake ou no certificado | Caminho do bundle de CAs, relógio do sistema (o TLS falha se a data estiver errada), hostname |
4 |
Usuário ou senha inválidos | Credenciais, quebra de linha no final do arquivo de ambiente |
5 |
Não autorizado | O client ID não corresponde às credenciais emitidas |
2 |
Identificador rejeitado | Client ID vazio ou duplicado |
| A conexão trava na 8883 | Firewall bloqueia a porta | Teste com openssl s_client -connect broker.connectmq.com:8883 |
| A memória cresce ao longo de dias | Mensagens ou topics não liberados | MQTTClient_freeMessage e MQTTClient_free em todos os caminhos do callback |
Vale insistir na questão do relógio no Linux embarcado: placas sem relógio de tempo real inicializam em 1970, e todo certificado parece "ainda não válido" até o NTP sincronizar. Ordene o seu serviço depois de time-sync.target se isso for um problema. Mais padrões de falha de TLS estão em MQTT TLS handshake failed.
Comece a usar o ConnectMQ
O seu gateway em C está pronto para um broker real. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, coloque as credenciais geradas na unit do systemd e veja as mensagens chegando no explorador de topics ao vivo do portal. Quando a frota crescer, o mesmo binário se conecta a brokers pagos, dedicados ou BYOC sem mudanças no código.
Perguntas frequentes
Contra qual biblioteca Paho C devo fazer o link para usar TLS?
Faça o link com paho-mqtt3cs para a API síncrona com TLS, ou com paho-mqtt3as para a API assíncrona com TLS. As variantes sem o s no final não têm suporte a SSL.
O cliente Paho MQTT C reconecta automaticamente?
A API síncrona MQTTClient não reconecta, então verifique MQTTClient_isConnected e reconecte você mesmo. A API assíncrona MQTTAsync oferece a opção automaticReconnect.
O Paho C suporta MQTT 5?
Sim. Crie o cliente com MQTTClient_createWithOptions e MQTTVERSION_5 e use a família de funções MQTTClient_connect5 e MQTTClient_publish5; o ConnectMQ aceita tanto MQTT 3.1.1 quanto 5.0.
Quanta memória o Paho MQTT C precisa?
Ele é pequeno o suficiente para gateways Linux embarcados típicos, mas o consumo exato depende das opções de build e do OpenSSL. Meça no seu hardware de destino com a variante que você realmente usa no link.



