Testes de brokers

Teste quais recursos do MQTT um broker suporta (MQTT 5, QoS 2, retain)

Descubra as capacidades reais de um broker MQTT: leia o CONNACK e verifique QoS máxima, retain, wildcards e assinaturas compartilhadas com Python.

Atualizado · 9 min de leitura

Teste quais recursos do MQTT um broker suporta (MQTT 5, QoS 2, retain)

Documentação e tabelas comparativas dizem o que um broker consegue fazer. O que importa é o que o seu broker, com a sua configuração e as suas credenciais, realmente permite. O jeito mais rápido e confiável de descobrir é sondá-lo: conectar com MQTT 5, ler as propriedades do CONNACK e depois testar cada recurso, olhando os reason codes que voltam.

Este artigo explica quais sinais verificar e traz um script Python (paho-mqtt 2.x) que imprime um relatório de capacidades para qualquer broker.

O que um broker informa no CONNACK

Um broker MQTT 5 anuncia seus limites no pacote CONNACK. A regra importante: uma propriedade só é enviada quando difere do padrão. A ausência de RetainAvailable não significa "desconhecido" — significa "sim".

Propriedade do CONNACKPadrão se ausenteO que quebra se for restrita
Maximum QoS2Publicar acima dela gera 0x9B QoS not supported e a conexão é fechada
Retain AvailablesimPublicações retidas recebem 0x9A Retain not supported
Wildcard Subscription AvailablesimAssinaturas com + / # recebem 0xA2 Wildcard Subscriptions not supported
Shared Subscription AvailablesimAssinaturas $share/... recebem 0x9E Shared Subscriptions not supported
Subscription Identifiers AvailablesimAssinaturas com identificador recebem 0xA1
Receive Maximum65.535Quantas mensagens QoS 1/2 você pode ter em trânsito para o broker
Maximum Packet Sizesem limite (máximo do protocolo)Pacotes maiores recebem 0x95 Packet too large
Topic Alias Maximum0 (aliases não aceitos)Usar um alias que o broker não permitiu é 0x94 Topic Alias invalid
Server Keep Aliveo seu valorO broker sobrescreve seu keep-alive; seu cliente precisa usar o valor dele
Assigned Client Identifier—Enviado quando você conecta com um client ID vazio

O MQTT 3.1.1 não tem nada disso. Um broker 3.1.1 só diz se a conexão foi aceita, então tudo precisa ser verificado testando. Você pode consultar qualquer um desses códigos na referência de reason codes.

O script de sondagem

O script abaixo faz seis verificações: versões do protocolo, propriedades do CONNACK, QoS concedida por assinatura, assinaturas com wildcard e compartilhadas, mensagens retidas e uma entrega QoS 2 de ponta a ponta. Configure-o com variáveis de ambiente e rode em cada ambiente que importa para você.

probe.py
"""Report which MQTT features a broker supports. Requires paho-mqtt >= 2.0."""
import os, threading, time, uuid
import paho.mqtt.client as mqtt

HOST = os.environ.get("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.environ.get("MQTT_PORT", "1883"))
USER, PASS = os.environ.get("MQTT_USER"), os.environ.get("MQTT_PASS")
BASE = f"testmqtt/probe/{uuid.uuid4().hex[:8]}"

DEFAULTS = {  # value that applies when the broker omits the property
    "MaximumQoS": 2, "RetainAvailable": 1, "WildcardSubscriptionAvailable": 1,
    "SharedSubscriptionAvailable": 1, "SubscriptionIdentifierAvailable": 1,
    "ReceiveMaximum": 65535, "MaximumPacketSize": "no limit",
    "TopicAliasMaximum": 0, "ServerKeepAlive": "client value",
}

def connect(protocol=mqtt.MQTTv5, timeout=10):
    c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                    client_id=f"probe-{uuid.uuid4().hex[:8]}", protocol=protocol)
    if USER:
        c.username_pw_set(USER, PASS)
    if PORT == 8883:
        c.tls_set()
    done, res = threading.Event(), {}
    def on_connect(client, userdata, flags, reason_code, properties):
        res.update(rc=reason_code, props=properties)
        done.set()
    c.on_connect = on_connect
    c.connect(HOST, PORT, keepalive=30)
    c.loop_start()
    if not done.wait(timeout):
        c.loop_stop()
        raise TimeoutError("no CONNACK")
    return c, res["rc"], res["props"]

