跳转到内容


FlatBuffers 高性能二进制序列化工具分析和使用

flatbuffers

  • 您无法回复此主题
No replies to this topic

#1 冰力

    Administrator

  • 总版主
  • 1269 帖子数:

发表于 2017/06/14 20:52:38

简介

google跨平台数据打包库——flatbuffers,目前支持C++, C#, C, Go, Java, JavaScript, PHP, and Python,最初设计应用于游戏等高性能场景。
优势
  • 能够在不解包的情况下直接使用序列化数据——能够直接使用二进制分层数据
  • 内存性能高效高速——只有buffer内存而无额外数据。能够很好的应用于mmap(或者streaming),他的存取速度在于虚表对optional字段的解析
  • 平滑性——Optional字段可以很好的向前向后兼容
  • 少量代码——通过少量代码就可以定义出完整的包含序列化和反序列化函数的头文件
  • 强类型——错误将会在编译器展示而不是在运行期出现
  • 使用便捷——生成的代码简洁明了,另外有参数支持解析表单或者类json数据来生成相应代码
  • 跨平台无依赖——生成的代码能够跨平台使用,没有其他依赖
与 Protocol Buffers相比

相比于protocol buffers, flatbuffers不需要解包的到额外的表示结构,而是可以直接获取数据,这样节省了各个数据结构的开销,代码量级将更小。另外, protocol buffers没有如import/export或者union这样的语法特征。
使用方法
  • 写一个表单,定义序列化结构
  • 使用flatc生成generate文件,这个文件唯一依赖flatbuffers.h
  • 使用FlatBufferBuilder类构建二进制流
  • 使用函数获取字段,通常会是object->field()
表单语法

首先看一个示例:
// example IDL file

namespace MyGame;

attribute "priority";

enum Color : byte { Red = 1, Green, Blue }

union Any { Monster, Weapon, Pickup }

struct Vec3 {
  x:float;
  y:float;
  z:float;
}

table Monster {
  pos:Vec3;
  mana:short = 150;
  hp:short = 100;
  name:string;
  friendly:bool = false (deprecated, priority: 1);
  inventory:[ubyte];
  color:Color = Blue;
  test:Any;
}

root_type Monster;
上例中涉及到大部分的语法,我们根据例子一一进行解释。
table

table语法是flatbuffers中最终要的语法之一,他定义了类,包括各种可变类型。上例中的Monster就是一个类,他罗列了他的属性列表,每个属性包含四个部分:属性名属性类型属性默认值属性限制,其中,默认值和限制可以省略。
每个属性都是一个optional,不必在表单中专门说明。对于每个特定的对象,可以选择忽略一些字段。因此,我们可以平滑的增加字段,而不必担心数据的暴涨,这就是flatbuffers向前向后兼容的原理。注意:
  • 你只能在表单后面增加新字段。老的数据在新代码中会正常读取,对新字段给予默认值;新的数据在老的代码中同样会正常读取,直接忽视掉新增加的字段。另外,如果如果想在表单中任意位置增加字段,需要额外指定每个字段的id来标示他的位置(和protocol buffers中一样)。
  • 对于不需要的字段,不能删除,实际上只需要不填充数据就可以达到相同的效果。当然,在使用c++时,你可以使用deprecated字段强制取消某一字段的使用,但需要注意的是,这种操作可能会对代码造成影响(实际上是定义函数失效)。
struct

和table一样可以定义一个对象,不同的是他的所有字段都是非optional并且无default值,字段不可增减。struct的字段只能是struct或者其他标量类型,建议确保不变的结构体使用这样的结构。性能上struct比table更高效。
types

在flatbuffers中,内建的标量类型有:
  • 8 bits: byte, ubyte, bool
  • 16 bits: short, ushort
  • 32 bits: int, uint, float
  • 64 bits: long, ulong, double
内建的非标量类型有:
  • Vector类型,语法为[type],不能够使用嵌套的vector,可以使用table进行代替
  • string类型仅支持UTF-8或者7-bit ASCII字符串,如果有其他的二进制字段,可以使用[byte]或者[ubyte]来代替
  • 嵌套引用其他的table, struct, enum, union
