当前位置: 首页 > 工具软件 > SFrame > 使用案例 >

分布式游戏服务器框架sframe(三)—— 序列化与反序列化

濮阳烨然
2023-12-01

        序列化和反序列化是我们在实际开发经常用到的东西。一套好的序列化和反序列化的解决方案,往往会起到事半功倍的效果。sframe拥有一套很方便高效的序列化和反序列化机制。在本篇文章中,我将主要介绍这套机制的实现原理。

        序列化和反序列化已经是非常成熟的技术了,网上也有着一大堆不一样的实现。已有现成的各种各样的开源库供我们选择。很多较为流行的库都比较大,并且大多都是基于代码生成技术的实现。比如google的protobuf,非常庞大,跨语言,通过proto文件生成目标代码。但是sframe的服务间通信用不到这么多东西,而且我一向都不怎么喜欢用代码生成技术,我个人更倾向于纯粹一点的东西,能用代码本身实现尽量用代码本身实现(当然,这仅仅是我的个人偏好而已,并不是针对代码生成技术)。所以决定自己搞一套简单的序列化库。

       sframe已经将服务间消息的发送以及接收都做了比较全面的封装,所以在服务间消息收发上,必须用自己的序列化方式。其他的情况(比如客户端与服务器通信)并未做强行限制,可以选择其他的序列化方式。

         sframe支持char、int8_t、uint8_t、int16_t、uint16_t、int32_t、uint32_t、int64_t、uint64_t、int8_t、double、std::string、std::vector、std::list、std::set、std::unordered_set、std::map、std::unordered_map、std::shared_ptr、自定义结构体以及以上所有类型的数组的序列化与反序列化。sframe的序列化部分很轻量级,只有一个头文件sframe/sfram/util/Serialization.h。

         在c++中,序列化的就是将各种对象编码成一个字节流,反序列化就是将一个字节流解码为各种对象。我们先来看看,使用sframe,将如何完成这两件事。

         使用sframe进行序列化和反序列化示例代码:

std::string s1 = "hellow,world";
double d1 = 1111111.001;
int64_t num1 = 55555;

char buf[256];
sframe::StreamWriter writer(buf, sizeof(buf));
sframe::AutoEncode(writer, s1, d1, num1);

std::string s2;
double d2;
int64_t num2;

sframe::StreamReader reader(buf, writer.GetStreamLength());

        这段代码演示了sframe的序列化和反序列化。从以上代码我们可以看出,将若干对象序列化成字节流,仅需要调用sframe::AutoEncode(stream_writer, obj…)。反之将一个字节流解码为若干对象,也只需调用sframe::AutoDecode(stream_reader,obj…)。这种自动化编解码的方式,我们用起来是很方便的(至少我认为是很方便的>_<!)。那么,在c++中,我们是如何实现这样的功能的呢?接下来我将介绍其关键实现。我只讲解sframe::AutoEncode的实现,sframe::AutoDecode的原理和sframe::AutoEncode是一样的。

        c++ 不像 c# java 这些高级语言,内置反射系统。所以 c++ 要实现自动化的编解码,使用模板技术是个非常不错的选择。首先,要使 sframe::AutoEncode 支持任意多个任意类型的参数,它必须是可变参数模板函数。下面我们来看一下它的代码:

inline bool AutoEncode(StreamWriter & stream_writer)
{
	return true;
}

template<typename T>
inline bool AutoEncode(StreamWriter & stream_writer, const T & t)
{
	return Encoder::Encode<T>(stream_writer, t);
}

template<typename T, typename... T_Args>
inline bool AutoEncode(StreamWriter & stream_writer, const T & t, const T_Args&... args)
{
	return AutoEncode<T>(stream_writer, t) && AutoEncode<T_Args...>(stream_writer, args...);
}
        从此我们可以看出, AutoEncode 函数的功能,主要就是对N个对象依次调用Encoder::Encode<T>函数实现编码。那么Encoder又是什么呢?先来看一下它的代码:
class Encoder
{
public:
	template<typename T>
	static bool Encode(StreamWriter & stream_writer, const T & obj)
	{
		return call<T>(decltype(match<T>(nullptr))(), stream_writer, obj);
	}

private:
	template<typename T>
	static bool call(std::false_type, StreamWriter & stream_writer, const T & obj)
	{
		return Serializer<T>::Encode(stream_writer, obj);
	}