def close(*clients):
    for c in clients:
        c.disconnect()
        c.loop_stop()

def subscribe(c, topic_filter, qos=2, timeout=5):
    done, res = threading.Event(), {}
    def on_subscribe(client, userdata, mid, reason_code_list, properties):
        res["rc"] = reason_code_list[0]
        done.set()
    c.on_subscribe = on_subscribe
    c.subscribe(topic_filter, qos=qos)
    return res["rc"] if done.wait(timeout) else "no SUBACK"

def show(label, value):
    print(f"  {label:34} {value}")

def main():
    print(f"Broker {HOST}:{PORT}  (test topics under {BASE})\n")

    print("Protocol versions")
    for name, proto in (("MQTT 3.1.1", mqtt.MQTTv311), ("MQTT 5", mqtt.MQTTv5)):
        try:
            c, rc, _ = connect(proto)
            show(name, "OK" if not rc.is_failure else f"refused: {rc}")
            close(c)
        except Exception as e:
            show(name, f"FAILED ({e})")

    c, rc, props = connect()
    if rc.is_failure:
        raise SystemExit(f"MQTT 5 connect refused: {rc}")

    print("\nCONNACK properties")
    for name, default in DEFAULTS.items():
        value = getattr(props, name, None)
        show(name, value if value is not None else f"{default} (default)")

    print("\nSubscriptions (granted QoS or reason code)")
    for q in (0, 1, 2):
        show(f"QoS {q} subscription", subscribe(c, f"{BASE}/qos/{q}", qos=q))
    show("Wildcard filter (+)", subscribe(c, f"{BASE}/+/wild", qos=1))
    show("Shared subscription ($share)", subscribe(c, f"$share/probe/{BASE}/shared", qos=1))

    print("\nActive tests")
    got = {}
    received = threading.Event()
    def on_message(client, userdata, msg):
        got[msg.topic] = msg
        received.set()
    c.on_message = on_message

    # End-to-end QoS 2: publish at 2 to a QoS 2 subscription, check delivered QoS
    received.clear()
    c.publish(f"{BASE}/qos/2", b"qos2-probe", qos=2)
    if received.wait(5):
        show("QoS 2 delivery", f"delivered at QoS {got[f'{BASE}/qos/2'].qos}")
    else:
        show("QoS 2 delivery", "NOT delivered")

    # Retained message: publish, then subscribe with a fresh client
    topic = f"{BASE}/retain"
    try:
        c.publish(topic, b"retained-probe", qos=1, retain=True).wait_for_publish(5)
        sub, _, _ = connect()
        hit = threading.Event()
        sub.on_message = lambda client, u, m: m.retain and hit.set()
        subscribe(sub, topic, qos=1)
        show("Retained message", "OK" if hit.wait(5) else "NOT delivered")
        close(sub)
        c.publish(topic, b"", qos=1, retain=True).wait_for_publish(5)  # clear it
    except Exception as e:
        show("Retained message", f"FAILED ({e})")

    close(c)

if __name__ == "__main__":
    main()

Rode com os seus dados de conexão:

Rodar a sonda
pip install "paho-mqtt>=2.0"
MQTT_HOST=broker.connectmq.com MQTT_PORT=8883 \
MQTT_USER="$MQTT_USER" MQTT_PASS="$MQTT_PASS" python probe.py

Os valores de QoS concedida aparecem como objetos ReasonCode do paho: Granted QoS 2 significa que o broker aceitou a assinatura nesse nível, enquanto um código 0x80 ou maior (por exemplo Not authorized ou Shared Subscriptions not supported) é uma recusa. Se a assinatura compartilhada for aceita e você quiser saber se as mensagens são realmente balanceadas, use o teste de assinaturas compartilhadas.

Como ler o relatório

