9.18. MQTT, octet par octet

À ce stade, la caméra dispose de tous les éléments nécessaires pour communiquer avec un véritable service sur l’internet ouvert : une socket TCP, TLS pour la chiffrer, DNS pour nommer le pair, et asyncio pour permettre au même script d’effectuer d’autres tâches pendant que la connexion est ouverte. MQTT est le premier protocole filaire qui réunit tous ces éléments en quelque chose qu’un produit déployé utilise réellement.

Cette page traite du protocole lui-même – le format sur le fil, les rôles de chaque participant et les compromis de sa conception – avec suffisamment de franchise pour que le client mqtt fourni apparaisse comme un habillage évident de ce qui est déjà connu plutôt que comme un acte de foi.

9.18.1. Pub/sub contre requête/réponse

HTTP – le protocole vers lequel la plupart des projets de caméra se tournent en premier – fonctionne en requête/réponse. Un client demande une ressource précise à un serveur précis ; le serveur répond. Chaque échange est un-à-un, et les deux extrémités connaissent l’adresse de l’autre à l’avance.

MQTT fonctionne en publication/abonnement (publish/subscribe). Les clients se connectent à un tiers intermédiaire appelé broker. Un éditeur (publisher) envoie un message à un topic nommé sans savoir ni se soucier de qui écoute. Un abonné (subscriber) indique au broker quels topics il souhaite et reçoit ensuite chaque message publié sur ces topics. Le broker assure la diffusion : une seule publication sur yard-cam/motion atteint tous les appareils abonnés à yard-cam/motion, qu’ils soient zéro, un ou cinquante.

Trois conséquences découlent de ce changement de modèle :

  • Découplage. Les éditeurs n’ont pas à savoir que des abonnés existent. Les abonnés peuvent apparaître et disparaître sans que l’éditeur s’en aperçoive. Ajouter un second tableau de bord ne demande qu’une ligne de code sur le nouveau tableau de bord ; la caméra ne change pas.

  • Diffusion. Le broker gère chaque duplicata, de sorte que la caméra envoie un seul paquet quel que soit le nombre d’appareils qui le lisent. C’est le cas d’usage pour lequel MQTT a été conçu.

  • Asymétrie. Le broker est désormais un élément d’infrastructure indispensable – sans lui, le protocole ne fonctionne pas. Pour les projets domestiques, il s’agit généralement d’un broker public gratuit (test.mosquitto.org, broker.hivemq.com) ou d’un petit broker que vous exécutez vous-même.

Une caméra publiant sur un topic yard-cam/motion d'un broker tandis que deux tableaux de bord de navigateur et un archiveur cloud reçoivent chacun le même message.

9.18.2. Topics

Les topics sont des chaînes séparées par des barres obliques. La convention est : le plus général à gauche, le plus spécifique à droite:

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

Deux caractères génériques fonctionnent dans les abonnements (mais pas dans les publications) :

  • + correspond à un seul niveau. +/motion s’abonne au topic motion de chaque caméra ; yard-cam/+ s’abonne à chaque sous-topic de yard-cam.

  • # correspond à un ou plusieurs niveaux terminaux. yard-cam/# s’abonne à yard-cam/motion, yard-cam/temperature, yard-cam/temperature/sensor-3 et tout ce qui se trouve sous yard-cam/. Il doit apparaître à la fin de l’abonnement.

Les chaînes de topic sont sensibles à la casse. Selon la spécification, un $ en tête marque des topics internes au broker ($SYS/...) sur lesquels les éditeurs ne devraient pas écrire.

9.18.3. Le format des paquets

MQTT fonctionne au-dessus de TCP. Chaque paquet de contrôle commence par un en-tête fixe d’un octet suivi d’un champ Remaining Length de longueur variable, puis d’un en-tête variable spécifique au type de paquet, puis de la charge utile. Le même format externe couvre toutes les commandes – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT et les autres – ce qui explique pourquoi un client MQTT peut être écrit en quelques centaines de lignes.

La disposition des octets d'un paquet PUBLISH MQTT montrant l'octet de type et de drapeaux de l'en-tête fixe, le champ Remaining Length de longueur variable, le nom du topic, l'identifiant de paquet optionnel et les octets de la charge utile.

L’en-tête fixe tient sur un octet :

  • Les bits 7..4 sont le type de paquet de contrôle. 0x3 est PUBLISH (le premier octet commence donc généralement par 0x3?). 0x1 est CONNECT, 0x2 CONNACK, 0x8 SUBSCRIBE, 0xC PINGREQ, 0xE DISCONNECT, etc.

  • Les bits 3..0 sont des drapeaux spécifiques au type de paquet. Pour PUBLISH, les drapeaux encodent le drapeau de retransmission DUP, le niveau QoS (2 bits) et le drapeau RETAIN.

