混合语言数据标准

出于对可扩展性的考虑,如今构建系统采用Service调用的模式已经是常见的方案。不同的服务可能基于不同的语言平台来实现,因此之间的数据交换协议就变成了首要需要解决的问题。传统的客户端与服务器,其实也可看作两个不同服务实例之间的远程调用。

在消息数据的描述上,比较古老的两个技术是XML和JSON。这两个数据定义方式各有其优点,但最大的问题在于其数据存储的大小以及序列化和反序列化的时间相对较长。因此在对网络传输性能要求比较高的场景下,直接采用这两个方案会比较浪费资源。

在如今这几年,新出现的两个比较有代表性的方案是Google Protocol Buffer和Thrift。在需要跨通讯机制和语言平台的场景下,这两个方案已经是比较成熟的选择。两者之间从整体的结构上相差不大,但Thrift针对更多语言有项目官方的支持,而Google Protocol Buffer作为Google的内部项目,官方支持的语言平台相对少一些。这两个方案主要的优势在于高效的二进制存储和序列化反序列化过程。

以Protobuf为例,其对数据序列化之后的信息非常紧凑,这意味着消息传输时的字节数更小,消耗的IO性能也就更少。这种压缩的基本概念是来自于Varint,即采用一个或多个字节表示一个数字的紧凑方式。通常,对于int32类型的数字,需要占用4个byte,当数字比较小时,其实只用一个byte就可以。在Varint的处理方式中,每个byte的最高bit为作为一个标示,如果是1,则表示后续的byte也是该数字的一部分,如果为0,则表示后续byte已经属于其它数据部分。因此可以看到,对于小于128的数字,是可以用1个byte来表示的,然而对于特别大的数字,则可能会需要比以往额外多一个byte。但一般来说,应用上是较小的数字占绝大比例,因此这样的节约就变得很可观。

在Protobuf序列化后的消息里,是以Key-Value对的基本结构组成,解包的时候,根据key的值来确定具体的数据field。Key也分为两部分,一部分为field_number,一部分为write_byte,也就是传输的数据类型。相比JSON,能在数据定义模版里指定好数据类型,是对一些强类型语言的代码生成很重要和很便利的部分。值得注意的细节是,Protobuf定义了sint这种类型,这其实也是个传统的zigzag编码手法,即用交错的正负数编码方式来用无符号数表示有符号数字。这是因为,在计算机中,一个负数通常会被表示为一个很大的整数,这样在采用Varint的编码方式时,对负数就几乎都需要使用较多的byte。所以,在定义数据类型时,需要留心指定正确的类型。这是使用Protobuf乃至Thrift时容易被忽略的细节,从而带来不必要的性能损失。

而从解包速度的角度来讲,相比于传统的XML或者JSON,由于是从巧妙编码的二进制数据流中读取,而且是根据数据定义协议自动生成的代码中获取各项数据,其过程不过几个简单的数学运算即可,而避免了较复杂的语法分析过程,自然是要快上很多。

在实际使用过程中,Protobuf和Thrift都需要针对数据定义协议生成代码,略微会显得有一点麻烦,而且针对Erlang的库似乎都有点不太好用和小问题存在。值得留意的是一个叫做Message Pack的新项目,这个项目在消息编码压缩和序列化反序列化过程中保持了Protobuf和Thrift的优点,但使用起来却更简单,不需要维护大量的数据格式定义文件和代码生成。在系统并不是那么复杂或需要灵活处理部分消息时,是个值得考虑选择的方案,部分语言平台上使用起来更方便些。缺点是对于复杂数据结构在一些支持特性上还并不完整,项目版本相对还比较低,在使用中可能会有一点风险。

如果要使用Erlang作为一个游戏网关,那么在消息的数据协议上,可以考虑优先选择Message Pack。