Achados típicos e o que fazer com eles:

  • MQTT 5 recusado, 3.1.1 OK — versão antiga do broker ou um listener limitado ao 3.1.1. Seu cliente MQTT 5 precisa fazer fallback, ou o broker precisa ser atualizado. Veja MQTT 5 vs 3.1.1.
  • MaximumQoS 1 — comum em serviços de IoT na nuvem e em brokers configurados para throughput. Uma publicação QoS 2 vai desconectar seu cliente com 0x9B; projete para QoS 1 com handlers idempotentes.
  • Assinatura concedida com QoS menor que a solicitada — isso é permitido. O broker rebaixa em vez de recusar, e as mensagens chegam no nível concedido. Verifique a QoS concedida no seu cliente, não só a solicitada.
  • RetainAvailable 0 ou mensagem retida não entregue — dispositivos que dependem de estado retido (último valor ao assinar) vão parecer quebrados. Detalhes em retain não suportado.
  • TopicAliasMaximum 0 — o broker não aceita topic aliases dos clientes; bibliotecas que atribuem aliases automaticamente precisam desligar isso.
  • MaximumPacketSize definido — compare com o seu maior payload mais topic e propriedades usando a calculadora de tamanho de pacote.
  • Not authorized em uma assinatura — não é recurso faltando, é ACL. Rode a sonda com o usuário dos seus dispositivos e veja testar permissões de ACL no MQTT.

A mesma verificação em JavaScript

Com o MQTT.js 5, as propriedades do CONNACK estão no pacote passado ao evento connect:

connack.mjs
import mqtt from 'mqtt';

const client = mqtt.connect('mqtts://broker.connectmq.com:8883', {
  protocolVersion: 5,
  username: process.env.MQTT_USER,
  password: process.env.MQTT_PASS,
});

client.on('connect', (connack) => {
  // Absent keys mean the spec default applies
  console.log(connack.properties ?? '(no properties: all defaults)');
  client.subscribe('$share/probe/testmqtt/probe/js', { qos: 2 }, (err, granted) => {
    console.log(err ?? granted); // granted[0].qos is the granted QoS or a reason code
    client.end();
  });
});

client.on('error', (err) => console.error('connect failed:', err.message));

Aqui os nomes das propriedades estão em camelCase: maximumQoS, retainAvailable, maximumPacketSize, topicAliasMaximum, wildcardSubscriptionAvailable, sharedSubscriptionAvailable, subscriptionIdentifiersAvailable, receiveMaximum e serverKeepAlive.

Sem código? Use o mqtt test

A HiveMQ MQTT CLI já tem essa sonda embutida: mqtt test -h broker.connectmq.com imprime as capacidades do MQTT 3 e do MQTT 5, incluindo tamanho máximo de topic e de payload medidos na prática. O guia do mqtt-cli explica a saída. Para uma checagem manual rápida, conecte com o cliente MQTT online usando MQTT 5 e teste uma publicação retida e uma assinatura $share/.

Coloque no seu pipeline

Capacidades mudam sem aviso: uma atualização do broker, um listener novo, outro plano num serviço gerenciado. Salve a saída da sonda no repositório e compare no CI:

Quebrar o build quando as capacidades mudarem
python probe.py | sed -E 's#testmqtt/probe/[0-9a-f]+#testmqtt/probe/X#' > capabilities.txt
git diff --exit-code capabilities.txt

O sed remove o sufixo aleatório do topic para que só mudanças reais apareçam no diff. Junte isso às verificações de como testar um broker MQTT e você vai saber de um recurso faltando antes de um dispositivo em campo.

Veja os topics da sonda no navegador

Uma sonda de cinco minutos vale mais que uma tarde depurando desconexões "aleatórias". Leia o CONNACK, teste cada recurso de que você depende e mantenha o relatório no controle de versão, para que diferenças entre brokers e ambientes nunca sejam surpresa.

Perguntas frequentes

Como saber se um broker suporta MQTT 5?

Conecte com a versão de protocolo 5. Um broker que suporta responde com um CONNACK do MQTT 5, geralmente com propriedades. Um que não suporta retorna 'unacceptable protocol version' (0x01 no 3.1.1, 0x84 no MQTT 5) ou fecha a conexão.

Se uma propriedade não vem no CONNACK, o recurso não é suportado?

Não, é o contrário. No MQTT 5, uma propriedade ausente significa que vale o padrão: retain, wildcards, assinaturas compartilhadas e identificadores de assinatura estão disponíveis, a QoS máxima é 2 e o receive maximum é 65.535. Brokers só enviam as propriedades que restringem ou alteram esses padrões.

Um broker pode suportar um recurso mas negá-lo ao meu cliente?

Sim. ACLs, configurações por listener e planos de serviços gerenciados podem restringir QoS, retain, wildcards ou topics para usuários específicos. Por isso a sonda deve rodar com as mesmas credenciais e a mesma porta dos seus dispositivos.