9.18. MQTT, byte a byte

A esta altura, a câmara tem tudo o que precisa para comunicar com um serviço real na internet aberta: um socket TCP, TLS para o envolver, DNS para identificar o par, e asyncio para permitir que o mesmo script execute outras tarefas enquanto a ligação está aberta. MQTT é o primeiro protocolo de fio que une tudo isso em algo que um produto em produção utiliza de verdade.

Esta página aborda o próprio protocolo – o formato em fio, os papéis desempenhados por cada participante, e as compensações do seu design – com honestidade suficiente para que o cliente mqtt incluído pareça um mero revestimento do que já se sabe, em vez de um salto de fé.

9.18.1. Pub/sub vs pedido/resposta

HTTP – o protocolo que a maioria dos projetos com câmara utiliza em primeiro lugar – é pedido/resposta. Um cliente solicita a um servidor específico um recurso específico; o servidor responde. Cada troca é um para um, e ambas as partes conhecem o endereço uma da outra com antecedência.

MQTT é publicação/subscrição. Os clientes ligam-se a um terceiro no meio chamado broker. Um publicador envia uma mensagem para um tópico nomeado sem saber nem se preocupar com quem está a escutar. Um subscritor indica ao broker os tópicos que pretende e recebe todas as mensagens publicadas nesses tópicos a partir daí. O broker é o ponto de distribuição: uma publicação em yard-cam/motion chega a todos os dispositivos subscritos a yard-cam/motion, mesmo que sejam zero, um ou cinquenta.

Três consequências decorrem dessa mudança de modelo:

  • Desacoplamento. Os publicadores não precisam de saber que existem subscritores. Os subscritores podem entrar e sair sem que o publicador repare. Adicionar um segundo painel de controlo é uma linha de código no novo painel; a câmara não muda.

  • Distribuição em leque. O broker trata de cada duplicado, pelo que a câmara envia um único pacote independentemente de quantos dispositivos o leem. É o caso de uso para o qual o MQTT foi criado.

  • Assimetria. O broker é agora uma peça de infraestrutura obrigatória – sem um, o protocolo não funciona. Em projetos domésticos, isso é normalmente um broker público gratuito (test.mosquitto.org, broker.hivemq.com) ou um pequeno broker que corre você mesmo.

One cam publishing to a yard-cam/motion topic on a broker while two browser dashboards and one cloud archiver each receive the same message.

9.18.2. Tópicos

Os tópicos são cadeias de carateres separadas por barras. A convenção é mais geral à esquerda, mais específica à direita:

yard-cam/motion
yard-cam/temperature
workshop-cam/motion
workshop-cam/temperature/sensor-3

Dois carateres universais funcionam em subscrições (não em publicações):

  • + corresponde a um único nível. +/motion subscreve ao tópico de movimento de todas as câmaras; yard-cam/+ subscreve a todos os sub-tópicos de yard-cam.

  • # corresponde a um ou mais níveis finais. yard-cam/# subscreve a yard-cam/motion, yard-cam/temperature, yard-cam/temperature/sensor-3, e a tudo o mais sob yard-cam/. Deve aparecer no final da subscrição.

As cadeias de tópicos são sensíveis a maiúsculas e minúsculas. De acordo com a especificação, um $ inicial marca tópicos internos do broker ($SYS/...) para os quais os publicadores não devem escrever.

9.18.3. O formato do pacote

O MQTT corre sobre TCP. Cada pacote de controlo começa com um cabeçalho fixo de um byte seguido de um campo Comprimento Restante de comprimento variável, depois um cabeçalho variável específico do tipo de pacote, e depois o payload. O mesmo formato externo cobre todos os comandos – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT, e os restantes – razão pela qual um cliente MQTT pode ser escrito em algumas centenas de linhas.

The byte layout of an MQTT PUBLISH packet showing the fixed-header type and flags byte, the variable-length Remaining Length field, the topic name, the optional packet identifier, and the payload bytes.

O cabeçalho fixo tem um byte:

  • Os bits 7..4 são o tipo de pacote de controlo. 0x3 é PUBLISH (por isso o primeiro byte começa geralmente em 0x3?). 0x1 é CONNECT, 0x2 CONNACK, 0x8 SUBSCRIBE, 0xC PINGREQ, 0xE DISCONNECT, etc.

  • Os bits 3..0 são flags específicas do tipo de pacote. Para PUBLISH, as flags codificam o flag DUP de retransmissão, o nível de QoS (2 bits) e o flag RETAIN.