在使用时不能够改变字段的类型,只能通过reinterpret_cast的方式进行转换。
enum

枚举类型是一序列的常量,通常以0开始,逐次加1。另外,想要将枚举类型项定义为整型以下的类型(如上例中的byte),可以在枚举的后面以:来标示,那么枚举的所有项目都将按该类型定义。
union

union和enum类似,不同的是他针对的是table类型,而不是整型常量。通过union来引用各种table,然后根据_type(如上例中为Any_type())函数获得table对应的一个枚举值,从而在运行期确定对象到底是根据union中的那种类型来解析。
values (default)

数值类型是一个数字序列。数值类型可以通过一些数学符号来进行表示,包括., +, -, e或E,如-1e2。
只有标量值才会有默认值,非标量值(string,table, [])未引用时为NULL。
有些时候你希望用到默认值,但是通常默认值会在代码中生成,而不是在序列化数据中生成,这个时候我们需要在表单中修改,除非特别需求,否则尽量不要重构代码。
namespace

命名空间。有助于在c++或者java等语言中进行命名隔离。
include

我们可以在当前表单中包含其他表单,这有助于我们的数据结构模块化。include不会重复包含。需要注意的是:当使用flatc进行generate文件生成的时候,只会生成当前表单文件的头文件,而不会对其他表单文件进行处理。因此,我们需要分别对各个表单文件进行生成。
root_type

root_type定义了序列化数据的解析root,通常由union扮演,当然也可以是其他的类型。这个对于json数据的解析尤其重要,因为json数据通常不会包含对象的类型信息。
file_identifier和file_extension

文件标识。由于二进制流没有标识自己的特征,只能需要特定的root类型去进行解析。然而,当我们用文件格式来描述流的时候,我们可以增加一个文件标识来标识对应类型,在解析的时候通过很小的代价来进行解析。如:
file_identifier "MYFI";
文件标识必须是4个字符的长度,在buffers中会是第4-7位。
对于所有具有文件标识的表单,flatc会对表单中的指定的数据结构中增加对应的文件标识(一般表现为在Finish root_type Buffer函数中)。如果在这种情况下想去掉标识的话,那么需要使用函数FlatBufferBuilder::Finish来结束二进制流的构造。
在加载这样的二进制串之后,可以通过函数MonsterBufferHasIdentifier来验证是否有这样的标识。
通常这个标识会是在开放式的环境中使用,如将二进制流保存在文件中。如果在网络中使用不同的数据结构,我们一般会使用一个union结构体来包含所有的数据接口,然后通过上节讲述的方法进行解析。
另外,一般情况下文件保存的文件后缀名为.bin,我们可以使用file_extension来指定后缀名:
file_extension "ext";
RPC interface

flatbuffers另一个语法是在表单中定义RPC接口,他通过定义一个request和response对来实现RPC框架代码生成。如:
rpc_service MonsterStorage {
  Store(Monster):Stat (streaming: "none");
  Retrieve(Stat):Monster (idempotent);
}
目前,flatbuffers支持GRPC框架,在调用flatc生成代码时加上参数--grpc会生成框架代码。
attributes

