Se você já publicou um tópico MQTT a partir de um microcontrolador, provavelmente usou uma biblioteca em C herdada, um wrapper de vendor ou um stub gerado para o seu RTOS. O que poucos percebem é que o Zig é uma das linguagens mais confortáveis para falar MQTT hoje: o protocolo é binário, compacto e previsível, e o Zig foi desenhado justamente para ler e montar bytes assim, sem alocação escondida e sem runtime. O resultado é um cliente MQTT que cabe em poucos quilobytes, compila para o seu alvo embarcado e você entende por inteiro.
Este guia mostra como usar MQTT com Zig de forma prática: entender o modelo pub/sub, escolher entre usar uma biblioteca C (como a libmosquitto) e escrever um cliente nativo em Zig, montar os pacotes CONNECT, PUBLISH e SUBSCRIBE na mão, garantir QoS, keepalive e last will, fechar a conexão com TLS e operar em produção. Ele complementa o material sobre IoT e embarcados em Zig, o firmware de ESP32 com FreeRTOS, os fundamentos de sockets TCP/UDP e a interoperabilidade com C. Para a parte de carreira, vale ligar com o guia de carreira em embedded com Zig e a página de vagas de Zig no Brasil.
Por que MQTT combina com Zig
O MQTT é um protocolo de mensageria leve sobre TCP criado para redes com banda limitada e dispositivos restritos. Em vez de o cliente falar diretamente com outro cliente, todo mundo publica e assina em um broker central usando tópicos hierárquicos — por exemplo fabrica/linha1/sensor/temperatura. Esse modelo pub/sub desacopla produtores e consumidores e lida bem com devices que desconectam o tempo todo.
As características que tornam o MQTT bom para embarcados são exatamente as que casam com a filosofia do Zig:
- binário e compacto: o cabeçalho fixo tem 2 bytes; cada mensagem carrega pouco overhead, o que importa quando você roda em um enlace de rádio de baixa taxa;
- sem mágica de runtime: não há coletor de lixo nem máquina virtual entre o seu código e o socket;
- controle fino de memória: você aloca buffers do tamanho exato para os pacotes, sem suprir as dependências de uma biblioteca genérica;
- compila para qualquer alvo: o mesmo cliente MQTT roda no seu notebook, em um Raspberry Pi e, com os cuidados de cross-compilação, direto no firmware.
Em suma, o MQTT resolve o problema de “como vários dispositivos conversam de forma resiliente”, e o Zig resolve o problema de “como fazer isso sem desperdiçar RAM e sem perder previsibilidade”.
Como o MQTT funciona por baixo dos panos
Antes de escrever código, vale fixar os conceitos que aparecem em qualquer cliente:
- Broker: o servidor que recebe
PUBLISHe replica para quem temSUBSCRIBEcasando com o tópico (Mosquitto, EMQX e HiveMQ são exemplos comuns). - Tópicos e wildcards: tópicos são separados por
/. O+casa um nível (sensor/+/temp) e o#casa vários níveis a partir dali (fabrica/#). - QoS (Quality of Service):
0é fire-and-forget (no máximo uma vez),1exigePUBACK(pelo menos uma vez, pode duplicar) e2faz um handshake de quatro passos (exatamente uma vez, mais caro). - Mensagem retida: o broker guarda a última mensagem publicada com a flag retain e a entrega para novos assinantes daquele tópico.
- Last Will and Testament (LWT): uma mensagem que o broker publica em seu nome se a conexão cair sem um
DISCONNECTlimpo — útil para sinalizar que um device ficou offline. - Keepalive: a cada N segundos sem tráfego, cliente e broker trocam
PINGREQ/PINGRESP; se um lado parar de responder, a conexão é considerada morta.
As portas padrão são 1883 (TCP puro) e 8883 (MQTT sobre TLS). Brokers também costumam expor WebSocket em 8083/8443 para aplicações web.
Duas abordagens: biblioteca C ou cliente nativo
Você tem dois caminhos, e a escolha depende do seu projeto:
- Usar a libmosquitto (ou Paho MQTT C) via FFI — é a opção mais segura para produção. A libmosquitto é madura, trata reconexão, QoS 2 e TLS. Em Zig você a importa com
@cImporte linka nobuild.zig, como mostrado no guia de interoperabilidade com C. O custo é a dependência externa e o acoplamento à API em C. - Escrever um cliente MQTT nativo em Zig — ideal para embarcados restritos, para quem quer eliminar dependências e para entender o protocolo. Funciona perfeitamente para QoS 0 e 1 (a esmagadora maioria dos casos de telemetria). É o que vamos detalhar a seguir.
A regra prática: use a libmosquitto quando precisar de QoS 2, sessões persistentes complexas ou TLS com mTLS corporativo; escreva o cliente em Zig quando quiser um binário mínimo, controle total e um footprint previsível em flash.
O detalhe que define o protocolo: o remaining length
Todo pacote MQTT começa com um byte de tipo/flags seguido de um campo chamado Remaining Length, que diz quantos bytes vêm depois. Esse campo é um inteiro de comprimento variável (1 a 4 bytes, 7 bits úteis por byte, com o bit mais alto indicando continuação). É a parte mais fácil de errar — e onde o Zig brilha:
// Codifica o "remaining length" do MQTT (1 a 4 bytes).
fn encodeRemainingLength(out: *[4]u8, len: u32) usize {
var written: usize = 0;
var x: u32 = len;
while (true) {
var encoded_byte: u8 = @intCast(x % 128);
x = x / 128;
if (x > 0) encoded_byte |= 0x80; // continuation bit
out[written] = encoded_byte;
written += 1;
if (x == 0) break;
}
return written;
}
Sabendo disso, o resto é só montar o payload na ordem certa. Essa função, sozinha, já elimina o atrito de 90% dos tutoriais de MQTT em outras linguagens.
Conectando a um broker com um cliente nativo
Abaixo está o esqueleto de um cliente MQTT 3.1.1 em Zig que conecta a um broker local (por exemplo, um mosquitto rodando em 127.0.0.1:1883), autentica, assina um tópico e fica lendo mensagens publicadas pelo broker. As APIs de std.net mudam entre versões do Zig; trate o trecho como a estrutura canônica e ajuste os nomes da sua versão (0.14+ / 0.15+).
const std = @import("std");
const net = std.net;
// Monta um pacote CONNECT do MQTT 3.1.1 (clean session, keepalive 60s).
fn buildConnect(buf: []u8, client_id: []const u8) []u8 {
// Variable header: protocol name "MQTT", level 4, flags, keepalive.
var vh: [10]u8 = .{
0x00, 0x04, 'M', 'Q', 'T', 'T', // protocol name
0x04, // protocol level (3.1.1)
0x02, // clean session flag
0x00, 60, // keepalive = 60s (big endian)
};
var pos: usize = 0;
var rl: [4]u8 = undefined;
const rl_len = encodeRemainingLength(&rl, vh.len + 2 + client_id.len);
buf[pos] = 0x10; // CONNECT
pos += 1;
@memcpy(buf[pos .. pos + rl_len], &rl);
pos += rl_len;
@memcpy(buf[pos .. pos + vh.len], &vh);
pos += vh.len;
// Payload: client ID com prefixo de comprimento (u16 big endian).
buf[pos] = 0x00;
buf[pos + 1] = @intCast(client_id.len);
pos += 2;
@memcpy(buf[pos .. pos + client_id.len], client_id);
pos += client_id.len;
return buf[0..pos];
}
pub fn main() !void {
const addr = try net.Address.parseIp("127.0.0.1", 1883);
var stream = try net.tcpConnectToAddress(addr);
defer stream.close();
var buf: [256]u8 = undefined;
const connect = buildConnect(&buf, "zig-client-01");
try stream.writeAll(connect);
// Lê o CONNACK (4 bytes: 0x20 0x02 0x00 <return code>).
var connack: [4]u8 = undefined;
_ = try stream.read(&connack);
std.debug.assert(connack[3] == 0x00); // 0x00 = aceito
// SUBSCRIBE no tópico "sensor/#" com QoS 0.
const subscribe = [_]u8{
0x82, 0x08, // SUBSCRIBE, remaining length 8
0x00, 0x01, // packet identifier = 1
0x00, 0x07, 's', 'e', 'n', 's', 'o', 'r', '/', '#',
0x00, // requested QoS 0
};
try stream.writeAll(&subscribe);
// Loop de leitura: imprime payloads de PUBLISH recebidos.
var inbox: [512]u8 = undefined;
while (true) {
const n = try stream.read(&inbox);
if (n == 0) break;
// PUBLISH: byte 0 = 0x30 (QoS0). Ignora cabeçalho e tópico aqui.
if (n > 0 and (inbox[0] & 0xF0) == 0x30) {
const topic_len: usize =
(@as(usize, inbox[2]) << 8) | inbox[3];
const payload = inbox[4 + topic_len .. n];
std.debug.print("recebido: {s}\n", .{payload});
}
}
}
Esse exemplo é deliberadamente minimal: ele não decodifica o SUBACK, não implementa reconexão nem keepalive. Mas ele é o esqueleto real — a partir dele, cada peça de robustez é um acréscimo local e testável, exatamente como nos padrões de concorrência e rede em Zig.
Publicando telemetria
Para publicar, monte um PUBLISH. O byte de tipo carrega flags: bit 3 é DUP, bits 1–2 são o QoS e o bit 0 é retain. Para QoS 0 sem retain, o byte é 0x30:
fn buildPublish(buf: []u8, topic: []const u8, payload: []const u8) []u8 {
var pos: usize = 0;
var rl: [4]u8 = undefined;
const remaining = 2 + topic.len + payload.len;
const rl_len = encodeRemainingLength(&rl, @intCast(remaining));
buf[pos] = 0x30; // PUBLISH, QoS0, no retain
pos += 1;
@memcpy(buf[pos .. pos + rl_len], &rl);
pos += rl_len;
buf[pos] = @intCast((topic.len >> 8) & 0xFF);
buf[pos + 1] = @intCast(topic.len & 0xFF);
pos += 2;
@memcpy(buf[pos .. pos + topic.len], topic);
pos += topic.len;
@memcpy(buf[pos .. pos + payload.len], payload);
pos += payload.len;
return buf[0..pos];
}
O padrão que emerge é sempre o mesmo: calcular o remaining length, copiar o cabeçalho, copiar os comprimentos em big endian, copiar os dados. Depois do terceiro pacote, você escreve MQTT sem consultar a especificação.
QoS, keepalive e reconexão
Para telemetria, QoS 0 basta: perde-se uma leitura ocasional e tudo bem. Para comandos (abrir uma válvula, acionar um atuador), suba para QoS 1, que exige um PUBACK do broker e reenvio em caso de timeout. Em Zig, o reenvio é trivial: mantenha o pacote em um buffer, dispare um timer com std.time e reenvie se o PUBACK não vier — o padrão de timeout e retry é o mesmo usado no guia de circuit breaker, timeout e retry.
O keepalive evita que firewalls e NATs derrubem conexões ociosas: a cada keepalive segundos sem enviar nada, mande um PINGREQ (0xC0 0x00) e espere um PINGRESP (0xD0 0x00). Se o broker não responder dentro do tempo razoável, feche o socket e reconecte. Reconexão com backoff exponencial, limite de tentativas e estado salvo em flash é o que separa um protótipo de um device que sobrevive a quedas de energia e de rede — recomendações detalhadas estão no material sobre sistemas de tempo real.
Last Will e mensagens retidas
Configure o Last Will no CONNECT (na seção de variável header/payload) para que o broker publique, por exemplo, dispositivos/zig-client-01/status = offline se a sua conexão cair abruptamente. Combine com uma mensagem retida que você mesmo publica ao reconectar (.../status = online, com a flag retain): novos assinantes saberão instantaneamente o estado atual de cada device, sem precisar esperar a próxima publicação.
TLS para MQTT
Rodar telemetria em texto puro na internet pública é um risco. Use a porta 8883 com TLS. Em embarcados com recursos, a libmosquitto cuida do handshake; em um cliente nativo, você encadeia o socket em std.crypto.tls (quando disponível no seu alvo) ou terceiriza para a biblioteca do sistema. Os cuidados com cadeia de certificados, validação de hostname e mTLS são os mesmos do guia de TLS, HTTPS e mTLS em produção.
Backpressure, fila local e observabilidade
Devices de campo produzem dados em rajadas. Se o broker estiver lento, não bloqueie a coleta: acumule em uma fila local em memória (ou em flash, se volátil) e drene quando o socket respirar. Esse padrão de fila de workers com descarte controlado é o do guia de filas e workers em background. E, como em qualquer sistema distribuído, exponha métricas: conexões estabelecidas, mensagens publicadas, backoff atual, reconexões — integrando com o que já existe em observabilidade e Prometheus.
Quando NÃO usar MQTT
O MQTT não é sempre a resposta. Considere alternativas quando:
- o cliente é um navegador ou app que já fala HTTP/WebSocket —
SSEe Websockets costumam bastar (veja Server-Sent Events em Zig); - você precisa de mensageria entre serviços de backend comordering forte, partitions e replay — NATS, Kafka ou filas AMQP são mais adequados;
- o tráfego é consulta/resposta simples de baixa frequência — um HTTP REST enxuto é mais simples de operar.
O MQTT brilha no meio termo: muitos devices, mensagens pequenas e frequentes, tolerância a reconexão e baixo consumo de b banda.
Checklist de produção
Antes de levar um cliente MQTT em Zig para campo, confirme que:
- o
client_idé único por device (senão o broker derruba a conexão anterior); - o keepalive está configurado e há
PINGREQ/PINGRESPefetivos; - há reconexão com backoff e limite de tentativas;
- o Last Will e a mensagem retida de status estão definidos;
- o TLS está habilitado em
8883com validação de certificado (ou mTLS, se exigido); - há fila/backpressure para rajadas e métricas exportadas;
- os buffers têm tamanho fixo conhecido, sem alocação no hot path;
- o QoS escolhido corresponde à criticidade de cada tópico (telemetria = 0, comando = 1).
Perguntas frequentes
Preciso de uma biblioteca para falar MQTT em Zig? Não. O protocolo é simples o suficiente para um cliente nativo com algumas centenas de linhas, como mostrado aqui. Para QoS 2 e TLS corporativo, porém, a libmosquitto via FFI poupa tempo.
Qual broker usar em desenvolvimento? O Mosquitto é o padrão de fato. Rode docker run -p 1883:1883 eclipse-mosquitto e aponte seu cliente Zig para 127.0.0.1:1883.
MQTT 5 ou 3.1.1? Muitos dispositivos embarcados ainda usam 3.1.1 (level 4), que é o deste guia. O MQTT 5 adiciona shared subscriptions, reason codes e properties, mas aumenta a complexidade do encoder.
Qual o tamanho típico de um cliente MQTT em Zig? Um cliente QoS 0/1 com reconexão e keepalive costuma ficar entre 4 e 8 KB de binário, dependendo do alvo — um dos motivos pelo qual o Zig é atrativo para firmware.
Próximos passos
Para aprofundar, leia sobre IoT e embarcados em Zig, firmware de ESP32 com FreeRTOS, os fundamentos de rede com sockets TCP/UDP e a interoperabilidade com C para o caminho da libmosquitto. Para comparar com outra linguagem de sistemas forte em embarcados — onde o cenário MQTT costuma exigir bindar uma biblioteca C via crate extern — veja como o tema aparece em Rust para embarcados: a diferença é que o Rust depende de bindings (bindgen/cc) para a pilha de rede embarcada, enquanto o Zig compila o mesmo código C junto no build e fala o protocolo diretamente, sem camada extra de FFI para o caso nativo.