FlatBuffer内部解析原理简介
程序员文章站
2022-07-13 16:19:48
...
简介
Flatbuffer 是一个高效的跨平台、支持多种语言序列化数据的库。最初由谷歌为游戏开发而开发的,现在也用于多种对性能要求严苛的应用中。FlatBuffer有以下优点(直接翻译官网文档,详细介绍看这里):
- 可不需要解析、拆包,而直接访问序列化后的数据;
- 内存利用率高以及读取速度快;
- 灵活性;
- 生成代码量小;
- 强类型;
- 使用方便;
- 代码跨平台无其他依赖。
关于使用方法就不多介绍了,对于源码编译、Schema的编译官网有详细介绍,点击这里可以了解更多使用方法。这里想介绍的是解析一个已经序列化的数据文件的方法。其实如果只是想要知道怎么使用FlatBuffer,这部分内容是没必要了解的,但是好奇心的驱使下,还是想一探究竟。这是一篇个人理解文章,如果想看官方介绍请戳这里。
闲言少叙,书归正传。
铺垫
为了便于理解,我在Ubuntu16.04下下载源码并编译出了FlatBuffer编译器flatc
, 通过--binary
选项将一个JSON
数据文件编译成一个二进制文件Monster.bin
,用到的Schema和数据如下:
Monster.fbs
// Example IDL file for our monster's schema.
namespace MyGame.Sample;
enum Color:byte { Red = 0, Green, Blue = 2 }
union Equipment { Weapon } // Optionally add more tables.
struct Vec3 {
x:float;
y:float;
z:float;
}
table Monster {
pos:Vec3; // Struct.
mana:short = 150;
hp:short = 30;
name:string;
friendly:bool = false (deprecated);
inventory:[ubyte]; // Vector of scalars.
color:Color = Blue; // Enum.
weapons:[Weapon]; // Vector of tables.
equipped:Equipment; // Union.
path:[Vec3]; // Vector of structs.
}
table Weapon {
name:string;
damage:short;
}
root_type Monster;
Monster.json
{ pos: { x: 1, y: 2, z: 3 }, name: "fred", hp: 30}
二进制文件内容
正文
概念介绍
FlatBuffer中有几个主要的概念:
- Struct: 在Schema中表现为
struct
定义。主要是为了获得极致的访问效率,Struct中的内容紧凑的排列在一起,内联在其所属的父类容器中,也就是可以直接得到其中的内容而不需要额外计算保存的偏移地址;举个不恰当的栗子:有个强迫症患者他的书柜中有《语文》、《数学》、《英语》三本书,这三本书在强迫症的作用下一定是按照这个顺序摆放的,任何时候你找到语文书的位置,你肯定知道下一本是数学,就算闭着眼你也知道拿到的第三本肯定是英语。 - Table: 在Schema中表现为
table
定义。table
中定义的元素存储的位置是不确定的,对于同一个数据不同的实现方法对于同一字段的存放位置可能完全不一样,为了确定某一字段的具体位置,需要通过一个VTable来获取。这里再来个栗子:假设有两个图书馆,拥有数量和内容一样的书,哪个强迫症也是不可要求两个图书馆同样的书摆放在一样的位置。这回就算你找到了语文书的位置,你也是不能保证下一本就是数学,闭着眼睛拿也可能拿到《母猪的产后护理》,这就乱套了,那怎么办,只能通过记在一张纸上的索书号来找,这张纸就相当于vtable
吧。table
通过用它的起始地址减去开头用一个soffset_t
类型的数据来得到Vtable的地址,例如,table
起始地址是16,soffset_t
类型的值为12,那么table
所对应的vtable
的起始地址addr=16-12=4
,即从地址4开始。 - Vector: 在Schema中表现为一个列表。String就是一个特化的Vector,一个用于保存字符的vector。
- Offset: 用于找到各种内容的偏移地址。类似于军师给了我一个锦囊让我到某地;到达某地后我又打开第二个锦囊,按照锦囊的内容再干点啥,然后敌军的人头就是我的了。每个文件开头用一个
uoffset_t
类型来表示跟对象的的其实位置,例如在Monster.fbs
中根对象就是root_type
所指得tableMonster
- Vtable: 一个保存
table
各个字段相对于table
起始地址的偏移地址。vtable
中所有元素类型都是voffset_t
, 也就是uint16_t
,如下图。第一个元素用来表示整个vtable的字节数,包括第一个元素所占的;第二个元素表示整个vtable所表示的对象的大小;接下来的元素便是按照Schema中定义的顺序,例如Schema顺序定义了a,b,c三个字段,那么接下来的三个元素就分别指示a,b,c相对于table
起始地址的偏移地址,如果某个元素为0,那么说明这个元素在table中没有定义,获取到的将是默认值。如果某个字段已经超出了vtable的长度,也说明在这个对象中没有该字段,例如如果vtable中只有a,b两个元素,那么说明c定义的字段使用了默认值。
栗子来啦
有以上概念,根据铺垫
一节中所给出的数据,我们可以从凡人脱胎成CPU了。
- 二进制文件开头四个字节表示跟对象地址,因为数据是以小端格式存储的那么
0x10000000
转换成十进制就是16,说明根对象从第17个字节开始; - table对象开头四个字节为
0x0c000000
,转换成十进制就是12,16-12=4,所以vtable地址为4; - 找到地址4,第一个元素为vtable长度,由于vtable元素的类型都是
voffset_t
,在这里就是两个字节,值为0x0c00
,转换为十进制为12,说明vtable中一共包含12个字节;第三第四个字节这里用不到; - vtable第3,4个字节为
0x0800
,转成十进制为8,也就是从table起始位置16开始,再偏移8个字节得到的是Schema中定义的Vec
字段,因为Vec
是struct,是内联的,所以可以知道从24到35这12个字节每四个字节一组依次存储Vec中的三个浮点数x,y,z,分别等于1.0,2.0,3.0; - vtable第5,6个字节值为0,说明table Monster的第二个字段
mana
在这个二进制文件中不存在,获取到的将是默认值150; - vtable第7,8个字节的值为6,也就是从table起始位置16开始,再偏移6个字节得到的是Schema中定义的
hp
字段的内容,short
类型占两个字节,也就是从地址22开始区两个字节得到0x1e00
,转成十进制为30; - vtable第9,10个字节值为20,从table起始位置16开始,再偏移20个字节,也就是地址36开始得到的是Schema中定义的
name
字段的内容,由于name
是一个vector,所以得到的内容将是其相对于地址36的偏移,0x04000000
转为十进制是4,即name
实际内容的开始地址为40; - vector开头固定四个字节表示vector长度,多以40至43四个字节存储长度,
0x04000000
转为十进制为4,表示这个vecto里面有四个元素,由于此vector中保存的是是字节,所以44至47保存的是f r e d
这四个字符的ASCII码点。剩下多余的字节分别是一个终止符和填充字符。
至此,我们对这个二进制文件的解析结束。
总结
- FlatBuffer以小端格式(little-endian)存储;
- 二进制开头
uoffset_t
保存跟对象其实地址,具体占字节数视情况而定; - 每个对象开头
soffset_t
保存其vtable的偏移地址,计算方法是vtable = uoffset_t - soffset_t
; - 对象中嵌套的对象,其字段存储的值是该对象的地址而不是实际对象内容,例如:table对象中含有的一个
name
字段是vector对象,那么时期vector的存储对象地址为table_addr + vtable[name] + name
,例如:table其实地址为16,查vtable表name字段地址偏移为6,读取16 + 6 处内容为16,则实际Vector存储地址为16 + 6 + 16 = 38,即地址38为Vector起始地址; - 对于vector中存储的table的值得查找,也是从该地址开始,通过紧跟着的
uoffset_t
获取该对象的跟位置,进而获取vtable。例如Vectorweapons
中保存的是tableWeapon
,当找到weapons
的第一个元素的地址,假设地址为x
,那么第一个元素对象的根即等于x + uoffset_t
,例如通过上面第四点算得Vectorweapons
实际存储地址为96,第一个元素起始地址为100即为x
,若紧随地址100的uoffset_t
为24则第一个tableWeapon
的根地址为124;
最后用一张图结束:
本文首发于个人公众号TensorBoy。如果你觉得内容还不错,欢迎分享并关注我的公众号TensorBoy,扫描下方二维码获取更多精彩原创内容!
上一篇: Git内部原理剖析