字段属性值在定义字段或者非标量结构(table/struct/enum/union)时添加。一些属性可以由编译器解释,如deprecated;另一些用户定义属性(uda)需要在使用之前进行声明,如上例中的priority,然后在运行时通过query进行获取。这通常运用在自定义生成器时添加额外的一些信息,以便在程序运行期间能够实现一些自定义的策略。以下是flatbuffers能够解释的属性:
  • id:n(table field): 将该字段标识为n。一旦使用这个属性,你需要对所有的这个table中的所有字段都进行id标识,标识位是从零开始的自然数,逐一递增。另外,由于union实际上时两种类型,进行id标识时占用两个id位,并且以后一位进行标识,比如说,union之前的一位id为6,那么union本身占用7、8两位,以8来进行标识。通常来说,id字段的作用是使你的字段能够在表单中以任意顺序展示,然而添加新字段时必须使用下一个可用的id。
  • deprecated(table field): 标识字段废弃,阻止代码再次获取该字段。
  • required (on a non-scalar table field): 这个属性标明这个字段不能为空。实际上,这个字段会强制代码对字段进行初始化,而不再以NULL来表现。这样,当我们使用的时候就不再需要检查这个字段是不是空字段。这对数据结构的前后兼容和平滑性会有很大的帮助。
  • original_order (on a table): 一般情况下,table中的元素不会需要按照特定顺序进行排列,代码会根据size进行排序组合进行空间上的优化。当我们对table定义这样一个属性的时候,代码将按照原始顺序排列空间。
  • force_align : size (on a struct): 强制结构体对齐,在buffer中将会进行空间对齐表现。
  • bit_flags (on an enum): bit位方式,这将使枚举类型不再逐一递增,而是按照位右移的方式递增,当你指定对应枚举项的数字时,将会右移相应位,如enum Color:byte (bit_flags) { Red = 0, Green, Blue = 3, }将会得到enum Color { Color_Red = 1, Color_Green = 2, Color_Blue = 8, Color_NONE = 0, Color_ANY = 11};,否则的话将会按照1,2,4,8...的顺序进行取值。值得一说的是,这种情况下生成的枚举类型会额外添加None和Any两个枚举项,来对应表示无命中和全命中。
  • nested_flatbuffer: "table_name"(on a field): 这个属性表明该字段是嵌套的flatbuffers的二进制流数据,数据格式为指定的table名称。指定这个属性,在代码生成的时候会给出一个接口得到table对应的root类型。
  • key (on a field): 排序键值,当我们对vector中的值进行排序时会用到这个key。
JSON parsing

flatc支持json数据格式的解析。不同于其他的json解析器,flatbuffers的解析器要求强类型,解析结果直接到FlatBuffer。对于怎么解析json数据,除了要求的表单之外,还需要一些额外的改变:
  • 他接受没有引号的字段名,输出的格式中字段名也没有引号,指定strict_json时会添加引号
  • 如果字段有枚举类型,解析器能够识别枚举值。如果字段为整型,仍然能够使用枚举符号,但是需要枚举的前缀,并用引号包含,如field: "Enum.EnumVal"。对于多个枚举值的OR操作,我们用空格进行分隔,如field: "EnumVal1 EnumVal2"或者field: "Enum.EnumVal1 Enum.EnumVal2"
  • 类似的,对于共用体,我们在代码序列化时需要通过两个field来进行标识,第一个field标识类型,第二个field标识字段,如:test_type: Monster, test: { name: "Fred", pos: null },
  • 如果json字段中某一个字段的值为null,那么意味着这个字段值为默认值
  • json支持一些內建函数,目前支持rad, deg, cos, sin, tan, acos, asin, atan
  • 另外,json支持一些转义字符,常见的有\n, \r, \t, \b, \f, \", \\, \/, \u, \x
c++中的调用步骤

在c++里面,要使用flatbuffers,需要包含以下两个头文件:

#include "flatbuffers/flatbuffers.h"
#include "mystruct_generate.h"

load的流可以通过Get[RootType]来获得这样一个生成的函数来获得,对应的字段根据相应的函数获得。

//Monster作为root时
auto monster = GetMonster(buf);
monster->hp();
//当以共用体作为root时
///< union Any { Monster, Boss }
///< table Root { any : Any }
///< root_type : Root;
auto root = GetRoot(buf);
switch(root->any_type()) {
    case Any::Monster:
       auto monster = reinterpret_cast<const Monster *>(root->any()); 
       monster->hp();
    break;
}

当进行一个结构的构建时,需要以下几个步骤:

flatbuffers::FlatBufferBuilder fbb;
auto value = CreateMonster(fbb, args...);
FinishMonsterBuffer(fbb, value);
///< 对于非标量的参数,需要在使用之前先进行创建
string monster_name {"test"};
auto name = fbb.CreateString(monster_name.c_str());
vector<unsigned char> monster_inventory {0,1,2,3};
auto inventory = fbb.CreateVector(monster_inventory);
auto value = CreateMonster(fbb, ...,name,...,inventory,...);

其他语言中的调用

可以参考相关示例。