O Comprimento Restante é um inteiro de comprimento variável de 1 a 4 bytes que conta todos os bytes após si próprio. O bit superior de cada byte é um marcador de continuação – 1 significa «segue outro byte de comprimento», 0 significa «este é o último». Um comprimento inferior a 128 cabe num byte; payloads maiores utilizam mais bytes. O comprimento máximo codificável é 256 MiB.

Para um PUBLISH, o cabeçalho variável é o nome do tópico – um comprimento de 2 bytes, depois bytes UTF-8 – seguido de um identificador de pacote de 2 bytes que só existe quando o QoS é 1 ou 2. Os bytes restantes são o payload, tratado como bytes opacos pelo protocolo.

Um PUBLISH mínimo de QoS 0 de ok para a/b é:

30 07 00 03 'a' '/' 'b' 'o' 'k'
  • 30 – PUBLISH, todas as flags a zero.

  • 07 – seguem-se 7 bytes.

  • 00 03 – comprimento do tópico 3.

  • 'a' '/' 'b' – tópico.

  • 'o' 'k' – payload.

Nove bytes em fio e a mensagem chega a todos os subscritores de a/b no broker.

9.18.4. Níveis de QoS

A Qualidade de Serviço controla o esforço do broker (e do cliente) para garantir a entrega. Os três níveis:

QoS 0 – no máximo uma vez. Disparar e esquecer. O pacote PUBLISH é enviado e nunca confirmado. Se o TCP entregar, o broker reencaminha. Se a ligação cair a meio do envio, a mensagem perde-se. A maior parte da telemetria de sensores funciona bem com QoS 0 – uma leitura de temperatura em falta num fluxo que emite a cada 30 segundos não tem importância.

QoS 1 – pelo menos uma vez. O publicador inclui um identificador de pacote e aguarda um PUBACK. Se não chegar nenhum PUBACK antes de um tempo limite, o publicador retransmite com o flag DUP definido. O broker pode acabar por entregar a mesma mensagem duas vezes a um subscritor no mesmo nível; o subscritor tem de estar disposto a lidar com duplicados.

QoS 2 – exatamente uma vez. Um handshake de quatro etapas (PUBREC / PUBREL / PUBCOMP) garante que a mensagem chega exatamente uma vez, mesmo após reconexões. Dispendioso em termos de round-trips e estado do broker. Poucas aplicações com câmara precisam disto.

O cliente mqtt incluído implementa QoS 0 e QoS 1; QoS 2 levanta uma exceção se for solicitado. Para uma câmara a reportar leituras de sensores, QoS 0 é quase sempre a resposta certa.

9.18.5. Mensagens retidas e último testamento

Duas funcionalidades merecem atenção porque alteram o que o broker memoriza sobre o seu tópico.

RETAIN. Se um PUBLISH tiver o flag RETAIN definido, o broker armazena a mensagem e encaminha-a para cada futuro subscritor no momento em que subscreve. É assim que o MQTT trata «qual é o valor atual?» – um sensor publica a sua leitura mais recente como retida, e um painel de controlo que subscreve dez minutos depois continua a receber o valor mais recente em vez de aguardar a próxima publicação. Publicar novamente com o mesmo tópico substitui o valor retido; publicar com um payload vazio limpa-o.

Último testamento. Quando um cliente se liga, pode fornecer ao broker um «último testamento»: um tópico, um payload, um QoS e um flag de retenção. Se esse cliente se desligar de forma não limpa – TCP RESET, perda de energia, queda de rede sem pacote DISCONNECT – o broker publica o testamento em nome do cliente. Os subscritores veem-no como a notificação da câmara de que ficou offline. A própria câmara nunca envia o testamento; o broker faz-o, porque a essa altura a câmara já desapareceu.

9.18.6. Keepalive e reconexão

O CONNECT transporta um intervalo de keepalive em segundos. Se o cliente estiver em silêncio durante esse tempo, o broker considera-o morto. Para evitar isso, o cliente envia periodicamente um PINGREQ (um byte: 0xC0) e recebe de volta um PINGRESP (0xD0) – o heartbeat mais pequeno e mais barato que o protocolo pode transportar. A maioria das aplicações com câmara define o keepalive para 30 ou 60 segundos.

Se a ligação TCP cair, ambos os lados reparam e reconectam-se do zero. As subscrições feitas antes da queda perdem-se a menos que o cliente tenha utilizado uma sessão persistente na ligação; para aplicações simples com câmara, o padrão de resubscrição na reconexão é mais curto e igualmente eficaz.

Isto é suficiente para ler a especificação MQTT ou escrever manualmente um cliente sobre um socket.socket. O cliente incluído em mqtt faz exatamente isso, mais uma API sensata para o código de aplicação.