序列化

it2022-05-09  27

为何需要序列化

我们常常需要将对象持久化或者在通信中传递对象,这时我们就会使用到序列化。

C++中的序列化

对于C++这样的静态语言来说,序列化往往都很不方便,它对于类型转换非常敏感和严格,所以说想要对C++对象进行序列化那必须对每个序列化的对象进行针对性编码。

C++中的反序列化

因为C++类型是在编译时决定的,而序列化是运行时行为,反序列化需要生成的对象类型必须在编译时决定好,这让人感到很无力。为了能够正确的进行反序列化,我们通常会生成一个映射表,将对象的类型所对应的ID一并序列化,这样就能在运行时通过”预反序列化“得到类型ID,然后查找映射表来得到需要创建的对象类型,然后创建对象进行反序列化。

序列化类型映射表

理想的序列化类型映射表能够在编译时生成,因为我们需要序列化的类型都是编译时决定好了的,所以在编译时生成其映射表应该理所当然。然而非常尴尬的是,C++如何建立类型ID到类型的映射?它是静态类型的语言,如果要建立一个类型的映射表,即使使用模板技术也只能做到编译期映射(也就是需要用编译时常量进行类型查找,对于运行时的反序列化行为,这显然是没有任何作用的)。

一个可行的方案

为了能够在运行时进行查找,我使用了反序列化辅助对象作为映射类型的载体。这个对象利用模板来关联序列化类型,它同时也要求序列化类型需要提供默认构造,并且默认构造能够被其所访问。

