Documentation pour le client eMQTT5

Introduction

eMQTT5 est un client MQTT pour le protocole version 5.0 écrit en C++.

Son objectif principal est de fournir un client léger en termes de taille du binaire, d'empreinte mémoire vive et de taille de code source, qui implémente la norme autant que possible.

Il est multiplateforme et prévoit l'écriture d'une couche d'abstraction matérielle (HAL) pour la communication réseau. Deux exemples d'implémentation de HAL sont fournis dans la base de code, l'un utilisant l'API socket de BSD (avec la bibliothèque mbed_tls pour le cryptage TLS) et l'autre utilisant un code haute performance (mais moins léger) dérivé de ClassPath.

Concepts

Le MQTT est en gros un système de messagerie où les paquets sont formatés selon un protocole de sérialisation spécifique. Vous ne pouvez rien envoyer via MQTT, sauf si vous appliquez les règles de sérialisation spécifiques à MQTT. Le protocole définit 16 types de paquets et le séquençage de leur génération dans une communication.

Cette bibliothèque effectue le travail de bas niveau consistant à générer les paquets comme prévus par la norme, et à respecter, autant que possible, la séquence attendue pour chaque paquet.

Elle permet d'abstraire la complexité du protocole (comme la connexion initiale avec authentification, la gestion de la qualité de service lors de la publication et de la réception des paquets, la sérialisation de la charge utile et bien d'autres).

Du point de vue de votre application, vous devrez généralement faire :

  1. Vous connecter au courtier MQTT (opt. s'authentifier)
  2. S'abonnez à certains sujets
  3. Publier quelques messages
  4. Faites tourner la boucle de l'événement aussi longtemps que vous souhaitez recevoir des paquets sur les sujets surveillés
  5. Se déconnecter du serveur

Pas de copie mémoire

Un grand soin a été apporté à éviter de copier les données. Cela signifie que l'utilisation de la mémoire vive sera minimale puisque vos données seront utilisées telles quelles et que vous pourrez lire directement les paquets reçus. La bibliothèque utilise le concept de View (comme en C++ string_view) sur vos données lorsqu'il est possible de le faire.

Utilisation des propriétés avec le client

Comme cette bibliothèque est orientée vers une utilisation embarquée, un grand soin a été apporté pour éviter l'utilisation du tas (heap) et minimiser la taille du code.

Paquet à destination du courtier

Afin d'atteindre ces objectifs, la classe Properties est une liste chaînée où chaque noeud stocke un drapeau indiquant s'il a été alloué sur la pile (par défaut) ou sur le tas.

Lorsque la liste chaînée est détruite, chaque noeud se suicide (se delete s'il était alloué sur le tas) ou enchaîne la demande de destruction au noeud suivant.

L'analyse de la liste chaînée se fait de manière récursive, mais cela ne devrait pas poser de problème puisque le nombre de propriétés possibles pour chaque paquet est faible.

L'ajout de propriétés se fait de cette manière :

    Property<uint32> maxProp(PacketSizeMax, recvBufferSize) ;

    if (!packet.props.getProperty(PacketSizeMax))
        packet.props.append(&maxProp) ; // C'est possible avec une propriété sur la pile tant que la durée de vie de l'objet dépasse celle du paquet

Pas d'allocation de mémoire

Sauf dans des cas très spécifiques de votre part, aucune allocation de mémoire n'est faite dans la bibliothèque, et aucune mémoire n'est allouée dans la boucle d'événements. Tout est pré-alloué lors de la construction et n'évolue jamais. Cela signifie également qu'il n'y a pas de fragmentation de la mémoire du tas, de sorte que votre système peut fonctionner pendant des mois sans jamais devoir être réinitialisé en raison d'un échec de l'allocation.

Bibliothèque de typage fort et pas de duplication de code

Les template C++ sont utilisés dans la mesure du possible pour limiter la taille du code de la bibliothèque. L'utilisation des template est très simple (pas besoin d'être un gourou du C++ pour comprendre comment sont utilisés le template dans la bibliothèque). L'héritage est utilisé pour éviter le template-bloat (un grand soin a été apporté à l'orthogonisation du modèle commun, chaque symbole est vérifié pour ne pas partager un byte-code commun avec un autre). Il n'y a pas de besoin de vtable, le linker les élimine dans le binaire final.

Modèle de visiteur pour les propriétés

Contrairement à MQTT version 3.11, dans MQTT version 5, chaque paquet peut stocker des propriétés pour ajouter des fonctionnalités optionnelles. Les clients habituels MQTT v5.0 copient la valeur donnée dans le tableau des propriétés du paquet. eMQTT5 évite cette copie, à la place, vous utiliserez simplement un visiteur pour obtenir une vue sur la propriété. C'est à la fois plus rapide et plus léger pour vos ressources système.

