FlatBuffer 是一个二进制 buffer,它使用 offset 组织嵌套对象(struct,table,vectors,等),可以使数据像任何基于指针的数据结构一样,就地访问数据。
然而 FlatBuffer 与大多数内存中的数据结构不同,它使用严格的对齐规则和字节顺序来确保 buffer 是跨平台的。
FlatBuffers 的主要目标是避免反序列化。这是通过定义二进制数据协议来实现的,该协议指定了将定义好的将数据转换为二进制数据的方法。由该协议创建的二进制结构可以 wire 发送,并且无需进一步处理即可读取。
基础使用方法请参考 使用FlatBuffers序列化数据 。
FlatBuffers 是一个高效的数据格式,但要实现效率,您需要一个高效的 schema。
使用FlatBuffers的第一步就是编写Schema文件,它支持多种数据类型。字段可以有标量类型(所有大小的整数/浮点数),也可以是字符串,任何类型的数组,引用另一个对象,或者一组可能的对象(Union)。
Table 中每个字段都是可选 optional 的,并会设置默认值为0/null
Table 允许新添加字段,但只能在定义的末尾添加,具有后向兼容性
Table 不允许删除不再使用的字段,但可以通过把它标记为 deprecated,从而停止将它们写入数据,这与删除的效果相同
Table 允许更改字段,但只有在类型改变是相同大小的情况下,是可行的
structs 没有任何字段是可选的(所以也没有默认值)
structs 字段可能不会被添加或被弃用
structs 可能只包含标量或其他结构
structs 不提供前向/后向兼容性,但占用内存更小。对于不太可能改变的非常小的对象(例如坐标对或RGBA颜色)存成 struct 是非常有用的
table 的内存开销很小(因为 vtables 很小并且共享)访问成本也很小(间接访问),但是提供了很大的灵活性。table 甚至可能比等价的 struct 花费更少的内存,因为字段在等于默认值时不需要存储在 buffer 中。
如果确定以后不会进行任何更改,structs 使用的内存少于 table,并且访问速度更快(它们总是以串联方式存储在其父对象中,并且不使用虚拟表)。
序列化的一般步骤为:
flatbuffers::FlatBufferBuilder builder;
序列化包含在 Data 中的所有对象,使用深度优先,先根遍历序列化数据树DataBuilder data_builder(builder);
创建Table节点,其中 Data
为Table名称。当有多个Table时,也是一样的用法。const uint8_t *buf = builder.GetBufferPointer();
获取序列化后的数据索引,以二进制方式保存或者发送到网络。反序列化的一般步骤为:
GetData(bufp)
获取根节点的数据索引,之后可以直接读取数据。其中 Data
为IDL中定义的root_type指定的Tableflatbuffers::GetRoot<DataTemp>(bufp)
获取非root_type指定的Table索引当需要通信的数据结构有多个时,就需要有多个Table,因为一个Schema只有一个Root Table,可以每个Table写一个Schema文件,但当数据结构较多时,会生成较多的头文件,使用不便。
可以在一个Schema文件中写多个Table,然后用一个union联合体把它们全部包含进来,再定义一个包含该union的Table,并设该Table为Root Table,后续操作都使用该Root Table即可。
如,有三种数据类型需要处理,分别为A, B, C。定义的Schema大概如下:
table A {
price:double;
name:string;
}
table B {
name:string;
age:int;
}
table C {
name:string;
weight:int;
}
union DataType {
A,
B,
C
}
table DataTypeEntry {
entry:DataType;
}
root_type DataTypeEntry;
序列化:
//A
flatbuffers::FlatBufferBuilder fbb;
auto name = fbb.CreateString("a");
ABuilder ab(fbb);
ab.add_price(2.30);
ab.add_name(name);
auto data = ab.Finish();
auto dataEntry = CreateDataTypeEntry(fbb, DataType_A, data.Union());
fbb.Finish(dataEntry);
uint8_t *buf = fbb.GetBufferPointer();
// 以二进制发送或者保存
// 其他类似
反序列化:
// buf为存储FlatBuffers内容的内存区域,size为其大小
// 验证数据
flatbuffers::Verifier verifier((const uint8_t *)buf, size);
if (!VerifyDataTypeEntryBuffer(verifier))
{
std::cerr << "error data" << endl;
return;
}
// 解析
auto data = GetDataTypeEntry(buf);
switch (data->entry_type()) // union自带type
{
case DataType_A:
{
auto quote = reinterpret_cast<const A *>(data->entry());
cout << "name: " << quote->name()->c_str() << ", price: " << quote->price() << endl;
break;
}
case // 其他类似
}
在网络编程中,通信双方使用约定协议才能解析数据。基于TCP的字节流传输也需要一定的方式才能接收到完整的数据对象。
在发送端,可以使用send发送一定长度的二进制数据,但是接收端并不知道需要接收多长才能停止,所以需要告诉接收端数据长度。
FlatBuffers提供了增加字节前缀进行数据构造的方法,在 FlatBufferBuilder
完成构建时,使用 FinishSizePrefixed
代替Finish
即可。
这样就在数据对象的前4个字节存储了真实数据的长度。
非常简单,在发送端只需要改动这一行代码即可。
接收端在接收时,需要先接收前4个字节,得到真实数据长度,然后再次把数据接收完整。
使用boost asio 进行异步接收的代码片断如下:
// 代码片断
// 读取数据头
void do_read_header()
{
// msg_header_len = 4
boost::asio::async_read(m_sock, boost::asio::buffer(m_readBuf, msg_header_len), boost::bind(on_read_header, shared_from_this(), _1, _2));
}
// 解析头,并开始读取body
void on_read_header(const boost::system::error_code &err, size_t bytes)
{
if (!err)
{
assert(bytes == msg_header_len);
std::string msg(m_readBuf, bytes);
size_t prefixed_size = flatbuffers::ReadScalar<flatbuffers::uoffset_t>(msg.c_str()); //得到真实数据长度
std::cout << __LINE__ << ": msg body len = " << prefixed_size << std::endl;
// 后续数据继续保存
boost::asio::async_read(m_sock, boost::asio::buffer(m_readBuf + msg_header_len, prefixed_size), boost::bind(on_read_body, shared_from_this(), _1, _2));
}
else
{
stop();
}
}
// 反序列化数据
void on_read_body(const boost::system::error_code &err, size_t bytes)
{
if (!err)
{
std::string msg(m_readBuf, bytes + msg_header_len); // 使用FlatBuffers构建的全部数据
try
{
// verify
flatbuffers::Verifier verifer((const uint8_t *)msg.c_str(), msg.length());
if (!VerifySizePrefixedxxx(verifer))
{
cout << "invalid flatbuffers data" << endl;
return;
}
// 得到数据
auto data = GetSizePrefixedxxx(msg.c_str());
cout << data->usr()->c_str() << endl;
}
catch (const std::exception &e)
{
std::cout << __LINE__ << ": Error occured! Message: " << e.what();
}
}
else
{
stop();
}
}
FinishSizePrefixed
代替Finish
flatbuffers::ReadScalar<flatbuffers::uoffset_t>
得到真实数据长度VerifySizePrefixedxxx
验证数据GetSizePrefixedxxx
得到数据,完成深入浅出 FlatBuffers 之 Schema
Is the size prefix for each buffer or entire program?
Cannot find message size when reading a stream with prefixed buffer size messages.
flatbuffers::FlatBufferBuilder Class Reference