Tests de brokers

Tester les fonctionnalités MQTT prises en charge par un broker (MQTT 5, QoS 2, retain)

Sondez les capacités réelles d'un broker MQTT : lisez le CONNACK et vérifiez QoS max, retain, wildcards et abonnements partagés avec un script Python.

Mis à jour · 9 min de lecture

Tester les fonctionnalités MQTT prises en charge par un broker (MQTT 5, QoS 2, retain)

La documentation et les tableaux comparatifs vous disent ce qu'un broker sait faire. Ce qui compte, c'est ce que votre broker, avec votre configuration et vos identifiants, autorise réellement. Le moyen le plus rapide et le plus fiable de le savoir est de le sonder : se connecter en MQTT 5, lire les propriétés du CONNACK, puis essayer chaque fonctionnalité et observer les reason codes renvoyés.

Cet article explique quels signaux vérifier et fournit un script Python (paho-mqtt 2.x) qui affiche un rapport de capacités pour n'importe quel broker.

Ce que le broker annonce dans le CONNACK

Un broker MQTT 5 annonce ses limites dans le paquet CONNACK. La règle essentielle : une propriété n'est envoyée que si elle diffère de la valeur par défaut. L'absence de RetainAvailable ne signifie pas « inconnu », mais « oui ».

Propriété du CONNACKValeur par défaut si absenteCe qui casse si elle est restreinte
Maximum QoS2Publier au-dessus provoque 0x9B QoS not supported et la fermeture de la connexion
Retain AvailableouiLes publications retenues reçoivent 0x9A Retain not supported
Wildcard Subscription AvailableouiLes abonnements + / # reçoivent 0xA2 Wildcard Subscriptions not supported
Shared Subscription AvailableouiLes abonnements $share/... reçoivent 0x9E Shared Subscriptions not supported
Subscription Identifiers AvailableouiLes abonnements avec identifiant reçoivent 0xA1
Receive Maximum65 535Combien de messages QoS 1/2 peuvent être en vol vers le broker
Maximum Packet Sizepas de limite (maximum du protocole)Les paquets plus gros reçoivent 0x95 Packet too large
Topic Alias Maximum0 (alias refusés)Utiliser un alias non autorisé donne 0x94 Topic Alias invalid
Server Keep Alivevotre valeurLe broker remplace votre keep-alive ; votre client doit utiliser sa valeur
Assigned Client Identifier—Envoyé lorsque vous vous connectez avec un client ID vide

MQTT 3.1.1 ne propose rien de tout cela. Un broker 3.1.1 indique seulement si la connexion est acceptée : tout le reste doit être vérifié en essayant. Vous pouvez retrouver chacun de ces codes dans la référence des reason codes.

Le script de sondage

Le script ci-dessous effectue six vérifications : versions du protocole, propriétés du CONNACK, QoS accordée par abonnement, abonnements avec wildcard et partagés, messages retenus et une livraison QoS 2 de bout en bout. Configurez-le avec des variables d'environnement et lancez-le sur chaque environnement qui vous intéresse.

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

Lancez-le avec vos paramètres de connexion :

Lancer la sonde
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

Les QoS accordées s'affichent sous forme d'objets ReasonCode de paho : Granted QoS 2 signifie que le broker a accepté l'abonnement à ce niveau, tandis qu'un code 0x80 ou supérieur (par exemple Not authorized ou Shared Subscriptions not supported) est un refus. Si l'abonnement partagé est accepté et que vous voulez savoir si les messages sont réellement répartis, utilisez le test des abonnements partagés.

Lire le rapport