Réception des paquets du courtier

Lors de la réception d'un paquet, le code ne fait jamais de copie, donc la sérialisation à partir des propriétés se fait directement à partir du tampon reçu.

Dans ce cas, vous aurez affaire à la classe PropertiesView et plus particulièrement à sa méthode getProperty :

bool getProperty(VisitorVariant & visitor, PropertyType & type, uint32 & offset) const

En général, vous créez une instance de type VisitorVariant, une instance de type PropertyType et un offset (utilisé en lors de la visite pour se positionner sur la prochaine propriété), puis vous appelez getProperty. Cette méthode permet de remplir chaque instance avec le visiteur et le type appropriés. L'offset est augmenté jusqu'à la position suivante de la propriété dans le tampon observé (reçu).

C'est ensuite à vous de vérifier quelle propriété vous intéresse, et d'extraire la valeur de la propriété visitée comme ceci :

    PropertyType type = BadProperty;
    uint32 offset = 0;
    VisitorVariant visitor;
    while (packet.props.getProperty(visitor, type, offset))
    {
        switch (type)
        {
            case PacketSizeMax:
            {
                auto pod = visitor.as< LittleEndianPODVisitor<uint32> >();
                maxPacketSize = pod->getValue();
                break;
            }
   [...]

Interface du client

Le client fournit tout ce qui est nécessaire à la réalisation des actions ci-dessus. Vous allez instancier un objet Network::Client::MQTTv5 et appeler l'une des méthodes ci-dessous :

Interface MessageReceived

L'interface "MessageReceived" qui doit être surchargée :

struct MessageReceived
{
    // This is called upon published message reception.
    virtual void messageReceived(const DynamicStringView & topic, 
                                 const DynamicBinDataView & payload, 
                                 const uint16 packetIdentifier, const PropertiesView & properties) = 0;
    // This is usually called upon creation to know what it the maximum packet size you'll support.
    // By default, MQTT allows up to 256MB control packets. 
    // On embedded system, this is very unlikely to be supported. 
    // This implementation does not support streaming so a buffer is created on the heap with this size  
    // upon client construction to store the received control packet. 
    virtual uint32 maxPacketSize() const; // Default to 2048 bytes

    // An authentication packet was received. 
    virtual void authReceived(const ReasonCodes reasonCode, const DynamicStringView & authMethod, const DynamicBinDataView & authData, const PropertiesView & properties) { } // Optionnal: Depends on library configuration
};
MessageReceived::messageReceived
Paramètre Utilisation
topic Le sujet de ce message
payload La charge utile (éventuellement vide) de ce message
packetIdentifier Si non zéro, contient l'identifiant du paquet. Celui-ci est généralement ignoré
properties Si elles sont jointes au paquet, vous en trouverez la liste ici.
MessageReceived::authReceived

Si cette fonction est activée dans la configuration de la bibliothèque et que vous utilisez une authentification avancée pour votre client, elle sera appelée lors de la connexion au courtier, avec un challenge à relever. Vous appellerez alors la méthode MQTTv5::auth pour soumettre votre réponse.

Par défaut, aucune action n'est effectuée sur les paquets d'authentification. C'est à vous d'implémenter ces paquets

Paramètres Utilisation
reasonCode L'un de: Success, ContinueAuthentication, ReAuthenticate
authMethod La méthode d'authentification
authData Les données d'authentification
properties Si elles sont jointes au paquet, vous en trouverez la liste ici.

MQTTv5(...)

Signature

MQTTv5(const char * clientID, 
       MessageReceived * callback, 
       const DynamicBinDataView * brokerCert = 0)
Paramètres Utilisation
clientID Un pointeur vers une chaîne terminée par un zéro contenant un ID de client. Peut être nullptr pour l'ID client attribué par le courtier
callback Un pointeur obligatoire vers une instance "MessageReceived" (voir ci-dessous)
brokerCert Un pointeur optionnel vers le certificat du courtier codé en DER

Certificat de courtier

Il n'est généralement pas possible de stocker une chaîne complète d'autorités de certification dans un système intégré. Et même si cela était possible, la mise à jour et la maintenance d'une telle bibliothèque seraient assez pénibles. C'est pourquoi le client propose de stocker directement le certificat du courtier attendu et de ne le valider que par rapport à celui-ci.

L'utilisation de DER pour stocker un certificat réduit la taille binaire flash requise de votre certificat d'environ 33 % (par rapport au format textuel X509 ordinaire).

Aucune copie n'est faite à partir du pointeur donné, donc veuillez vous assurer que les données pointées sont valides tant que ce client est valide.

Si vous n'avez pas de certificat codé PEM, utilisez cette commande pour enregistrer le certificat du serveur de courtage dans un fichier .PEM

$ openssl s_client -servername your.server.com -connect your.server.com:8883 2>/dev/null | openssl x509 > cert.pem

Si vous avez un certificat codé PEM, utilisez ce code pour le convertir au format DER (33% plus petit)

$ openssl x509 -in cert.pem -outform der -out cert.der

connectTo(...)

Signature

ErrorType connectTo(
     // Broker configuration
     const char * serverHost, 
     const uint16 port, 
     bool useTLS = false,

     // Session configuration
     const uint16 keepAliveTimeInSec = 300,                                      
     const bool cleanStart = true,

     // Credentials
     const char * userName = nullptr, 
     const DynamicBinDataView * password = nullptr,

     // Last will message
     WillMessage * willMessage = nullptr, 
     const QoSDelivery willQoS = QoSDelivery::AtMostOne, 
     const bool willRetain = false,

     // Additional properties
     Properties * properties = nullptr);
Paramètres Utilisation
serverHost Le nom d'hôte du serveur auquel se connecter
port Le port 16 bits du courtier auquel se connecter
useTLS Si c'est le cas, une connexion TLS sera tentée. Vous pouvez spécifier le certificat de courtier attendu dans le constructeur MQTTClient
keepAliveTimeInSec Le délai avant que la connexion ne soit considérée comme perdue
cleanStart Si faux, le courtier reprend une session précédente sinon il démarre d'une session vide
userName L'identifiant de l'utilisateur, vide si vous utilisez le mode AUTH
password Mot de passe de l'utilisateur
willMessage En cas de déconnexion inattendue, définissez un message pour vos dernière volonté ici
willQoS Le niveau de qualité de service de ce message
willRetain Si activé, le courtier conserve le message de dernière volonté sur les sujets afin qu'il soit envoyé lors de l'inscription du client
properties Les propriétés à intégrer dans le paquet de connexion

Authentification

L'authentification dans MQTT v3.1.1 était principalement basée soit sur l'identifiant du client, soit sur le nom d'utilisateur/mot de passe, un peu comme le faisait HTTP/1.0. Dans MQTT v5.0, il est maintenant possible d'avoir une authentification à plusieurs étapes avec un protocole spécifique par courtier/par client. L'authentification nécessite un nouveau type de paquet de contrôle, et, à ce titre, vous pouvez utiliser la méthode d'authentification du client pour construire ce paquet.

Le processus habituel d'authentification est le suivant :

  1. Client => CONNECT avec quelques propriétés d'authentification attachées => Serveur
  2. Serveur => AUTH avec quelques défis/méthodes/données => Client
  3. Client => AUTH avec quelques réponses/données => Serveur
  4. Serveur => CONNACK => Client

Pour la première étape, vous devrez ajouter autant de propriétés que nécessaire (voir la section Propriétés ci-dessous pour savoir comment faire) lors de votre premier appel à connectTo .

Lorsque le serveur répondra (à l'étape 2), votre instance de rappel sera appelée avec la méthode d'authentification et les données déjà analysées pour vous.

Vous utiliserez ensuite la méthode d'authentification à l'étape 3 pour terminer le défi, la méthode revient avec un succès (étape 4) ou un échec.

Veuillez noter qu'aucun des éléments ci-dessus n'est requis pour l'identification du client habituel ou la connexion par nom d'utilisateur / mot de passe.

Si cette option est activée dans la configuration de la bibliothèque, vous pourrez préparer le paquet AUTH à envoyer au courtier en réponse à un défi d'authentification via la méthode auth :

auth(...)

Signature

ErrorType auth(const ReasonCodes reasonCode, 
               const DynamicStringView & authMethod, 
               const DynamicBinDataView & authData, 
               Properties * properties = nullptr)
Paramètres Utilisation
reasonCode Un de: Success, ContinueAuthentication, ReAuthenticate
authMethod La méthode d'authentification
authData Les données d'authentification
properties Si vous avez besoin d'en joindre au paquet, faites-le ici.

subscribe(...)

Signature

ErrorType subscribe(
    const char * topic, 
    const RetainHandling retainHandling = RetainHandling::GetRetainedMessageForNewSubscriptionOnly, 
    const bool withAutoFeedBack = false, 
    const QoSDelivery maxAcceptedQoS = QoSDelivery::ExactlyOne, 
    const bool retainAsPublished = true, 
    Properties * properties = nullptr)
Paramètres Utilisation
topic Le sujet auquel il faut s'abonner. Il peut s'agir d'un filtre sous la forme "a/b/préfixe*" (le préfixe peut aussi être manquant)
retainHandling La politique de traitement des messages retenus (généralement, la manière dont vous souhaitez recevoir un message retenu pour le sujet)
withAutoFeedback Si c'est vrai, si vous publiez sur ce sujet, vous recevrez également votre message
maxAcceptedQoS La qualité de service maximale acceptée. Le client gère n'importe quel niveau, vous pouvez donc l'ignorer en toute sécurité.
retainAsPublished Si c'est le cas, le drapeau "Retain" du message de publication sera conservé dans le paquet envoyé lors de l'abonnement (ce qui est généralement utilisé pour les courtiers par procuration)
properties Si elles sont fournies, ces propriétés seront envoyées avec le paquet d'abonnement. Les propriétés autorisées pour le paquet d'abonnement sont les suivantes (Subscription Identifier, User property)

Vous pouvez appeler cette méthode autant de fois que vous avez de sujets à abonner, ou vous pouvez utiliser l'autre overload subscribe avec une liste chaînée de sujets à s'abonner. Après l'appel de cette méthode, MessageReceived::messageReceived callback sera appelé à la réception d'un message.

publish(...)

Signature

ErrorType publish(
    const char * topic, 
    const uint8 * payload, 
    const uint32 payloadLength, 
    const bool retain = false, 
    const QoSDelivery QoS = QoSDelivery::AtMostOne, 
    const uint16 packetIdentifier = 0, 
    Properties * properties = nullptr)
Paramètres Utilisation
topic Le sujet à publier.
payload La charge utile à envoyer à cette publication, peut être nulle
payloadLength La longueur de la charge utile, en octets
retain Si vrai, le drapeau "Retain" sera collé à ce message et il sera envoyé aux nouveaux abonnés lors de l'inscription
QoS La qualité du service. N'importe laquelle des catégories AtMostOne, At leastOne, ExactlyOne
packetIdentifier Si non zéro, contient l'identifiant du paquet. Celui-ci est généralement ignoré
properties Si elles sont fournies, ces propriétés seront envoyées avec le paquet de publication. Les propriétés autorisées pour le paquet de publication sont les suivantes : (Payload Format Indicator, Message Expiry Interval, Topic Alias, Response topic, Correlation Data, Subscription Identifier, User property, Content Type)

eventLoop()

Signature

ErrorType eventLoop()

MQTTv5 n'a aucune notion de fil ou de tâche. C'est à vous d'appeler la boucle d'événement à intervalles réguliers. L'implémentation sous-jacente se rabat généralement sur les fonctions "select", "epoll" ou "kqueue", de sorte qu'il est possible de l'appeler en boucle sans aucun délai. Le callback MessageReceived::messageReceived sera appelé dans cette boucle et toute gestion de la qualité de service sera également effectuée dans cette boucle.

disconnect(...)

Signature

ErrorType disconnect(const ReasonCodes code, Properties * properties = nullptr)
Paramètres Utilisation
code La raison de la déconnexion.
properties Si elles sont fournies, ces propriétés seront envoyées dans le paquet de déconnexion. Les propriétés autorisées pour le paquet de publication sont : (Session Expiry Interval, Reason String, Server Reference, User property)

Il n'est pas nécessaire de se déconnecter du courtier dans le protocole MQTT puisqu'il y a des timeouts. Cependant, vous pouvez indiquer au courtier le motif de votre déconnexion grâce à cette méthode. Après avoir appelé cette méthode, vous devrez appeler connectTo à nouveau pour communiquer avec le courtier.

setDefaultTimeout(...)

Signature

void setDefaultTimeout(const uint32 timeoutMs)
Paramètres Utilisation
timeoutMs La durée maximum en milliseconde pour attendre.

Par défaut, lorsque le client n'est pas configuré en mode LowLatency, l'eventLoop se bloque pendant une durée configurable, en attendant qu'un paquet soit reçu ou qu'un autre soit envoyé. Ceci est utile pour limiter l'utilisation du CPU puisque pendant cette attente, le timeslice de la tâche est abandonnée pour d'autres travaux sur le CPU. Cependant, si vous avez besoin d'effectuer d'autres travaux dans la même tâche que la boucle d'événement, cela signifie que vous verrez une pause de la durée configurée entre chaque travail.

Lorsque le client est configuré avec LowLatency, l'eventLoop vérifie instantanément s'il y a de l'activité sur la socket, et si aucune activité n'est détectée, elle quitte l'eventLoop immédiatement, sans bloquer. Si vous n'avez pas d'autre travail à effectuer, cela accaparera le CPU à 100% et augmentera la consommation d'énergie du système. Cependant, si vous avez du travail à effectuer, et que ce travail s'auto-bloque (par exemple, en attendant une interruption ou un timer), vous pouvez l'utiliser en toute sécurité.

Article précédent