class SerializableObject; class UnserializerBase{ protected : UnserializerBase(){} public : virtual ~ UnserializerBase(){} public : virtual SerializableObject * Unserialize( const char *& pData) = 0 ;};template < typename T > class Unserializer : public UnserializerBase{ public : typedef T Object; public : Unserializer(){} public : SerializableObject * Unserialize( const char *& pData) { Object * pObj = new Object; // 要求需要序列化支持的类具有默认构造函数 pData += pObj -> Unserialize(pData); return pObj; }};

这里显而易见的是我们的序列化对象必须从SerializableObject派生。

类型ID的计算

简单起见,我们可以通过C++提供的typeid来计算类型的ID,当然这需要开启相应编译开关。序列化类型可以通过模板参数将自身传递给某个模板函数来计算其ID,并将这个ID和反序列化辅助类型Unserializer对象建立起映射关系。

template < typename Object > static ClassIDType BuildClassID(){ static Unserializer < Object > unserializer; static std:: string name(typeid(Object).name()); static ClassIDType id = std::hash < BinaryData > ()(BinaryData(name.c_str(), name.length())); GetIDMapSerializer()[id] = & unserializer; return id;} static std::map < ClassIDType, UnserializerBase *>& GetIDMapSerializer(){ static std::map < ClassIDType, UnserializerBase *> id_unserializer; return id_unserializer;} 此处的实现对类型名求了hash值来作为类型ID,至于碰撞的概率和我们的序列化类型数量相比较就可以忽略不计了。并且创建的都是静态对象,这能够保证每个序列化类型值生成一次类型ID和反序列化辅助对象。

类型ID的序列化和反序列化

序列化对象对于其ID的序列化和反序列化时的查找过程并不关心,那么我们应该透明的去完成这个过程。同时考虑到序列化对象需要将自身类型进行注册,我们可以在此时就完成我们的类型ID到反序列化辅助对象的映射的创建。我们在SerializableObject基类和序列化类型之间插入了一个中间模板类,它完成我们刚才提到的映射的创建以及ID的序列化工作(后面讲为什么它不完成ID反序列化的工作)。

// 派生类将自身类型传递给模板参数 template < typename T > class Serializable : public SerializableObject{ protected : static const ClassIDType ID; public : typedef T Object; protected : Serializable(){ID;} public : virtual ~ Serializable(){} public : template < typename BufferType > void Serialize(BufferType & buffer) { SaveToBuffer(ID, buffer); // 这里应该判断子类序列化是否成功,如果不成功则回滚,暂时未加此功能 static_cast < Object *> ( this ) -> DoSerialize_(buffer); } size_t Unserialize( const char * pData) { return DoUnserialize_(pData); } static const ClassIDType GetClassID(){ return ID;} private : // 通过模板模拟虚函数 template < typename BufferType > void DoSerialize_(BufferType & buffer){}; virtual size_t DoUnserialize_( const char * pData) = 0 ;};template < typename T > const typename Serializable < T > ::ClassIDType Serializable < T > ::ID = Serializable < T > ::BuildClassID < T > ();

这里为了能够减少对存放序列化内容的缓冲区的依赖,我们对序列化接口使用了模板推导,但是这导致我们难以通过虚函数来延迟序列化实现。这里我们采用了一个非标准的实现方式——子类注册自身类型给父类,并要求子类必须实现一个名为DoSerialize_的模板成员函数,那么我们就可以通过类型转换去调用子类接口了,达到类似虚函数的功能。

我们最希望的反序列化应该是这样的:

Type* pObject = dynamic_cast<Type*>(Unserialize(pData));

那么,反序列化接口就不能作为一个模板类的静态函数。所以我们将这个接口放到了SerializableObject当中

// 此类实现的静态方法Create用于反序列化 class SerializableObject{ public : typedef size_t ClassIDType; protected : SerializableObject(){} public : virtual ~ SerializableObject(){} public : static SerializableObject * Create( const char *& pData) { ClassIDType id = 0 ; pData += LoadFromData(id, pData); std::map < ClassIDType, UnserializerBase *>& id_serializer = GetIDMapSerializer(); auto iter = id_serializer.find(id); if (id_serializer.end() != iter) { return ( * iter).second -> Unserialize(pData); } return 0 ; } protected : template < typename Object > static ClassIDType BuildClassID() { static Unserializer < Object > unserializer; static std:: string name(typeid(Object).name()); static ClassIDType id = std::hash < BinaryData > ()(BinaryData(name.c_str(), name.length())); GetIDMapSerializer()[id] = & unserializer; return id; } private : static std::map < ClassIDType, UnserializerBase *>& GetIDMapSerializer() { static std::map < ClassIDType, UnserializerBase *> id_unserializer; return id_unserializer; }};

这样,基本上我们就可以这样来定义我们的序列化类型了:

class Test : public Serializable < Test > { int num_; public : explicit Test( int n = 0 ) : num_(n){} private : friend class Serializable < Test > ; template < typename BufferType > void DoSerialize_(BufferType & buffer) { SaveToBuffer(num_, buffer); } size_t DoUnserialize_( const char * pData) { return LoadFromData(num_, pData); }};

这里的SaveToBuffer和LoadFromData是一对序列化帮助函数。

这个序列化和反序列化的实现有个很大的缺点,那就是映射的建立。因为我们使用的是模板,所以说序列化类型不被使用就不会被编译,那么这个类型就不会被加入到映射中,那么我们的程序就可能出现问题(找不到某个ID所映射的反序列化辅助对象)。解决这个问题的方法就是强制编译器编译我们可能会使用到的序列化类型,比如为每个序列化类型定义一个其类型ID的静态引用,如果是程序中已经使用到序列化类型的话则不必。

示例测试:

假设我们有一套命令系统,各种不同的命令派生自一个命令基类Command,并实现其名为Execute的接口。我们需要将不同的命令类型对象序列化和反序列化(这在网络通信协议中普遍使用,网络两端可定义相同的命令类型,一端只对这个命令对象存储参数,一端负责实现其执行过程),反序列化过程中不需要关心具体的命令类型,只需要调用其接口Execute便可。

我们的序列化和反序列化非常适合这个系统,下面就给出测试工程代码,测试工程没有建立网络通信,只是体现我们的序列化和反序列化的方式。

下载测试工程源码

转载于:https://www.cnblogs.com/EvilGhost/articles/Serialize.html


最新回复(0)