1. 为什么使用FlatBuffers
使用FlatBuffers的原因很简单,那就是简单、效率高和便利。
为了传输数据,我们做了不少努力,研制出不少编解码方法,如:BER、PER、JSON、BSON、XML、HTML等。然而,不管使用何种方法,最终都是直接对数据进行操作,中间的编码和解码运算似乎是多余的。奥卡姆剃刀告诉我们:“如无必要,勿增实体”。在不增加实体的情况下,直接对数据进行操作,这就是FlatBuffers的开发目的。FlatBuffers仅仅增加了VTable和偏移量两个实体。
FlatBuffers很简单,使用起来一点都不难。首先定义好协议文件(schema
),然后使用工具编译成源码文件,然后就可以直接调用源码的接口来操作数据。
FlatBuffers的效率很高。FlatBuffers数据在缓冲区内都是平整的,可以直接访问。
FlatBuffers使用起来很便利。协议文件设计好后就可以发布。任何人拿到这个文件都可以编译成源码来对数据进行操作。这对团队开发很有利。另外使用FlatBuffers不用担心协议文件设计不周到的问题,因为你可以随意往Table里面添加或者删除成员。
2. 为什么不使用FlatBuffers
因为我不关心效率,所以我不使用FlatBuffers。但是能省点CPU资源也没什么不好,至少可以多跑几个服务。
FlatBuffers传输的数据较大,所以我不使用FlatBuffers。其实可以对数据进行压缩再传输。
因为目前开发的项目都使用XML和HTTP,所以我不使用FlatBuffers。建议能在新的模块中使用FlatBuffers。
因为打包后的FlatBuffers中的矢量数据不能随意修改,感觉不爽,所以不使用。对于一般的应用来说,打完包后就会立即发送,很少会修改数据的。
因为目前FlatBuffers的union类型不支持大于256个类型,然而项目中的union将包含超过256个类型的数据结构,所以我不使用FlatBuffers。对于这个问题,可以使用二级或者多级union来解决,即由多个union来实现,如二级union可以至少支持65536个类型。
FlatBuffers传输的数据没有JSON、XML和HTML那么直观,所以不想使用。对于这个问题,可以在收包程序进行LOG来弥补,可以通过查看LOG来获知传输的内容。另外,由于不是明文传输,所以一般的人,如果没有协议文件,即使截取到数据包还得需要花一定的力气才能解开,有利于保密。
FlatBuffers目前没有办法生成C源码,我们的项目使用的都是C源码。对于这个问题,我相信不久就会有版本能生成C源码。
3. FlatBuffers详解
FlatBuffers其实就是一个保存了一系列标量和矢量的缓冲区。这个缓冲区中的标量和矢量可以被直接访问。
缓冲区的数据一旦构造成功,里面的矢量数据一般不能变更,除非矢量的长度不大于构造时的长度,且矢量保存的不是偏移量,否则会产生错误。
3.1. 标量
所有的整形变量(8位~64位)和浮点变量均为标量。标量的特点是长度固定,字节序列为LittleEndian,这和大部分CPU的一样,以加快访问速度。
FlatBuffers中的偏移量也是标量,但是在构造后不能变更。
Struct结构也可以当做是标量来看待。
3.2. 矢量
字符串和数组是矢量。字符串是以'\0'结尾。矢量的开头必须是一个32位的长度,用来指明矢量的长度,这个长度不包括'\0'和长度本身所占的空间。
图 1
如图1所示,STRING和VECTOR都是矢量,唯一的区别是STRING包含一个'\0'结束符合。VECTOR SIZE保存的是VECTOR ELEMENTS的长度,单位是字节。
如果VECTOR ELEMENTS是标量或者STRUCT,那么其中保存的内容就是其数组中的内容;如果是TABLE,那么保存的就是一个偏移量数组,这些偏移量为32位,指向TABLE OBJECT。
3.3. 数据结构
3.3.1. Struct
Struct类型的数据结构是不可以更改的结构。当结构定义好,结构的成员、位置和大小就会固定,不能变更,否则所有的程序都要重新编译和升级。Struct的优点是访问速度快,占用内存少。
3.3.2. Table
Table类型可以随意增加和删除成员,是一个很灵活的类型。当要删除一个成员时,你把它指定为deprecated即可(这个成员必须保存在成员列表内,不能删除)。
图 2
如图2所示,一个VTABLE可以为多个TABLE OBJECT提供描述信息,每个TABLE OBJECT必须包含一个32位的TABLE OFFSET,指定哪个VTABLE描述它的字段信息。
每个VTABLE都有一个16位的VTABLE SIZE,用来记录本身的大小(包括VTABLE SIZE);TABLE OBJECT SIZE用来记录TABLE OBJECT的大小(包括VTABLE OFFSET这4个字节);TABLE FIELD OFFSET用来记录TABLE INSTANCE内的字段相对TABLE OBJECT的偏移量,即TABLE INSTANCE的第一个字段偏移量必须为4(因为VTABLE OFFSET为32位,占了4个字节)。
每个TABLE INSTANCE保存一系列字段FIELD,这些FIELD可以是普通标量或者struct,也可以是32位的偏移量,指向其它TABLE或者矢量。
3.3.3. ROOT TYPE
FlatBuffers有一个特殊类型就是ROOT TYPE,是一个顶级类型。
图 3
如图3所示,ROOT OFFSET是一个32位的偏移量,指向ROOT DATA;ROOT DATA为封包内容。FILE ID为可选内容,可以不设置,可以用作这个封包的ID,注意无'\0'结尾,可以在协议文件中使用file_identifier来指定。
ROOT DATA为整个封包的数据部分,然而ROOT OFFSET未必指向ROOT DATA的开头,它是指向顶层TABLE的TABLE OBJECT,而不是指向VTABLE。因此,即使不填FILE ID,ROOT DATA的值也可能不为0。
3.3.4. Union
联合类型是一个重要的类型,是封包分支的重要方法。
图4
如图4所示,每个union有两个部分组成,一个部分是TYPE,另外一个是TABLE偏移量。
因为TYPE只要8位,所以联合类型里面最多只能包含256个TABLE类型,否则会出问题。如果想支持大于256个类型,那么必须通过union+table+union这种方法来扩充,如:
// 第二层union
table T2_1_t { // 分支1
A1:int;
}
table T2_2_t { // 分支2
A2:int;
}
union U2_1_t {
T2_1_t,
T2_2_t
}
table T2_3_t { // 分支3
A1:int;
}
table T2_4_t { // 分支4
A2:int;
}
union U2_2_t {
T2_3_t,
T2_4_t
}
// 第一层union
table T1_1_t {
u:U2_1_t ( id: 1 ); // 注意,由于union的类型占一个id,所以这里是1,不是0;0已经默认分配给type了
}
table T1_2_t {
u:U2_2_t ( id: 1 ); // 注意,由于union的类型占一个id,所以这里是1,不是0;0已经默认分配给type了
}
union U1_1_t { //可以至少支持多达256*256=65536个消息分支
T1_1_t,
T1_2_t
}
// 应用,这个Table可以至少保存多达256*256=65536种内容
table T_t {
u:U1_1_t ( id: 1 );// 注意,由于union的类型占一个id,所以这里是1,不是0;0已经默认分配给type了
}
4. 总结
FlatBuffers构造的数据总的来说是只要两种,即固定长度数据和可变长度数据,或者说是标量和矢量。可以认为struct也是标量,因为struct的大小也是固定的。在构造数据时,所有的标量都是直接写到缓冲区去的,所有的矢量需要先写到缓冲区,然后获得偏移量(32位),然后再把偏移量写到适当的结构中,每个矢量都离不开一个偏移量。
FlatBuffers和ProtoBuffers都是谷歌开发的开源产品。相对于ProtoBuffers已经在谷歌内部得到广泛的应用,FlatBuffers却是一个新生的事物。然而,FlatBuffers的反序列化速度是ProtoBuffers的百倍,相信将会得到越来越多的应用。
本文是在阅读FlatBuffers的源码和文档的基础上写出来的,如果有不正确的地方,请告知我