Pruebas de brokers

Prueba qué funciones de MQTT soporta un broker (MQTT 5, QoS 2, retain)

Comprueba las capacidades reales de un broker MQTT: lee el CONNACK y verifica QoS máxima, retain, wildcards y suscripciones compartidas con Python.

Actualizado · 9 min de lectura

Prueba qué funciones de MQTT soporta un broker (MQTT 5, QoS 2, retain)

La documentación y las tablas comparativas te dicen lo que un broker puede hacer. Lo que importa es lo que tu broker, con tu configuración y tus credenciales, permite de verdad. La forma más rápida y fiable de saberlo es sondearlo: conectarte con MQTT 5, leer las propiedades del CONNACK y después probar cada función y mirar los reason codes que devuelve.

Este artículo explica qué señales revisar y te da un script en Python (paho-mqtt 2.x) que imprime un informe de capacidades para cualquier broker.

Lo que un broker te dice en el CONNACK

Un broker MQTT 5 anuncia sus límites en el paquete CONNACK. La regla importante: una propiedad solo se envía cuando difiere del valor por defecto. Que falte RetainAvailable no significa «desconocido», significa «sí».

Propiedad del CONNACKValor por defecto si faltaQué se rompe si está restringida
Maximum QoS2Publicar por encima provoca 0x9B QoS not supported y se cierra la conexión
Retain AvailablesíLas publicaciones retenidas reciben 0x9A Retain not supported
Wildcard Subscription AvailablesíLas suscripciones con + / # reciben 0xA2 Wildcard Subscriptions not supported
Shared Subscription AvailablesíLas suscripciones $share/... reciben 0x9E Shared Subscriptions not supported
Subscription Identifiers AvailablesíLas suscripciones con identificador reciben 0xA1
Receive Maximum65.535Cuántos mensajes QoS 1/2 puedes tener en vuelo hacia el broker
Maximum Packet Sizesin límite (máximo del protocolo)Los paquetes más grandes reciben 0x95 Packet too large
Topic Alias Maximum0 (no se aceptan alias)Usar un alias que el broker no permitió es 0x94 Topic Alias invalid
Server Keep Alivetu valorEl broker sobrescribe tu keep-alive; tu cliente debe usar su valor
Assigned Client Identifier—Se envía cuando te conectas con un client ID vacío

MQTT 3.1.1 no tiene nada de esto. Un broker 3.1.1 solo te dice si aceptó la conexión, así que todo hay que comprobarlo probándolo. Puedes consultar cualquiera de estos códigos en la referencia de reason codes.

El script de sondeo

El script siguiente ejecuta seis comprobaciones: versiones del protocolo, propiedades del CONNACK, QoS concedida por suscripción, suscripciones con wildcard y compartidas, mensajes retenidos y una entrega QoS 2 de extremo a extremo. Configúralo con variables de entorno y ejecútalo contra cada entorno que te importe.

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()

Ejecútalo con tus datos de conexión:

Ejecutar la 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

Los valores de QoS concedida se imprimen como objetos ReasonCode de paho: Granted QoS 2 significa que el broker aceptó la suscripción en ese nivel, mientras que un código 0x80 o superior (por ejemplo Not authorized o Shared Subscriptions not supported) es un rechazo. Si la suscripción compartida se acepta pero quieres saber si los mensajes se reparten de verdad, usa el test de suscripciones compartidas.

Cómo leer el informe

Hallazgos típicos y qué hacer con ellos:

  • MQTT 5 rechazado, 3.1.1 OK: versión antigua del broker o un listener limitado a 3.1.1. Tu cliente MQTT 5 debe hacer fallback, o hay que actualizar el broker. Consulta MQTT 5 vs 3.1.1.
  • MaximumQoS 1: habitual en servicios IoT en la nube y en brokers configurados para rendimiento. Una publicación QoS 2 desconectará tu cliente con 0x9B; diseña para QoS 1 con handlers idempotentes.
  • Suscripción concedida con una QoS menor que la solicitada: es legal. El broker rebaja en lugar de rechazar, y los mensajes llegan con el nivel concedido. Comprueba la QoS concedida en tu cliente, no solo la solicitada.
  • RetainAvailable 0 o mensaje retenido no entregado: los dispositivos que dependen del estado retenido (último valor al suscribirse) parecerán rotos. Detalles en retain no soportado.
  • TopicAliasMaximum 0: el broker no acepta topic aliases de los clientes; las librerías que asignan alias automáticamente deben desactivarlo.
  • MaximumPacketSize definido: compáralo con tu payload más grande más el topic y las propiedades usando la calculadora de tamaño de paquete.
  • Not authorized en una suscripción: no falta una función, es una ACL. Ejecuta la sonda con el usuario de tus dispositivos y consulta probar permisos ACL de MQTT.

La misma comprobación en JavaScript

Con MQTT.js 5, las propiedades del CONNACK están en el paquete que recibe el 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));

Aquí los nombres de las propiedades van en camelCase: maximumQoS, retainAvailable, maximumPacketSize, topicAliasMaximum, wildcardSubscriptionAvailable, sharedSubscriptionAvailable, subscriptionIdentifiersAvailable, receiveMaximum y serverKeepAlive.

¿Sin código? Usa mqtt test

La HiveMQ MQTT CLI trae esta sonda integrada: mqtt test -h broker.connectmq.com imprime las capacidades de MQTT 3 y MQTT 5, incluida la longitud máxima de topic y el tamaño de payload medidos por prueba. La guía de mqtt-cli explica la salida. Para una comprobación manual rápida, conéctate con el cliente MQTT online usando MQTT 5 y prueba una publicación retenida y una suscripción $share/.

Inclúyelo en tu pipeline

Las capacidades cambian sin avisar: una actualización del broker, un listener nuevo, otro plan en un servicio gestionado. Guarda la salida de la sonda en tu repositorio y compárala en CI:

Hacer fallar la build si cambian las capacidades
python probe.py | sed -E 's#testmqtt/probe/[0-9a-f]+#testmqtt/probe/X#' > capabilities.txt
git diff --exit-code capabilities.txt

El sed elimina el sufijo aleatorio del topic para que en el diff solo aparezcan cambios reales. Combínalo con las comprobaciones de cómo probar un broker MQTT y sabrás que falta una función antes de que lo descubra un dispositivo en campo.

Mira los topics de la sonda en el navegador

Una sonda de cinco minutos vale más que una tarde depurando desconexiones «aleatorias». Lee el CONNACK, prueba cada función de la que dependes y guarda el informe en control de versiones para que las diferencias entre brokers y entornos nunca sean una sorpresa.

Preguntas frecuentes

¿Cómo sé si un broker soporta MQTT 5?

Conéctate con la versión de protocolo 5. Un broker que la soporta responde con un CONNACK de MQTT 5, normalmente con propiedades. Uno que no la soporta devuelve 'unacceptable protocol version' (0x01 en 3.1.1, 0x84 en MQTT 5) o cierra la conexión.

Si falta una propiedad en el CONNACK, ¿la función no está soportada?

No, al contrario. En MQTT 5, una propiedad ausente significa que se aplica el valor por defecto: retain, wildcards, suscripciones compartidas e identificadores de suscripción están disponibles, la QoS máxima es 2 y receive maximum es 65.535. Los brokers solo envían las propiedades que restringen o cambian esos valores.

¿Puede un broker soportar una función pero negársela a mi cliente?

Sí. Las ACLs, la configuración por listener y los planes de los servicios gestionados pueden restringir QoS, retain, wildcards o topics para usuarios concretos. Por eso la sonda debe ejecutarse con las mismas credenciales y el mismo puerto que tus dispositivos.