Bien sûr, je connais des formats tels que xml, json, bson, yaml, protobuf, Thrift, ASN.1. J'ai même trouvé un arbre exotique, qui est lui-même un tueur de JSON, XML, YAML et d'autres comme eux .
Alors pourquoi ne correspondaient-ils pas tous? Pourquoi ai-je été obligé d'écrire un autre sérialiseur?
Après la publication de l'article, dans les commentaires, ils ont donné plusieurs liens vers les formats CBOR , UBJSON et MessagePack que j'avais manqués . Et ils sont susceptibles de résoudre mon problème sans écrire un vélo.
C'est dommage que je n'ai pas pu trouver ces spécifications plus tôt, donc je vais ajouter ce paragraphe pour les lecteurs et pour mon propre rappel de ne pas se précipiter pour écrire du code ;-).
Revues de formats sur Habré: CBOR , UBJSON
Exigences initiales
Imaginez que vous ayez besoin de modifier un système distribué composé de plusieurs centaines d'appareils de types différents (plus de dix types d'appareils exécutant différentes fonctions). Ils sont combinés en groupes qui échangent des données entre eux via des lignes de communication série utilisant le protocole Modbus RTU.
En outre, certains de ces appareils sont connectés à une ligne de communication CAN commune, qui assure le transfert de données dans l'ensemble du système. Le taux de transfert de données sur la ligne de communication Modbus est jusqu'à 115200 bauds et la vitesse sur le bus CAN est limitée à la vitesse jusqu'à 50 kBaud en raison de sa longueur et de la présence d'interférences industrielles graves.
La grande majorité des appareils sont développés sur des microcontrôleurs des séries STM32F1x et STM32F2x. Bien que certains d'entre eux fonctionnent également sur STM32F4x. Et bien sûr, les systèmes basés sur Windows / Linux avec des microprocesseurs x86 comme contrôleurs de haut niveau.
Pour estimer la quantité de données traitées et transmises entre les appareils ou stockées en tant que réglages / paramètres de fonctionnement: Dans un cas - 2 nombres de 1 octet et 6 nombres de 4 octets, dans l'autre - 11 nombres de 1 octet et 1 nombre de 4 octets et etc. Pour référence, la taille des données dans une trame CAN standard est jusqu'à 8 octets, et dans une trame Modbus, jusqu'à 252 octets de charge utile.
Si vous n'avez pas encore pénétré la profondeur du terrier du lapin, ajoutez à ces données d'entrée: la nécessité de garder une trace des versions de protocole et des versions de micrologiciel pour différents types d'appareils, ainsi que la nécessité de maintenir la compatibilité non seulement avec les formats de données existants, mais aussi pour assurer la travail des appareils avec les générations futures, qui ne s'arrêtent pas non plus et sont en constante évolution et retravaillé au fur et à mesure que la fonctionnalité se développe et que les montants se trouvent dans les implémentations. De plus, interaction avec des systèmes externes, extension des exigences, etc.
Au départ, en raison des ressources limitées et des faibles vitesses des lignes de communication, un format binaire a été utilisé pour l'échange de données, qui était uniquement lié aux registres Modbus. Mais une telle implémentation n'a pas réussi le premier test de compatibilité et d'extensibilité.
Par conséquent, lors de la refonte de l'architecture, il a été nécessaire d'abandonner l'utilisation des registres Modbus standard. Et pas même parce que d'autres lignes de communication sont utilisées en plus de ce protocole, mais plutôt à cause de l'organisation trop limitée des structures de données basées sur des registres 16 bits.
En effet, à l'avenir, avec l'évolution inévitable du système, il peut être nécessaire (et en fait, c'était déjà nécessaire), de transférer des chaînes de texte ou des tableaux. En théorie, ils peuvent également être affichés sur la carte du registre Modbus, mais cela s'avère être de l'huile, car vient l'abstraction sur l'abstraction.
Bien entendu, vous pouvez transférer des données sous forme de blob binaire en référence à la version du protocole et au type de bloc. Et même si à première vue cette idée peut sembler judicieuse, après avoir fixé certaines exigences pour l'architecture, vous pouvez définir une fois pour toutes les formats de données, économisant ainsi considérablement les frais généraux qui seront inévitables lors de l'utilisation de formats tels que XML ou JSON.
Pour faciliter la comparaison des options, j'ai créé le tableau suivant pour moi-même:
Transfert de données binaires sans identification de champ:
Avantages:
:
:
:
:
Avantages:
- . , .
:
- , .
- . , .
- . , , . , .
- , .
:
:
- .
:
- . , .
- , , .
Et imaginez simplement comment plusieurs centaines de périphériques commencent à échanger des données binaires entre eux, même avec la liaison de chaque message à la version du protocole et / ou au type de périphérique, alors la nécessité d'utiliser un sérialiseur avec des champs nommés devient immédiatement évidente. Après tout, même une simple interpolation de la complexité de la prise en charge d'une telle solution dans son ensemble, bien qu'après un temps très court, vous oblige à prendre la tête.
Et ce, même sans prendre en compte les souhaits attendus du client d'augmenter la fonctionnalité, la présence de jambages obligatoires dans la mise en œuvre et les améliorations "mineures", à première vue, des améliorations, qui apporteront certainement avec elles un piquant particulier de la recherche de jambages récurrents dans le travail bien coordonné d'un tel zoo ...
Quelles sont les options?
Après un tel raisonnement, vous parvenez involontairement à la conclusion qu'il est nécessaire dès le début d'établir une identification universelle des données binaires, y compris lors de l'échange de paquets sur des lignes de communication à bas débit.
Et quand je suis arrivé à la conclusion qu'on ne peut pas se passer d'un sérialiseur, j'ai d'abord regardé les solutions existantes qui ont déjà fait leurs preuves du meilleur côté, et qui sont déjà utilisées dans de nombreux projets.
Les formats de base xml, json, yaml et autres variantes textuelles avec une syntaxe formelle très pratique et simple, bien adaptée au traitement de documents et en même temps pratique pour la lecture et l'édition par des humains, ont dû être supprimés immédiatement. Et juste en raison de leur commodité et de leur simplicité, ils ont une surcharge très importante lors du stockage de données binaires, qui avaient juste besoin d'être traitées.
Par conséquent, compte tenu des ressources limitées et des lignes de communication à faible débit, il a été décidé d'utiliser un format de présentation de données binaires. Mais même dans le cas de formats capables de convertir des données en une représentation binaire, tels que Protocol Buffers, FlatBuffers, ASN.1 ou Apache Thrift, la surcharge de la sérialisation des données, ainsi que la commodité générale de leur utilisation, n'ont pas contribué à l'implémentation immédiate de l'une de ces bibliothèques.
Le format BSON, qui a une surcharge minimale, était le meilleur ajustement pour l'ensemble de paramètres. Et j'ai sérieusement envisagé de l'utiliser. Mais en conséquence, j'ai quand même décidé de l'abandonner, car toutes choses étant égales par ailleurs, même BSON aura des frais généraux inacceptables.
Il peut sembler étrange à certains que vous ayez à vous soucier d'une douzaine d'octets supplémentaires, mais malheureusement, cette douzaine d'octets devra être transmise à chaque fois qu'un message est envoyé. Et dans le cas du travail sur des lignes de communication à bas débit, même dix octets supplémentaires dans chaque paquet sont importants.
En d'autres termes, lorsque vous travaillez avec dix octets, vous commencez à compter chacun d'eux. Mais avec les données, les adresses des appareils, les sommes de contrôle des paquets et d'autres informations spécifiques à chaque ligne de communication et protocole sont également transmises au réseau.
Qu'est-il arrivé
À la suite de la réflexion et de quelques expériences, un sérialiseur avec les caractéristiques et caractéristiques suivantes a été obtenu:
- La surcharge pour les données de taille fixe est de 1 octet (sans compter la longueur du nom du champ de données).
- , , — 2 ( ). , CAN Modbus, .
- — 16 .
- , , .. . , 16 .
- (, ) — 252 (.. ).
- — .
- . .
- « », , . , , - ( 0xFF).
- . , . .
- , . .
- 8 64 .
- .
- ( ).
- — . , , . ;-)
- . , .
Je voudrais noter séparément
L'implémentation se fait en C ++ x11 dans un fichier d'en-tête unique à l'aide du mécanisme de création de modèles SFINAE (l'échec de la substitution n'est pas une erreur).
Soutenu par la lecture correcte des données dans le tampon (variable) b Environ ng plus grande taille que le type de données stocké. Par exemple, un entier de 8 bits peut être lu dans une variable de 8 à 64 bits. Je pense que cela pourrait valoir la peine d'ajouter un ensemble d'entiers supérieurs à 8 bits, afin qu'ils puissent être transmis en un nombre plus petit.
Les tableaux sérialisés peuvent être lus à la fois en les copiant dans la zone de mémoire spécifiée ou en obtenant une référence normale aux données dans le tampon d'origine, si vous souhaitez éviter la copie, dans les cas où cela n'est pas nécessaire. Mais cette fonctionnalité doit être utilisée avec prudence, car les tableaux d'entiers sont stockés dans l'ordre des octets du réseau, qui peut différer d'une machine à l'autre.
La sérialisation de structures ou d'objets plus complexes n'était même pas prévue. Il est généralement dangereux de transférer des structures sous forme binaire en raison de l'alignement possible de ses champs. Mais si ce problème est néanmoins résolu d'une manière relativement simple, alors il y aura toujours un problème de conversion de tous les champs d'objets contenant des nombres entiers en ordre d'octets du réseau et inversement.
De plus, en cas d'urgence, les structures peuvent toujours être sauvegardées et restaurées sous forme de tableau d'octets. Naturellement, dans ce cas, la conversion des nombres entiers devra être effectuée manuellement.
la mise en oeuvre
L'implémentation est ici: https://github.com/rsashka/microprop
Comment l'utiliser est écrit dans des exemples avec différents degrés de détail:
Utilisation rapide
#include "microprop.h"
Microprop prop(buffer, sizeof (buffer));//
prop.FieldExist(string || integer); // ID
prop.FieldType(string || integer); //
prop.Append(string || integer, value); //
prop.Read(string || integer, value); //
Utilisation lente et réfléchie
#include "microprop.h"
Microprop prop(buffer, sizeof (buffer)); //
prop.AssignBuffer(buffer, sizeof (buffer)); //
prop.AssignBuffer((const)buffer, sizeof (buffer)); // read only
prop.AssignBuffer(buffer, sizeof (buffer), true); // read only
prop.FieldNext(ptr); //
prop.FieldName(string || integer, size_t *length = nullptr); // ID
prop.FieldDataSize(string || integer); //
//
prop.Append(string || blob || integer, value || array);
prop.Read(string || blob || integer, value || array);
prop.Append(string || blob || integer, uint8_t *, size_t);
prop.Read(string || blob || integer, uint8_t *, size_t);
prop.AppendAsString(string || blob || integer, string);
const char * ReadAsString(string || blob || integer);
Exemple d'implémentation utilisant enum comme identifiant de données
class Property : public Microprop {
public:
enum ID {
ID1, ID2, ID3
};
template <typename ... Types>
inline const uint8_t * FieldExist(ID id, Types ... arg) {
return Microprop::FieldExist((uint8_t) id, arg...);
}
template <typename ... Types>
inline size_t Append(ID id, Types ... arg) {
return Microprop::Append((uint8_t) id, arg...);
}
template <typename T>
inline size_t Read(ID id, T & val) {
return Microprop::Read((uint8_t) id, val);
}
inline size_t Read(ID id, uint8_t *data, size_t size) {
return Microprop::Read((uint8_t) id, data, size);
}
template <typename ... Types>
inline size_t AppendAsString(ID id, Types ... arg) {
return Microprop::AppendAsString((uint8_t) id, arg...);
}
template <typename ... Types>
inline const char * ReadAsString(ID id, Types... arg) {
return Microprop::ReadAsString((uint8_t) id, arg...);
}
};
Le code est publié sous la licence MIT, alors utilisez-le pour la santé.
Je serai heureux de tout commentaire, y compris des commentaires et / ou des suggestions.
Mise à jour: je ne me suis pas trompé en choisissant une image pour l'article ;-)