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.
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 :
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.
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.
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
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.
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.
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.
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;
}
[...]
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 :
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
};
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. |
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. |
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 |
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
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 |
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 :
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
:
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. |
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.
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) |
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.
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.
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é.