FlatBuffers 是一个序列化开源库,实现了与 Protocol Buffers,Thrift,Apache Avro,SBE 和 Cap'n Proto 相似的序列化格式,主要由 Wouter van Oortmerssen 编写,并由 Google 开源。Oortmerssen 最初为 Android 游戏和注重性能的应用而开发了FlatBuffers。如今它具备C ++,C#,C,Go,Java,PHP,Python 和 JavaScript 的端口。html
FlatBuffer 是一个二进制 buffer,它使用 offset 组织嵌套对象(struct,table,vectors,等),可使数据像任何基于指针的数据结构同样,就地访问数据。然而 FlatBuffer 与大多数内存中的数据结构不一样,它使用严格的对齐规则和字节顺序来确保 buffer 是跨平台的。此外,对于 table 对象,FlatBuffers 提供前向/后向兼容性和 optional 字段,以支持大多数格式的演变。android
FlatBuffers 的主要目标是避免反序列化。这是经过定义二进制数据协议来实现的,一种将定义好的将数据转换为二进制数据的方法。由该协议建立的二进制结构能够 wire 发送,而且无需进一步处理便可读取。相比较而言,在传输 JSON 时,咱们须要将数据转换为字符串,经过 wire 发送,解析字符串,并将其转换为本地对象。Flatbuffers 不须要这些操做。你用二进制装入数据,发送相同的二进制文件,并直接从二进制文件读取。git
尽管 FlatBuffers 有本身的接口定义语言来定义要与之序列化的数据,但它也支持 Protocol Buffers 中的 .proto
格式。github
在 schema 中定义对象类型,而后能够将它们编译为 C++ 或 Java 等各类主流语言,以实现零开销读写。FlatBuffers 还支持将 JSON 数据动态地分析到 buffer 中。算法
除了解析效率之外,二进制格式还带来了另外一个优点,数据的二进制表示一般更具备效率。咱们可使用 4 字节的 UInt 而不是 10 个字符来存储 10 位数字的整数。编程
JSON 是一种独立于语言存在的数据格式,可是它解析数据并将之转换成如 Java 对象时,会消耗咱们的时间和内存资源。客户端解析一个 20KB 的 JSON 流差很少须要 35ms,而 UI 一次刷新的时间是 16.6ms。在高实时游戏中,是不能有任何卡顿延迟的,因此须要一种新的数据格式;服务器在解析 JSON 时候,有时候会建立很是多的小对象,对于每秒要处理百万玩家的 JSON 数据,服务器压力会变大,若是每次解析 JSON 都会产生不少小对象,那么海量玩家带来的海量小对象,在内存回收的时候可能会形成 GC 相关的问题。Google 员工 Wouter van Oortmerssen 为了解决游戏中性能的问题,因而开发出了 FlatBuffers。(注:Protocol buffers 是 created by google,而 FlatBuffers 是 created at google)json
几年前,Facebook 宣称本身的 Android app 在数据处理的性能方面有了极大的提高。在几乎整个 app 中,他们放弃了 JSON 而用 FlatBuffers 取而代之。api
FlatBuffers (9490 star) 和 Cap'n Proto (5527 star)、simple-binary-encoding (1351 star) 同样,它支持“零拷贝”反序列化,在序列化过程当中没有临时对象产生,没有额外的内存分配,访问序列化数据也不须要先将其复制到内存的单独部分,这使得以这些格式访问数据比须要格式的数据(如JSON,CSV 和 protobuf)快得多。数组
FlatBuffers 与 Protocol Buffers 确实比较类似,主要的区别在于 FlatBuffers 在访问数据以前不须要解析/解包。二者代码也是一个数量级的。可是 Protocol Buffers 既没有可选的文本导入/导出功能,也没有 union 这个语言特性,这两点 FlatBuffers 都有。安全
FlatBuffers 专一于移动硬件(内存大小和内存带宽比桌面端硬件更受限制),以及具备最高性能需求的应用程序:游戏。
说了这么多,读者会疑问,FlatBuffers 使用的人多么?Google 官方页面上提了 3 个著名的 app 和 1 个框架在使用它。
BobbleApp,印度第一贴图 App。BobbleApp 中使用 FlatBuffers 后 App 的性能明显加强。
Facebook 使用 FlatBuffers 在 Android App 中进行客户端服务端的沟通。他们写了一篇文章《Improving Facebook's performance on Android with FlatBuffers》来描述 FlatBuffers 是如何加速加载内容的。
Google 的 Fun Propulsion Labs 在他们全部的库和游戏中大量使用 FlatBuffers。
Cocos2d-X,第一开源移动游戏引擎,使用 FlatBuffers 来序列化全部的游戏数据。
因而可知,在游戏类的 app 中,普遍使用 FlatBuffers。
编写一个 schema 文件,容许您定义您想要序列化的数据结构。字段能够有标量类型(全部大小的整数/浮点数),也能够是字符串,任何类型的数组,引用另外一个对象,或者一组可能的对象(Union)。字段能够是可选 optional 的也能够有默认值,因此它们不须要存在于每一个对象实例中。
举个例子:
// 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;
复制代码
上面是 schema 语言的语法,schema 又名 IDL(Interface Definition Language,接口定义语言),代码和 C 家族的语言很是像。
在 FlatBuffers 的 schema 文件中,有两个很是重要的概念,struct 和 table 。
Table 是在 FlatBuffers 中定义对象的主要方式,由一个名称(这里是 Monster)和一个字段列表组成。每一个字段都有一个名称,一个类型和一个可选的默认值(若是省略,它默认为 0 / NULL)。
Table 中每一个字段都是可选 optional 的:它没必要出如今 wire 表示中,而且能够选择省略每一个单独对象的字段。所以,您能够灵活地添加字段而不用担忧数据膨胀。这种设计也是 FlatBuffer 的前向和后向兼容机制。
假设当前 schema 是以下:
table { a:int; b:int; }
复制代码
如今想对这个 schema 进行更改。
有几点须要注意:
只能在表定义的末尾添加新的字段。旧数据仍会正确读取,并在读取时为您提供默认值。旧代码将简单地忽略新字段。若是但愿灵活地使用 schema 中字段的任何顺序,您能够手动分配 ids(很像 Protocol Buffers),请参阅下面的 id 属性。
举例:
table { a:int; b:int; c:int; }
复制代码
这样作能够。旧的 schema 读取新的数据结构会忽略新字段 c 的存在。新的 schema 读取旧的数据,将会取到 c 的默认值(在此状况下为 0,由于未指定)。
table { c:int a:int; b:int; }
复制代码
在前面添加新字段是不容许的,由于这会使 schema 新旧版本不兼容。用老的代码读取新的数据,读取新字段 c 的时候,其实读到的是老的 a 字段。用新代码读取老的数据,读取老字段 a 的时候,其实读到的是老的 b 字段。
table { c:int (id: 2); a:int (id: 0); b:int (id: 1); }
复制代码
这样作是可行的。若是您的意图是以有意义的方式对语义进行排序/分组,您可使用显式标识赋值来完成。引入 id 之后,table 中的字段顺序就无所谓了,新的与旧的 schema 彻底兼容,只要咱们保留 id 序列便可。
不能从 schema 中删除再也不使用的字段,但能够简单地中止将它们写入数据中,和写入和删除字段,两种作法几乎相同的效果。此外,能够将它们标记为 deprecated,如上例所示,被标记的字段不会再生成 C ++ 的访问器,从而强制该字段再也不被使用。 (当心:这可能会破坏代码!)。
table { b:int; }
复制代码
这种删除字段的方法不可行。咱们只能经过弃用来删除某个字段,而无论是否使用了明确的ID 标识。
table { a:int (deprecated); b:int; }
复制代码
上面这样的作法也是能够的。旧的 schema 读取新的数据结构会得到 a 的默认值,由于它不存在。新的 schema 代码不能读取也不能写入 a(现有代码尝试这样作会致使编译错误),但仍能够读取旧数据(它们将忽略该字段)。
能够更改字段名称和 table 名称,若是您的代码能够正常工做,那么您也能够更改它们。
table { a:uint; b:uint; }
复制代码
直接修改字段的类型,这样作可能可行,也有状况不行。只有在类型改变是相同大小的状况下,是可行的。若是旧数据不包含任何负数,这将是安全的,若是包含了负数,这样改变会出现问题。
table { a:int = 1; b:int = 2; }
复制代码
这样修改不可行。任何写入数值为 0 的旧数据都不会再写入 buffer,并依赖于从新建立的默认值。如今这些值将显示为1和2。有些状况下可能不会出错,但必须当心。
table { aa:int; bb:int; }
复制代码
上面这种修改方法,修改原来的变量名之后,可能会出现问题。因为已经重命名了字段,这将破坏全部使用此版本 schema 的代码(和 JSON 文件),这与实际的二进制缓冲区不兼容。
table 是 FlatBuffers 的基石,由于对于大多数须要序列化应用来讲,数据结构改变是必不可少的。一般状况下,处理数据结构的变动在大多数序列化解决方案的解析过程当中能够透明地完成的。可是一个 FlatBuffer 在被访问以前不会被分析。
为了解决数据结构变动的问题,table 经过 vtable 间接访问字段。每一个 table 都带有一个 vtable(能够在具备相同布局的多个 table 之间共享),而且包含存储此特定类型 vtable 实例的字段的信息。vtable 还可能代表该字段不存在(由于此 FlatBuffer 是使用旧版本的软件编写的,仅仅由于信息对于此实例不是必需的,或者被视为已弃用),在这种状况下会返回默认值。
table 的内存开销很小(由于 vtables 很小而且共享)访问成本也很小(间接访问),可是提供了很大的灵活性。table 甚至可能比等价的 struct 花费更少的内存,由于字段在等于默认值时不须要存储在 buffer 中。
structs 和 table 很是类似,只是 structs 没有任何字段是可选的(因此也没有默认值),字段可能不会被添加或被弃用。结构可能只包含标量或其余结构。若是肯定之后不会进行任何更改(如 Vec3 示例中很是明显),请将其用于简单对象。structs 使用的内存少于 table,而且访问速度更快(它们老是以串联方式存储在其父对象中,而且不使用虚拟表)。
structs 不提供前向/后向兼容性,但占用内存更小。对于不太可能改变的很是小的对象(例如坐标对或RGBA颜色)存成 struct 是很是有用的。
FlatBuffers 支持的 标量 类型有如下几种:
括号里面的名字对应的是类型的别名。
FlatBuffers 支持的 非标量 类型有如下几种:
标量类型的字段有默认值,非标量的字段(string/vector/table)若是没有值的话,默认值为 NULL。
一旦一个类型声明了,尽可能不要改变它的类型,一旦改变了,极可能就会出现错误。上面也提到过了,若是把 int 改为 uint,数据若是有负数,那么就会出错。
定义一系列命名常量,每一个命名常量能够分别给一个定值,也能够默认的从前一个值增长一。默认的第一个值是 0。正如在上面例子中看到的枚举声明,使用:(上面例子中是 byte 字节)指定枚举的基本整型,而后肯定用这个枚举类型声明的每一个字段的类型。
一般,只应添加枚举值,不要去删除枚举值(对枚举不存在弃用一说)。这须要开发者代码经过处理未知的枚举值来自行处理向前兼容性的问题。
这个是 Protocol buffers 中还不支持的类型。
union 是 C 语言中的概念,一个 union 中能够放置多种类型,共同使用一个内存区域。
可是在 FlatBuffers 中,Unions 能够像 Enums 同样共享许多属性,但不是常量的新名称,而是使用 table 的名称。能够声明一个 Unions 字段,该字段能够包含对这些类型中的任何一个的引用,即这块内存区域只能由其中一种类型使用。另外还会生成一个带有后缀 _type
的隐藏字段,该字段包含相应的枚举值,从而能够在运行时知道要将哪些类型转换为类型。
union 跟 enum 比较相似,可是 union 包含的是 table,enum 包含的是 scalar或者 struct。
Unions 是一种可以在一个 FlatBuffer 中发送多种消息类型的好方法。请注意,由于union 字段其实是两个字段(有一个隐藏字段),因此它必须始终是表的一部分,它自己不能做为 FlatBuffer 的 root。
若是须要以更开放的方式区分不一样的 FlatBuffers,例如文件,请参阅下面的文件标识功能。
最后还有一个实验功能,只在 C++ 的版本实现中提供支持,如上面例子中,把 [Any] (联合体数组) 做为一个类型添加到了 Monster 的 table 定义中。
这声明了您认为是序列化数据的根表(或结构)。这对于解析不包含对象类型信息的 JSON 数据尤其重要。
一般状况下,FlatBuffer 二进制缓冲区不是自描述的,即它须要您了解其 schema 才能正确解析数据。可是若是你想使用一个 FlatBuffer 做为文件格式,那么可以在那里有一个“魔术数字”是很方便的,就像大多数文件格式同样,可以作一个完整的检查来看看你是否阅读你指望的文件类型。
FlatBuffer 虽然容许开发者能够在 FlatBuffer 前加上本身的文件头,但 FlatBuffers 有一种内置方法,可让标识符占用最少空间,而且还能使 FlatBuffer 与不具备此类标识符的 FlatBuffer 相互兼容。
声明文件格式的方法相似于 root_type:
file_identifier "MYFI";
复制代码
标识符必须正好 4 个字符。这 4 个字符将做为 buffer 末尾的 [4,7] 字节。
对于具备这种标识符的任何 schema,flatc 会自动将标识符添加到它生成的任何二进制文件中(带-b),而且生成的调用如 FinishMonsterBuffer 也会添加标识符。若是你已经指定了一个标识符并但愿生成一个没有标识符的缓冲区,你能够经过直接显示调用FlatBufferBuilder :: Finish 来完成这一目的。
加载缓冲区数据之后,可使用像 MonsterBufferHasIdentifier 这样的调用来检查标识符是否存在。
给文件添加标识符是最佳实践。若是只是简单的想经过网络发送一组可能的消息中的一个,那么最好用 Union。
默认状况下,flatc 会将二进制文件输出为 .bin
。schema 中的这个声明会将其改变为任何你想要的:
file_extension "ext";
复制代码
RPC 声明了一组函数,它将 FlatBuffer 做为入参(request)并返回一个 FlatBuffer 做为 response(它们都必须是 table 类型):
rpc_service MonsterStorage {
Store(Monster):StoreResponse;
Retrieve(MonsterId):Monster;
}
复制代码
这些产生的代码以及它的使用方式取决于使用的语言和 RPC 系统,能够经过增长 --grpc
编译参数,代码生成器会对 GRPC 有初步的支持。
Attributes 能够附加到字段声明,放在字段后面或者 table/struct/enum/union 的名称以后。这些字段可能有值也有可能没有值。
一些 Attributes 只能被编译器识别,好比 deprecated。用户也能够定义一些 Attributes,可是须要提早进行 Attributes 声明。声明之后能够在运行时解析 schema 的时候进行查询。这个对于开发一个属于本身的代码编译/生成器来讲是很是有用的。或者是想添加一些特殊信息(一些帮助信息等等)到本身的 FlatBuffers 工具之中。
目前最新版能识别到的 Attributes 有 11 种。
id:n
(on a table field)deprecated
(on a field)required
(on a non-scalar table field)force_align: size
(on a struct)bit_flags
(on an enum)nested_flatbuffer: "table_name"
(on a field)flexbuffer
(on a field)key
(on a field)hash
(on a field)original_order
(on a table)native_inline
、native_default
、native_custom_alloc
、native_type
、native_include: "path"
。FlatBuffers 是一个高效的数据格式,但要实现效率,您须要一个高效的 schema。如何表示具备彻底不一样 size 大小特征的数据一般有多种选择。
因为 FlatBuffers 的灵活性和可扩展性,将任何类型的数据表示为字典(如在 JSON 中)是很是广泛的作法。尽管能够在 FlatBuffers(做为具备键和值的表的数组)中模拟这一点,但这对于像 FlatBuffers 这样的强类型系统来讲,这样作是一种低效的方式,会致使生成相对较大的二进制文件。在大多数系统中,FlatBuffer table 比 classes/structs 更灵活,由于 table 在处理 field 数量很是多,可是实际使用只有其中少数几个 field 这种状况,效率依旧很是高。所以,组织数据应该尽量的组织成 table 的形式。
一样,若是可能的话,尽可能使用枚举的形式代替字符串。
FlatBuffers 中没有继承的概念,因此想表示一组相关数据结构的方式是 union。可是,union 确实有成本,另一种高效的作法就是创建一个 table 。若是这些数据结构有不少类似或者能够共享的 field ,那么建议一个 table 是很是高效的。在这个 table 中包含全部数据结构的全部字段便可。高效的缘由就是 optional 字段是很是廉价的,消耗少。
FlatBuffers 默承认以支持存放的下全部整数,所以尽可能选择所需的最小大小,而不是默认为 int/long。
能够考虑用 buffer 中一个字符串或者 table 来共享一些公共的数据,这样作会提升效率,所以将重复的数据拆成共享数据结构 + 私有数据结构,这样作是很是值得的。
FlatBuffers 是支持解析 JSON 成本身的格式的。即解析 schema 的解析器一样能够解析符合 schema 规则的 JSON 对象。因此和其余的 JSON 解析器不一样,这个解析器是强类型的,而且解析结果也只是 FlatBuffers。具体作法请参照 flatc 文档和 C++ 对应的 FlatBuffers 文档,查看如何在运行时解析 JSON 成 FlatBuffers。
为了解析 JSON,除了须要定义一个 schema 之外,FlatBuffers 的解析器还有如下这些改变:
strict_json
标志输出它们。foo_type:FooOne
,FooOne 就是能够在 union 以外使用的 table。解析JSON时,解析器识别字符串中的如下转义码:
\n
- 换行。
\t
- 标签。
\r
- 回车。
\b
- 退格。
\f
- 换页。
\“
- 双引号。
\\
- 反斜杠。
\/
- 正斜杠。
\uXXXX
- 16位 unicode,转换为等效的 UTF-8 表示。
\xXX
- 8 位二进制十六进制数字 XX。这是惟一一个不属于 JSON 规范的地方(请参阅json.org/),可是须要可以将字符串中的任意二进制编码为文本并返回而不丢失信息(例如字节 0xFF 就不能够表示为标准的 JSON)。
当从二进制再反向表示生成 JSON 时,它还会再次生成这些转义代码。
schema 中的标识符是为了翻译成许多不一样的编程语言,因此把 schema 的编码风格改为和当前项目语言使用的风格,是一种错误的作法。应该让 schema 的代码风格更加通用。
还有 2 条关于书写格式的建议:
:
两边没有空格,=
两边各一个空格。
大多数可序列化格式(例如 JSON 或 Protocol Buffers)对于某个字段是否存在于某个对象中是很是明确,能够将其用做“额外”信息。
可是在 FlatBuffers 中,除了标量值以外,这也适用于其余全部内容。 FlatBuffers 默认状况下不会写入等于默认值的字段(对于标量),这样能够节省大量空间。 然而,这也意味着测试一个字段是否“存在”有点没有意义,由于它不会告诉你,该字段是不是经过调用add_field 方法调来 set 的,除非你对非默认值的信息感兴趣。默认值是不会写入到 buffer 中的。
可变的 FlatBufferBuilder 实现了一个名为 force_defaults 的方法,能够避免这种行为,由于即便与默认值相等,也会写入字段。而后可使用 IsFieldPresent 来查询 buffer 中是否存在某个字段。
另外一种方法是将标量字段包装在 struct 中。这样,若是它不存在,它将返回 null。这种方法厉害的是,struct 不会占用比它们所表明的标量更多的空间。
读完本篇 FlatBuffers 编码原理之后,读者应该能明白如下几点:
与 protocol buffers 相比,FlatBuffers 的数据结构定义文件,功能上有如下一些“改进”:
.proto
中扩展一个对象,须要在数字中寻找一个空闲的空位(由于 protocol buffers 有更紧凑的表示方式,因此必须选择更小的数字)。除了这点不方便以外,它还使得删除字段成为问题:若是保留它们,从语意表达上不是很明显的表达出这个字段不能读写了,保留它们,还会生成访问器。若是删除它们,就会有出现严重 bug 的风险,由于当有人重用了这些 ID,会致使读取到旧的数据,这样数据会发生错乱。除去功能上的不一样,再就是一些 schema 语法上的细微不一样:
关于 schema 全部的语法,能够参考这个文档
关于 flatbuffers 编解码性能相关的,原理分析和源码分析,将在下篇进行。
Reference:
flatbuffers 官方文档
Improving Facebook's performance on Android with FlatBuffers
GitHub Repo:Halfrost-Field
Follow: halfrost · GitHub
Source: halfrost.com/flatbuffers…