Constats fréquents, et que faire :

  • MQTT 5 refusé, 3.1.1 OK — ancienne version du broker ou listener limité à 3.1.1. Votre client MQTT 5 doit se replier, ou le broker doit être mis à jour. Voir MQTT 5 vs 3.1.1.
  • MaximumQoS 1 — courant sur les services IoT cloud et sur les brokers configurés pour le débit. Une publication QoS 2 déconnectera votre client avec 0x9B ; concevez pour la QoS 1 avec des handlers idempotents.
  • Abonnement accordé avec une QoS inférieure à celle demandée — c'est autorisé. Le broker rétrograde au lieu de refuser, et les messages arrivent au niveau accordé. Vérifiez la QoS accordée dans votre client, pas seulement celle demandée.
  • RetainAvailable 0 ou message retenu non livré — les appareils qui reposent sur un état retenu (dernière valeur à l'abonnement) sembleront cassés. Détails dans retain non pris en charge.
  • TopicAliasMaximum 0 — le broker n'accepte pas les topic aliases des clients ; les bibliothèques qui attribuent des alias automatiquement doivent désactiver cette option.
  • MaximumPacketSize défini — comparez-le à votre plus gros payload, topic et propriétés compris, avec le calculateur de taille de paquet.
  • Not authorized sur un abonnement — ce n'est pas une fonctionnalité manquante mais une ACL. Lancez la sonde avec l'utilisateur de vos appareils et consultez tester les permissions ACL MQTT.

La même vérification en JavaScript

Avec MQTT.js 5, les propriétés du CONNACK se trouvent dans le paquet transmis à l'événement 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));

Les noms des propriétés sont ici en camelCase : maximumQoS, retainAvailable, maximumPacketSize, topicAliasMaximum, wildcardSubscriptionAvailable, sharedSubscriptionAvailable, subscriptionIdentifiersAvailable, receiveMaximum et serverKeepAlive.

Pas de code ? Utilisez mqtt test

La HiveMQ MQTT CLI intègre cette sonde : mqtt test -h broker.connectmq.com affiche les capacités MQTT 3 et MQTT 5, y compris la longueur maximale de topic et la taille de payload mesurées par essai. Le guide mqtt-cli explique la sortie. Pour une vérification manuelle rapide, connectez-vous avec le client MQTT en ligne en MQTT 5 et essayez une publication retenue et un abonnement $share/.

Intégrez-le à votre pipeline

Les capacités changent sans prévenir : une mise à jour du broker, un nouveau listener, une autre offre sur un service managé. Enregistrez la sortie de la sonde dans votre dépôt et comparez-la en CI :

Faire échouer le build si les capacités changent
python probe.py | sed -E 's#testmqtt/probe/[0-9a-f]+#testmqtt/probe/X#' > capabilities.txt
git diff --exit-code capabilities.txt

Le sed supprime le suffixe aléatoire du topic pour que seuls les vrais changements apparaissent dans le diff. Associez cela aux vérifications de comment tester un broker MQTT et vous saurez qu'une fonctionnalité manque avant qu'un appareil sur le terrain ne le découvre.

Suivre les topics de la sonde dans le navigateur

Une sonde de cinq minutes vaut mieux qu'un après-midi passé à déboguer des déconnexions « aléatoires ». Lisez le CONNACK, testez chaque fonctionnalité dont vous dépendez et versionnez le rapport, pour que les différences entre brokers et environnements ne soient jamais une surprise.

Questions fréquentes

Comment savoir si un broker prend en charge MQTT 5 ?

Connectez-vous avec la version de protocole 5. Un broker compatible répond par un CONNACK MQTT 5, généralement avec des propriétés. Sinon, il renvoie 'unacceptable protocol version' (0x01 en 3.1.1, 0x84 en MQTT 5) ou ferme la connexion.

Si une propriété est absente du CONNACK, la fonctionnalité est-elle non prise en charge ?

Non, c'est l'inverse. En MQTT 5, une propriété absente signifie que la valeur par défaut s'applique : retain, wildcards, abonnements partagés et identifiants d'abonnement sont disponibles, la QoS maximale est 2 et le receive maximum vaut 65 535. Les brokers n'envoient que les propriétés qui restreignent ou modifient ces valeurs.

Un broker peut-il prendre en charge une fonctionnalité mais la refuser à mon client ?

Oui. Les ACLs, les réglages par listener et les offres des services managés peuvent restreindre la QoS, le retain, les wildcards ou certains topics pour des utilisateurs donnés. C'est pourquoi la sonde doit s'exécuter avec les mêmes identifiants et le même port que vos appareils.