Le Remaining Length est un entier de longueur variable sur 1 à 4 octets qui compte chaque octet qui le suit. Le bit de poids fort de chaque octet est un marqueur de continuation – 1 signifie « un autre octet de longueur suit », 0 signifie « c’est le dernier ». Une longueur inférieure à 128 tient sur un octet ; les charges utiles plus grandes en utilisent davantage. La longueur encodée maximale est de 256 Mio.

Pour un PUBLISH, l’en-tête variable est le nom du topic – une longueur sur 2 octets, puis des octets UTF-8 – suivi d’un identifiant de paquet sur 2 octets qui n’existe que lorsque le QoS vaut 1 ou 2. Les octets restants constituent la charge utile, traitée comme des octets opaques par le protocole.

Un PUBLISH minimal en QoS 0 de ok vers a/b est:

30 07 00 03 'a' '/' 'b' 'o' 'k'
  • 30 – PUBLISH, tous les drapeaux à zéro.

  • 07 – 7 octets suivent.

  • 00 03 – longueur du topic : 3.

  • 'a' '/' 'b' – topic.

  • 'o' 'k' – charge utile.

Neuf octets sur le fil et le message parvient à chaque abonné à a/b sur le broker.

9.18.4. Niveaux de QoS

La qualité de service (Quality-of-Service) contrôle l’effort que le broker (et le client) déploient pour garantir la livraison. Les trois niveaux :

QoS 0 – au plus une fois. Envoyer et oublier. Le paquet PUBLISH est envoyé et jamais confirmé. Si TCP le livre, le broker le retransmet. Si la connexion tombe en cours d’envoi, le message est perdu. La plupart de la télémétrie de capteurs convient très bien au QoS 0 – une seule lecture de température manquée dans un flux qui en émet toutes les 30 secondes n’a aucune importance.

QoS 1 – au moins une fois. L’éditeur inclut un identifiant de paquet et attend un PUBACK. Si aucun PUBACK n’arrive avant l’expiration d’un délai, l’éditeur retransmet avec le drapeau DUP activé. Le broker peut finir par livrer deux fois le même message à un abonné sur le même niveau ; l’abonné doit être prêt à gérer les doublons.

QoS 2 – exactement une fois. Une poignée de main en quatre étapes (PUBREC / PUBREL / PUBCOMP) garantit que le message arrive exactement une fois, même en cas de reconnexion. Coûteux en allers-retours et en état du broker. Peu d’applications de caméra en ont besoin.

Le client mqtt fourni implémente le QoS 0 et le QoS 1 ; le QoS 2 déclenche une erreur si vous le demandez. Pour une caméra qui transmet des lectures de capteurs, le QoS 0 est presque toujours la bonne réponse.

9.18.5. Messages retenus et dernière volonté

Deux fonctionnalités méritent d’être connues car elles modifient ce que le broker retient à propos de votre topic.

RETAIN. Si un PUBLISH a le drapeau RETAIN activé, le broker stocke le message et le retransmet à chaque futur abonné dès qu’il s’abonne. C’est ainsi que MQTT répond à la question « quelle est la valeur actuelle ? » – un capteur publie sa dernière lecture en mode retenu, et un tableau de bord qui s’abonne dix minutes plus tard reçoit tout de même la valeur la plus récente au lieu d’attendre la prochaine publication. Republier sur le même topic écrase la valeur retenue ; publier une charge utile vide l’efface.

Dernière volonté (last will). Lorsqu’un client se connecte, il peut confier au broker un « testament et dernières volontés » : un topic, une charge utile, un QoS et un drapeau retain. Si ce client se déconnecte de manière non propre – TCP RESET, coupure de courant, perte de réseau sans paquet DISCONNECT – le broker publie la dernière volonté pour le compte du client. Les abonnés y voient la notification que la caméra est passée hors ligne. La caméra elle-même n’envoie jamais sa dernière volonté ; c’est le broker qui le fait, car à ce moment-là la caméra a disparu.

9.18.6. Keepalive et reconnexion

CONNECT transporte un intervalle de keepalive en secondes. Si le client est resté silencieux pendant ce laps de temps, le broker le considère comme mort. Pour éviter cela, le client envoie périodiquement un PINGREQ (un octet : 0xC0) et reçoit en retour un PINGRESP (0xD0) – le battement de cœur le plus petit et le moins coûteux que le protocole puisse transporter. La plupart des applications de caméra fixent le keepalive à 30 ou 60 secondes.

Si la connexion TCP tombe, les deux côtés s’en aperçoivent et se reconnectent à partir de zéro. Les abonnements effectués avant la coupure sont perdus, à moins que le client n’ait utilisé une session persistante lors de la connexion ; pour des applications de caméra simples, le schéma de réabonnement à la reconnexion est plus court et tout aussi efficace.

C’est suffisant pour lire la spécification MQTT ou écrire soi-même un client au-dessus d’une socket.socket. Le client fourni dans mqtt fait exactement cela, plus une API sensée pour le code applicatif.