J'ai une petite hiérarchie d'objets que je dois sérialiser et transmettre via une connexion socket. Je dois à la fois sérialiser l'objet, puis le désérialiser en fonction de son type. Existe-t-il un moyen facile de le faire en C++ (comme il en existe en Java)?
Existe-t-il des exemples ou des didacticiels de code de sérialisation en ligne C++?
EDIT: Juste pour être clair, je cherche des méthodes pour convertir un objet en un tableau d'octets, puis de nouveau en un objet. Je peux gérer la transmission par socket.
En parlant de sérialisation, le boost API de sérialisation me vient à l'esprit. Quant à la transmission des données sérialisées sur le net, j'utiliserais soit des sockets Berkeley soit la bibliothèque asio .
Modifier:
Si vous souhaitez sérialiser vos objets dans un tableau d'octets, vous pouvez utiliser le sérialiseur boost de la manière suivante (tirée du site du didacticiel):
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
class gps_position
{
private:
friend class boost::serialization::access;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & degrees;
ar & minutes;
ar & seconds;
}
int degrees;
int minutes;
float seconds;
public:
gps_position(){};
gps_position(int d, int m, float s) :
degrees(d), minutes(m), seconds(s)
{}
};
La sérialisation réelle est alors assez simple:
#include <fstream>
std::ofstream ofs("filename.dat", std::ios::binary);
// create class instance
const gps_position g(35, 59, 24.567f);
// save data to archive
{
boost::archive::binary_oarchive oa(ofs);
// write class instance to archive
oa << g;
// archive and stream closed when destructors are called
}
La désérialisation fonctionne de manière analogue.
Il existe également des mécanismes qui vous permettent de gérer la sérialisation des pointeurs (les structures de données complexes comme tress, etc. ne posent aucun problème), les classes dérivées et vous pouvez choisir entre la sérialisation binaire et textuelle. En outre, tous les conteneurs STL sont pris en charge immédiatement.
Dans certains cas, lorsque vous traitez avec des types simples, vous pouvez faire:
object o;
socket.write(&o, sizeof(o));
C'est correct comme preuve de concept ou première ébauche, afin que les autres membres de votre équipe puissent continuer à travailler sur d'autres parties.
Mais tôt ou tard, généralement plus tôt, cela vous blessera!
Vous rencontrez des problèmes avec:
(De plus, vous devez savoir dans quoi vous déballez du côté réception.)
Vous pouvez améliorer cela en développant vos propres méthodes de marshalling/unmarshalling pour chaque classe. (Idéalement virtuels, afin qu'ils puissent être étendus dans des sous-classes.) Quelques macros simples vous permettront d'écrire assez rapidement différents types de base dans un ordre neutre grand/petit-endian.
Mais ce genre de travail de grognement est bien meilleur et plus facile à gérer via la bibliothèque de sérialisation de boost .
La sérialisation signifie transformer votre objet en données binaires. Alors que la désérialisation signifie recréer un objet à partir des données.
Lors de la sérialisation, vous poussez des octets dans un vecteur uint8_t
. Lors de la désérialisation, vous lisez des octets à partir d'un vecteur uint8_t
.
Il existe certainement des modèles que vous pouvez utiliser lors de la sérialisation des éléments.
Chaque classe sérialisable doit avoir une fonction serialize(std::vector<uint8_t> &binaryData)
ou similaire signée qui écrira sa représentation binaire dans le vecteur fourni. Ensuite, cette fonction peut transmettre ce vecteur aux fonctions de sérialisation de ses membres afin qu'ils puissent également y écrire leurs trucs.
Étant donné que la représentation des données peut être différente sur différentes architectures. Vous devez trouver un schéma pour représenter les données.
Commençons par les bases:
Écrivez simplement les octets en petit ordre endian. Ou utilisez la représentation varint si la taille est importante.
Sérialisation en petit ordre endien:
data.Push_back(integer32 & 0xFF);
data.Push_back((integer32 >> 8) & 0xFF);
data.Push_back((integer32 >> 16) & 0xFF);
data.Push_back((integer32 >> 24) & 0xFF);
Désérialisation du petit ordre endien:
integer32 = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
Autant que je sache, l'IEEE 754 a un monopole ici. Je ne connais aucune architecture traditionnelle qui utiliserait autre chose pour les flotteurs. La seule chose qui peut être différente est l'ordre des octets. Certaines architectures utilisent peu d'endian, d'autres utilisent un ordre d'octets gros endian. Cela signifie que vous devez faire attention à l'ordre dans lequel vous entendez les octets du côté récepteur. Une autre différence peut être la gestion des valeurs dénormales et infinies et NAN. Mais tant que vous évitez ces valeurs, vous devriez être OK.
Sérialisation:
uint8_t mem[8];
memcpy(mem, doubleValue, 8);
data.Push_back(mem[0]);
data.Push_back(mem[1]);
...
La désérialisation le fait à l'envers. Attention à l'ordre des octets de votre architecture!
Vous devez d'abord vous mettre d'accord sur un encodage. UTF-8 est courant. Ensuite, stockez-le sous la forme d'un préfixe de longueur: commencez par stocker la longueur de la chaîne en utilisant une méthode que j'ai mentionnée ci-dessus, puis écrivez la chaîne octet par octet.
Ce sont les mêmes que les cordes. Vous sérialisez d'abord un entier représentant la taille du tableau, puis sérialisez chaque objet qu'il contient.
Comme je l'ai déjà dit, ils devraient avoir une méthode serialize
qui ajoute du contenu à un vecteur. Pour désérialiser un objet, il doit avoir un constructeur qui accepte un flux d'octets. Il peut s'agir d'un istream
mais dans le cas le plus simple, il peut s'agir simplement d'un pointeur de référence uint8_t
. Le constructeur lit les octets qu'il souhaite dans le flux et configure les champs dans l'objet. Si le système est bien conçu et sérialise les champs dans l'ordre des champs d'objet, vous pouvez simplement transmettre le flux aux constructeurs du champ dans une liste d'initialisation et les désérialiser dans le bon ordre.
Vous devez d'abord vous assurer que ces objets sont vraiment quelque chose que vous souhaitez sérialiser. Vous n'avez pas besoin de les sérialiser si des instances de ces objets sont présentes sur la destination.
Vous venez de découvrir que vous devez sérialiser cet objet pointé par un pointeur. Le problème des pointeurs qu'ils ne sont valables que dans le programme qui les utilise. Vous ne pouvez pas sérialiser le pointeur, vous devez cesser de les utiliser dans les objets. Créez plutôt des pools d'objets. Ce pool d'objets est essentiellement un tableau dynamique qui contient des "boîtes". Ces cases ont un compte de référence. Le nombre de références non nul indique un objet vivant, zéro indique un emplacement vide. Ensuite, vous créez un pointeur intelligent semblable au shared_ptr qui ne stocke pas le pointeur sur l'objet, mais l'index dans le tableau. Vous devez également vous mettre d'accord sur un index qui dénote le pointeur nul, par exemple. -1.
Fondamentalement, ce que nous avons fait ici a remplacé les pointeurs par des index de tableau. Maintenant, lors de la sérialisation, vous pouvez sérialiser cet index de tableau comme d'habitude. Vous n'avez pas à vous soucier de l'emplacement de l'objet en mémoire sur le système de destination. Assurez-vous simplement qu'ils ont également le même pool d'objets.
Nous devons donc sérialiser les pools d'objets. Mais lesquels? Eh bien, lorsque vous sérialisez un graphique d'objet, vous ne sérialisez pas seulement un objet, vous sérialisez un système entier. Cela signifie que la sérialisation du système ne doit pas commencer à partir de parties du système. Ces objets ne devraient pas se soucier du reste du système, ils ont seulement besoin de sérialiser les index du tableau et c'est tout. Vous devez disposer d'une routine de sérialisation du système qui orchestre la sérialisation du système et parcourt les pools d'objets appropriés et les sérialise tous.
À la réception, tous les tableaux et les objets à l'intérieur sont désérialisés, recréant le graphique d'objet souhaité.
Ne stockez pas de pointeurs dans l'objet. Avoir un tableau statique qui contient les pointeurs vers ces fonctions et stocker l'index dans l'objet.
Étant donné que les deux programmes ont cette table compilée dans les étagères, l'utilisation de l'index devrait fonctionner.
Puisque j'ai dit que vous devriez éviter les pointeurs dans les types sérialisables et que vous devriez utiliser des index de tableau à la place, le polymorphisme ne peut tout simplement pas fonctionner, car il nécessite des pointeurs.
Vous devez contourner ce problème avec les balises de type et les unions.
En plus de tout ce qui précède. Vous souhaiterez peut-être que différentes versions du logiciel interagissent.
Dans ce cas, chaque objet doit écrire un numéro de version au début de sa sérialisation pour indiquer la version.
Lors du chargement de l'objet de l'autre côté, les nouveaux objets peuvent peut-être gérer les anciennes représentations mais les plus anciens ne peuvent pas gérer les nouveaux, ils doivent donc lever une exception à ce sujet.
Chaque fois que quelque chose change, vous devez augmenter le numéro de version.
Donc, pour conclure, la sérialisation peut être complexe. Mais heureusement, vous n'avez pas besoin de tout sérialiser dans votre programme, le plus souvent seuls les messages de protocole sont sérialisés, qui sont souvent de vieilles structures simples. Vous n'avez donc pas besoin des astuces complexes que j'ai mentionnées ci-dessus trop souvent.