FlatBuffers使用详解

简介


FlatBuffer 是一个二进制 buffer,它使用 offset 组织嵌套对象(struct,table,vectors,等),可以使数据像任何基于指针的数据结构一样,就地访问数据。

然而 FlatBuffer 与大多数内存中的数据结构不同,它使用严格的对齐规则和字节顺序来确保 buffer 是跨平台的。

FlatBuffers 的主要目标是避免反序列化。这是通过定义二进制数据协议来实现的,该协议指定了将定义好的将数据转换为二进制数据的方法。由该协议创建的二进制结构可以 wire 发送,并且无需进一步处理即可读取。

基础使用方法请参考 使用FlatBuffers序列化数据 。

Schema文件编写


FlatBuffers 是一个高效的数据格式,但要实现效率,您需要一个高效的 schema。

使用FlatBuffers的第一步就是编写Schema文件,它支持多种数据类型。字段可以有标量类型(所有大小的整数/浮点数),也可以是字符串,任何类型的数组,引用另一个对象,或者一组可能的对象(Union)。

Table vs Struct
  • Table 中每个字段都是可选 optional 的,并会设置默认值为0/null

  • Table 允许新添加字段,但只能在定义的末尾添加,具有后向兼容性

  • Table 不允许删除不再使用的字段,但可以通过把它标记为 deprecated,从而停止将它们写入数据,这与删除的效果相同

  • Table 允许更改字段,但只有在类型改变是相同大小的情况下,是可行的

  • structs 没有任何字段是可选的(所以也没有默认值)

  • structs 字段可能不会被添加或被弃用

  • structs 可能只包含标量或其他结构

  • structs 不提供前向/后向兼容性,但占用内存更小。对于不太可能改变的非常小的对象(例如坐标对或RGBA颜色)存成 struct 是非常有用的

table 的内存开销很小(因为 vtables 很小并且共享)访问成本也很小(间接访问),但是提供了很大的灵活性。table 甚至可能比等价的 struct 花费更少的内存,因为字段在等于默认值时不需要存储在 buffer 中。

如果确定以后不会进行任何更改,structs 使用的内存少于 table,并且访问速度更快(它们总是以串联方式存储在其父对象中,并且不使用虚拟表)。

关于类型的说明
  • 一旦一个类型声明了,尽量不要改变它的类型,一旦改变了,很可能就会出现错误。例如如果把 int 改成 uint,数据如果有负数,那么就会出错。
  • 标量类型的字段有默认值,非标量的字段(string/vector/table)如果没有值的话,默认值为 NULL。
  • 只应添加枚举值,不要去删除枚举值(对枚举不存在弃用一说)。
  • Unions 字段,该字段可以包含对这些类型中的任何一个的引用,即这块内存区域只能由其中一种类型使用。union 跟 enum 比较类似,但是 union 包含的是 table,enum 包含的是 scalar或者 struct。
  • Root type 声明了序列化数据的根表(或结构)。这对于解析不包含对象类型信息的 JSON 数据尤为重要。可以在一个Schema文件中定义多个Table,而只定义一个root_type,使用 flatbuffers::GetRoot(bufp); 访问非root_type指定的Table。
  • table 在处理 field 数量非常多,但是实际使用只有其中少数几个 field 这种情况,效率依旧非常高。因此,组织数据应该尽可能的组织成 table 的形式。
  • 如果可能的话,尽量使用枚举的形式代替字符串。
  • FlatBuffers 默认可以支持存放的下所有整数,因此尽量选择所需的最小大小,而不是默认为 int/long。
关于命名
  • Table, struct, enum and rpc names (types) 采用大写驼峰命名法。
  • Table and struct field names 采用下划线命名法。这样做方法自动生成小写驼峰命名的代码。
  • Enum values 采用大写驼峰命名法。
  • namespaces 采用大写驼峰命名法。
  • 大括号:与声明的开头位于同一行。
  • 间距:缩进2个空格。:两边没有空格,=两边各一个空格。

序列化


序列化的一般步骤为:

  • 使用 flatbuffers::FlatBufferBuilder builder;序列化包含在 Data 中的所有对象,使用深度优先,先根遍历序列化数据树
  • 使用 DataBuilder data_builder(builder);创建Table节点,其中 Data为Table名称。当有多个Table时,也是一样的用法。
  • 使用 const uint8_t *buf = builder.GetBufferPointer();获取序列化后的数据索引,以二进制方式保存或者发送到网络。

反序列化


反序列化的一般步骤为:

  • 从文件或者网络获取FlatBuffers序列化后的数据索引
  • 使用 GetData(bufp)获取根节点的数据索引,之后可以直接读取数据。其中 Data为IDL中定义的root_type指定的Table
  • 若要获取非Root类型的Table时,如DataTemp,使用 flatbuffers::GetRoot(bufp) 获取非root_type指定的Table索引

关于一个Schema文件多个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
  • 接收端先接收数据头,4个字节,使用 flatbuffers::ReadScalar得到真实数据长度
  • 接收端继续接收真实数据,接收完后使用 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

你可能感兴趣的:(工具使用)