	template<typename T>
	static bool call(std::true_type, StreamWriter & stream_writer, const T & obj)
	{
		return obj.Encode(stream_writer);
	}

	// 匹配器 ———— bool返回值类成员函数,形如 bool T_Obj::FillObject(T_Reader & reader)
	template<typename U, bool(U::*)(StreamWriter &) const>
	struct MethodMatcher;

	template<typename U>
	static std::true_type match(MethodMatcher<U, &U::Encode>*);

	template<typename U>
	static std::false_type match(...);
};

        从代码可看出,Encoder::Encode<T>函数的本质就是调用真正的对象序列化函数。那么有人会问了,Encoder就仅仅充当了一层调用包装作用,为什么要搞这么复杂?其实Encoder不仅仅只是一个调用包装,Encoder同时进行了一次选择。它会判断这个对象的类型是否有Encode()成员函数,若有则调用obj.Encode(stream_writer)。若没有则调用Serializer<T>::Encode(stream_writer,obj)。当然,这个选择的过程是通过模板匹配实现的,这一切都是静态过程,是编译时发生的,不会体现在运行时。

        为什么要做这个选择?主要是考虑到对于int、string这些类型,我们无法为其添加成员函数,只有使用静态函数重载的方式对其进行序列化。而对于自定义的结构体,我们更倾向于使用成员函数对其进行序列化。那么有人可能会问了!为什么搞这么麻烦,都采用静态函数重载的方式不就行了吗?其实这当然是可以的,但是若你的结构体在另外的名字空间的话,就会出问题了。所以只能采用这种方式。同时,采用此种方案,也使其支持多态,不同的派生类,调用不同的序列化方法。

        Serializer是个模板类,其有一个Encode()静态函数,完成了其真正的序列化过程。Serialize.h文件中,对所支持的每一种基础类型(这里的基础类型是除开自定义结构体的所有类型,包括string、这些)都有对应的特化实现。

        对于自定义结构体,要使其支持自动序列化,必须为其定义Encode成员函数。这没办法,对于不支持反射的语言来说,只能这样。好在定义Encode、Decode函数的套路都是一样的,所以我在Serialize.h提供了一些宏来实现这些成员函数的定义。示例代码如下。

        普通结构体的序列化和反序列化示例:
struct MyMsg 
{
	DEFINE_SERIALIZE_INNER(s, d, num)

	std::string s;
	double d;
	int64_t num;
};

int main()
{
	MyMsg msg1 = { "hellow,word", 1000.0111, 5555 };
	char buf[256];
	sframe::StreamWriter writer(buf, sizeof(buf));
	sframe::AutoEncode(writer, msg1);

	MyMsg msg2;
	sframe::StreamReader reader(buf, writer.GetStreamLength());
	sframe::AutoDecode(reader, msg2);

	return 0;
}
        多态结构体的序列化与反序列化示例:

struct Base
{
	DECLARE_PURE_VIRTUAL_SERIALIZE

	virtual uint16_t GetType() const = 0;

	static uint16_t GetObjectType(const Base * b)
	{
		return b->GetType();
	}

	static std::shared_ptr<Base> CreateObject(uint16_t t);
};

struct Derive : public Base
{
	DEFINE_SERIALIZE_INNER(s, d, num)

	uint16_t GetType() const override
	{
		return 1;
	}

	std::string s;
	double d;
	int64_t num;
};

std::shared_ptr<Base> Base::CreateObject(uint16_t t)
{
	if (t == 1)
	{
		return std::make_shared<Derive>();
	}

	return nullptr;
}

int main()
{
	std::shared_ptr<Derive> derive1 = std::make_shared<Derive>();
	derive1->s = "hellow, word";
	derive1->d = 100.00001;
	derive1->num = 55555;

	std::shared_ptr<Base> p1 = derive1;
	char buf[256];
	sframe::StreamWriter writer(buf, sizeof(buf));
	sframe::AutoEncode(writer, p1);

	std::shared_ptr<Base> p2;
	sframe::StreamReader reader(buf, writer.GetStreamLength());
	sframe::AutoDecode(reader, p2);

	return 0;
}
        序列化的原理大体就是如此,至于反序列化,原理是相同的,以此类推便可。这种模板的运用思想是个很有用的技巧,在整个 sframe 的实现中,会有很多地方运用这种技巧,感兴趣的同学请自己看源代码。

 类似资料: