Regardez, j'y suis allé aussi. Tout d'abord, le projet imprime simplement des informations de débogage pour un humain dans de belles chaînes descriptives faciles à comprendre. Ensuite, un outil doit enregistrer une valeur de capteur pour que les messages de débogage simples gagnent en structure. Maintenant, vos messages de débogage {{ressemblent à ceci:}}. C'est formidable jusqu'à ce qu'un deuxième capteur soit ajouté qui utilise des flotteurs au lieu des pouces. Maintenant, il y a des arrosages de personnages encore plus magiques entre les accolades. Quelques jours plus tard et les choses commencent à sembler complètes. À un moment donné, vous regardez et réalisez: «J'ai besoin d'une stratégie de sérialisation de la messagerie». Eh bien, vous êtes au bon endroit!

Sérialisation des messages?

La sérialisation des messages porte divers noms comme «marshalling» ou «packing», mais tous relèvent de la déclaration de la structure par laquelle les messages sont assemblés. La sérialisation des messages est la façon dont les données dans la mémoire d'un ordinateur sont converties en une forme sous laquelle elles peuvent être communiquées, de la même manière que le langage traduit les pensées humaines en une forme qui peut être partagée. Une pensée est sérialisée en un message qui peut être envoyé, puis désérialisée lorsqu'elle est reçue pour la transformer en pensée. Très agréable philosophiquement.

La sérialisation des messages est la façon dont les données dans la mémoire d'un ordinateur sont converties sous une forme dans laquelle elles peuvent être communiquées.

La sérialisation est utile à différents endroits. Il est possible d'écrire une structure de données brutes sur le disque pour la sauvegarder, mais si le programmeur souhaite que ces données soient facilement lisibles par d'autres langages et systèmes d'exploitation à l'avenir, il peut vouloir les sérialiser d'une manière ou d'une autre pour lui donner une structure formelle cohérente. Passer des messages entre des threads ou des processus implique probablement une certaine forme de sérialisation, mais peut-être plus limitée. Une application évidente envoie certainement des messages entre ordinateurs sur un réseau ou tout autre support.

Retirons le trou du lapin philosophique et définissons quelques contraintes. (C'est Hackaday après tout, pas un traité par jour). Aux fins de cet article, nous nous intéressons à un cas d'utilisation spécifique: l'échange de messages à partir d'un microcontrôleur. Étant donné la facilité avec laquelle il est possible de connecter un microcontrôleur à n'importe quoi, d'un bus série à Internet, l'étape suivante consiste à donner une forme aux messages à envoyer.

Que rechercher dans un schéma de sérialisation

L'offre infinie de "meilleur!", "Le plus rapide!", "Le plus petit!", "Le moins cher!" les options peuvent conduire à un peu de paralysie décisionnelle. De nombreux paramètres peuvent affecter votre sélection d'un format de sérialisation par rapport à un autre, mais certains sont toujours importants.

La première question à répondre en premier est: schemafull ou schemaless? Les deux stratégies spécifient le format de vos données, c'est le but de la sérialisation du message après tout. Mais ce qui se trouve à l'intérieur de chaque message peut être structuré différemment. Un schéma de sérialisation schemafull spécifie la structure – et à certains égards le contenu – de vos données dans le cadre du protocole. Dans un protocole schemafull, un message SensorReading a toujours le même ensemble de champs, par exemple SensorID (uint32_t), Horodatage (uint32_t) et Reading (float). La manière exacte de coder ces champs en octets est spécifiée, sérialisée selon un protocole, ainsi que leur existence et leur type. Si votre micrologiciel reçoit un SensorReading, il sait exactement de quelle forme et de quelle taille le contenu sera.

Étant donné que la description d'un protocole sans schéma est facile, elle n'a aucune contrainte du tout! JSON est peut-être l'exemple le plus répandu mais il y en a certainement beaucoup d'autres. Les données sont toujours encodées d'une manière prédéfinie mais le contenu exact de chaque message n'est pas spécifié à l'avance. Dans un tel schéma, il n'y a pas de contenu prédéfini pour notre message SensorReading. Peut-être qu'il a un seul flotteur? Peut-être dix entiers? Le récepteur doit vérifier si chaque champ est présent et tenter de le décoder pour obtenir le contenu.

Sous-produits du schéma et du non-schéma

"Mais attendez!" vous demandez: "ces deux méthodes ne fonctionnent-elles pas de la même manière?". Eh bien, oui, en quelque sorte. À certains égards, il s’agit plutôt d’une formalisation de l’accord contractuel entre les participants. Évidemment, pour décoder un message schématique, vous devez essayer de décoder chaque champ individuellement, tout comme dans un système sans schéma. Mais si vous supposez que le format est spécifié à l'avance, d'autres stratégies de décodage, telles que la conversion directe en une structure de données ou le code générant un encodeur et un décodeur, peuvent devenir disponibles.

Si la structure n'est pas votre truc, la flexibilité d'un protocole sans schéma pourrait être de mise.

Il existe également d'autres sous-produits. Souvent, une solution schématique permettra au développeur de spécifier le protocole séparément de l'implémentation, puis d'utiliser un générateur de code pour produire les fonctions de sérialisation et de désérialisation dans le langage requis. Cela peut produire un code spécifique plus efficace, mais plus important encore, il indique clairement au développeur ce qui est communiqué et comment.

Si la structure n'est pas votre truc, alors la flexibilité d'un protocole sans schéma pourrait être de mise. Si les données entrantes doivent être traitées comme une liste blanche, avec une application acceptant uniquement les champs qu’elle recherche, la surcharge de création et de maintenance d’un schéma peut être plus lourde que nécessaire. Assurez-vous simplement que tout le monde est sur la même page concernant ce qui est envoyé et comment.

Lors du choix des bibliothèques pour les microcontrôleurs, l'autre contrainte à connaître est le type d'allocation de mémoire requis. Parfois, il y a une préférence pour éviter complètement les tracas potentiels de l'allocation dynamique, ce qui peut limiter considérablement les choix. Exiger une allocation statique peut forcer le développeur à déformer son code pour gérer les messages de longueur variable, ou forcer des contraintes comme une taille maximale fixe. Mais plus que de forcer un schéma de sérialisation, les exigences de gestion de la mémoire contraignent les bibliothèques disponibles à utiliser.

Prenons quelques instants pour parler des bibliothèques. Il est évidemment possible de fabriquer à la main le protocole et la bibliothèque pour le décoder, en évitant la plupart des problèmes mentionnés ci-dessus et en produisant exactement le code nécessaire pour toutes les contraintes. Parfois, cela a du sens! Mais si vous prévoyez d'étendre le schéma plus d'une ou deux fois et que vous avez besoin qu'il fonctionne avec plusieurs langues ou systèmes, un protocole de sérialisation standard et des bibliothèques standard sont la solution. Un schéma commun offrira de nombreuses options de langues et d'outils sur les systèmes d'exploitation et les plates-formes, qui peuvent tous parler littéralement le même langage. Vous pouvez donc choisir la bibliothèque avec une petite empreinte mémoire et une allocation statique pour le petit microcontrôleur, et la grande dynamique entièrement équipée pour le backend.

Schémas et bibliothèques de sérialisation préférés

Voici quelques schémas de sérialisation que j'ai volontiers utilisés dans le passé et qui, à mon avis, méritent d'être examinés. Remarque: ces recommandations sont basées sur le travail que j'ai effectué avec des microcontrôleurs dans le passé, elles sont donc écrites en C pur pour une compatibilité maximale et prennent en charge l'allocation statique.

JSON

La spécification JSON est principalement composée de ces excellents diagrammes

JSON est devenu une sorte de lingua franca de l'Internet piloté par API. Initialement utilisé comme format de sérialisation pour communiquer le contenu des objets JavaScript, il apparaît maintenant plus ou moins partout, tout comme JavaScript. JSON est encodé en texte lisible et inscriptible par l'homme, ce qui facilite l'inspection ou la modification en un clin d'œil. Il est sans schéma et permet fondamentalement de stocker des objets et des tableaux d'objets qui se composent de paires de valeurs clés typées. Si vous parlez JSON, vous pouvez communiquer avec à peu près n'importe quoi.

Un effet secondaire de la convivialité de JSON est sa dépendance à l'égard des chaînes, ce qui peut rendre la gestion du C un peu difficile, en particulier en évitant l'utilisation de l'allocation dynamique. Une stratégie pour y faire face est une forte dépendance sur des tampons plats pour contenir les chaînes et des pointeurs pour pointer vers divers objets et champs sans leur allouer de mémoire. J'ai heureusement utilisé jsmn pour gérer cela lors de l'analyse JSON.

Jsmn est assez simple pour être complètement contenu dans un seul fichier d'en-tête et est un wrapper mince très, très mince autour de la technique basée sur le pointeur mentionnée ci-dessus. Il valide que les objets JSON sont correctement formatés et permet au développeur d'obtenir des pointeurs vers les différentes clés et valeurs contenues avec. C'est au développeur de comprendre comment interpréter les chaînes qui s'y trouvent, bien que cela vous donne leur type. C’est un peu comme strtok sous stéroïdes.

MessagePack / msgpack

MessagePack est un moyen intéressant entre JSON convivial pour l'homme et un protocole binaire optimisé au maximum efficace. La page d'accueil le facture littéralement comme «C'est comme JSON. Mais rapide et petit. " (Aïe, désolé JSON.) Comme JSON, il est sans schéma et est fondamentalement composé de paires clé / valeur. Contrairement à JSON, il n'est pas conçu pour être lisible par l'homme, et la frappe semble un peu plus explicite. Voir la figure ci-dessous pour une comparaison d'un message dans JSON et MessagePack. Il se vend à peu près. MessagePack est assez courant, il est donc facile de trouver des bibliothèques pour lire et écrire. Je l'ai utilisé dans Rust et C, mais il existe au moins une ou deux options pour la plupart des langages et plates-formes majeurs et mineurs: le projet revendique la prise en charge d'au moins 50. Si vous êtes intéressé par un peu plus de détails, ( Al Willaims) en a récemment parlé.

Un message en JSON et MessagePack

Lorsque je parle MessagePack à partir d'un microcontrôleur, j'ai utilisé CWPack. La documentation peut être un peu mince, mais le code est facile à suivre et se décompose très bien en petits blocs fonctionnels. En fait, c'est ma fonctionnalité préférée. CWPack est très facile à comprendre et à utiliser. La sérialisation et la désérialisation d'un message est un jeu d'enfant avec de petites fonctions autonomes. La plus grande chose à surveiller est qu'elle peut parfois être plus manuelle que prévu. Par exemple, lors de l'envoi d'une carte, vous devez encoder le début de la carte et le nombre d'éléments, puis pour chaque élément encoder séparément le nom de la clé puis la valeur. Une fois que vous vous êtes habitué à la façon de faire de CWPack, il produit certains des codes les plus faciles à comprendre.

Tampons de protocole / protobuf

Les tampons de protocole sont un poids lourd dans le monde de la messagerie schemeful. Construit à l'origine par Google, il peut, et a probablement été, utilisé pour coder des données pour tous les types de liaisons de données et d'applications imaginables. Protobuf est spécifié par des fichiers séparés dans un dialecte appelé proto, proto2 ou proto3. Ceux-ci définissent les messages, l'unité hiérarchique de base et tous les différents champs et relations entre eux. Celles-ci .proto les fichiers sont la spécification absolue de ce qui peut et ne peut pas être consommé par les encodeurs et décodeurs qui sont produits par la génération de code à partir de la spécification proto.

Parce que votre Protobuf est distinct de l'implémentation, faire référence à une «bibliothèque» dans ce contexte fait vraiment référence au générateur de code utilisé pour produire les sources qui seront compilées dans l'application finale. "Protocol" est le générateur fourni par Google et pour proto2 peut produire des implémentations en Java, Python, Objective-C et C ++. L'implémentation de proto3 ajoute la prise en charge de Dart, Go, Ruby et C #. Mais ce n'est que l'outillage officiel de Google, et il y a beaucoup, beaucoup plus d'options disponibles. N'oubliez pas que Protobuf spécifie les octets sur le câble, et non l'API pour interagir avec les messages individuels, de sorte que chaque générateur de code peut produire un code radicalement différent pour interagir avec les mêmes messages.

De l’excellent article de blog de (Martin Kleppmann)

Les protobufs peuvent être extrêmement puissants, mais il est important de bien réfléchir à la prise en charge de votre message. Les définitions de proto spécifient si les champs sont obligatoires ou facultatifs, quels sont leurs types et plus encore. En règle générale, un décodeur vérifie que toutes les métadonnées de chaque champ correspondent exactement à ce qu'il attend et ne pourra décoder rien d'autre. Si votre système doit prendre en charge un mélange hétérogène de versions logicielles, il est possible de se retrouver dans un état où les encodeurs et décodeurs plus récents refuseront d'interagir avec les messages encodés dans une version différente de votre protocole.

Cela dit, en particulier pour un grand projet, les tampons de protocole font un excellent travail en forçant tous les participants à se mettre d'accord exactement sur les données envoyées et comment. Pour les microcontrôleurs, je me tourne généralement vers nanopb comme générateur de code de mon choix. La taille compacte du code généré et la prise en charge de l'allocation statique facilitent l'intégration dans presque tous les projets. En fin de compte, vous donnez la structure à coder et les tampons à coder, ou vice-versa.

Aller de l'avant et encoder

Quelles que soient vos contraintes, il existe un format de sérialisation des messages pour chaque besoin. Ce sont quelques-unes de celles que j'ai trouvées utiles mais il y a évidemment beaucoup plus d'options. Avez-vous un format ou une bibliothèque préférée que vous utilisez tout le temps? Carillon dans les commentaires; J'adorerais savoir quoi essayer ensuite!

LAISSER UN COMMENTAIRE

Rédigez votre commentaire !
Entrez votre nom ici