【原创】flatBuffers基本用法详解(Go语言)

golang下使用flatbuffers

作者: koangel 发表于: 2018-01-02 19:02:01

flatBuffers基本用法

这是一篇描述flatbuffers在golang下的基本用法,但是Demo目前只提供golang版本,其他的细节可以参考具体的英文文档。

官方网站:flatBuffers

0x1 flatBuffers是个啥?

简单来说是一个跨平台的高性能序列化库,可用于C++, C#, C, Go, Java, JavaScript, PHP, 以及Python。那么有人肯定问了 有了protocol Buffer为啥还要flatBuffer?

首先这两个库都是google出品,那么品质毋庸置疑,问题在于flatbuffers拥有较好的上下协议兼容性以及超过PB的性能以及更小的协议,基本就是为游戏和移动终端量身定做的,下图为序列化性能对比:

C++版本性能对比

上图性能对比采用的是C++版本库

那么大体我们已经了解到基本上为什么要选fb了,实际上详细的内容请参考官方文档中对于Overview的章节。

flatBuffers平台以及支持列表:Platform & Language

0x2 编写Schema

和PB一样,FB需要我们编写固定的Schema再通过flatc这个程序编译后形成目标代码,那么我先介绍一下基本流程

  • 编写自己的Schema文件
  • 通过flatc编译为目标语言协议源码
  • 将源码丢入项目

是不是特别简单以及特别熟悉?嗯,用过PB的发现基本都是这个流程,那么唯一区别的是Schema的语法问题。

顺道一提,VSCode可以安装flatBuffers的插件,然后我们就能愉快的语法高亮了。

Schema基本语法

先看一个官方例子:

// 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 = 100;
  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;

是不是特别像pb,但是又有很多不同之处?

来我们一步一步理解一下Schema的用法,首先从编译开始,我们新建一个文件monster.fbs,新建到哪里都可以。

然后输入以上官方内容。

由于大部分使用PB或FB的用户都是跨语言的,那么我建议在生成源码时将源码输出到指定语言的目录,例如go、cpp、csharp

golang下编译

flatc -o .\go --go monster.fbs

C++下编译

flatc -o .\cpp --cpp monster.fbs

C# 下编译

flatc -o .\csharp --csharp monster.fbs

好了,我们完成了第一步编译,那么我们来根据这个文件分析语法。

命名空间(namespace)

首先从第一行开始 namespace,一般写C#和CPP的同学都知道,类似PB的package,这里是测试程序,那么自然写的是namespace MyGame.Sample;,结尾注意分号。

注意:建议编写Schema的时候有良好的namespace的命名规范,包括项目、协议类型等,这样更容易区分协议所属,方便定位。

枚举(enum)

首先枚举在golang中是没有的,至少没有这个关键字的,需要const来定义,所以golang的生成代码是const,这里没问题所以不要有疑问,因为枚举本身就是序列型的常量,接下来简单来说,枚举都知道整形,序列常量,从0开始,这个我就不废话了,只说一点FB官方确定,枚举关键字是不会弃用的,所以请安心使用。

基本枚举的例子:

enum Color:byte { Red = 0, Green, Blue = 2 }

枚举在FB中是可以指定类型的,这里是byte类型。

联合(union)

union这个东西和Cpp里面的不一样,Cpp的union是公用地址,而这里的联合可以理解为需要将多个数据合并发送时而采用的一种办法,而非是我们所理解的Cpp中的联合。

同时联合本身不能作为FB的Root Type使用,因为他本身是一个枚举表,公用属性的数据类型。

一个联合的例子:

union Equipment { Weapon } // Optionally add more tables.

其他类型联合的例子:

union Equip { Wear, Weapon, Pickup }

结构(struct)

一个小型的且类似于Table的类型,非主导类型,什么意思呢?就是你需要组合一个非常简单且不需要默认数值的类型时可以采用sturctsturct自身是可以作为Root Type或Table的一个变量存在的。

所以struct自身占用内存非常低且效率极高,因为没有大量的虚函数以及默认初始化的行为,自身非常适合简易而耗时的类型。

sturct自身仅仅可以包含基本类型和sturct不可以包含其他类型。

注意:struct本身是简易类型,不简易作为主要数据类型使用,结合Table最好。

向量例子(向量通常用于传递游戏内的坐标、Object位置以及Mesh寻路运算结果等):

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

角色基本属性列子(我们有hp,mp,sp类型为int):

struct ObjectBase {
  hp:int
  mp:int
  sp:int
}

消息主体(Table)

Table是主消息结构,也就是PB中的Message,所以我们在未来使用中会大量用到Table

Table本身支持多种类型、默认数值设定以及前后的协议兼容性。

Table是线性类型的,所以这里和PB是有区别的,因为不需要在每一行结尾指定其顺序,这个是Table自身向下兼容协议的根本依托。

Table如果某个类型不需要使用,那么在结尾使用(deprecated)将其标记为弃用,那么在生成代码时,会将该字段自动弃用且不在写入数据,但是特别注意,很容易破坏代码结构和协议结构,所以弃用时请谨慎,当然FB自身包含兼容协议,可以良好的处理该问题。

特别注意:向下兼容机制要求只能向结尾处添加新字段和新的数据类型,千万不要在中间或其他位置插入数据,否则会出现无法兼容甚至协议异常的情况。

我们来一个实际的Table类型:

table Monster {
  pos:Vec3; // 此处是一个结构体.
  mana:short = 150; // 标记基本的short类型,默认数值是150
  hp:short = 100;
  name:string; // 标记基本的string类型
  friendly:bool = false (deprecated); // 一个指定的弃用类型,默认数值为false
  inventory:[ubyte];  // 一个ubyte的vector(理解为array也可以)
  color:Color = Blue; // 标记为枚举类型,枚举为Blue.
  weapons:[Weapon];   // 一个table类型的vector
  equipped:Equipment; // 一个联合类型.
  path:[Vec3];        // 一个sturct类型的vector
}

好了我们分析了基本的Table类型,也有了一个大概的类型的理解,那么接下来我们详细说一说基础类型。

内建类型(Types)

内建数据类型为:

  • 8 bit: byte (int8), ubyte (uint8), bool
  • 16 bit: short (int16), ushort (uint16)
  • 32 bit: int (int32), uint (uint32), float (float32)
  • 64 bit: long (int64), ulong (uint64), double (float64)

其中括号内的是别名,比如使用int8来代替byte,但是我建议还是使用详细位长去写比较好,不过看个人项目需求。

内建其他类型:

  • vector - 任何类型使用关键字[Type],即可标记为vector,但是不支持嵌套(需要嵌套请把他包含到一个table中即可)。
  • string - 保存数据仅支持utf8或7位AscII码,如果类型特殊且需要保存二进制数据,建议使用[ubyte][byte]
  • 枚举和联合关键字,这个上文已经介绍了。

使用类型后会根据其类型长度分配相应内存(好吧是一句废话)。

特别注意,一旦使用了该类型,请不要轻易修改任何类型的,否则可能带来严重的协议兼容问题甚至破坏代码结构本身。

关键字

一般用于加在table的字段结尾处,例如:

table abc { id:int = 1 (required); }

那么我们来看看有哪些我们日常使用的关键字

  • required - 必要字段,该标记表明字段是必须填写的,否则会出现解析错误。
  • deprecated - 弃用字段,标记后该字段将被弃用。
  • id - 强制标记字段顺序,例如 uid:int (id:1)
  • force_align: size - 用于sturct,主要是标记其对齐方式,所以cpp的同学们可以使用,golang以及c#的同学自动无视即可。
  • bit_flags - 用于枚举,指定其字节(1<N),如果标记后不指定,你会得到1,2,4,8
  • nested_flatbuffer: “table_name” - 用于某个嵌套数据,给指定的[ubyte]生成快速访问器。
  • flexbuffer - 同上,区别是这个直接标记,上面关键字用于指定,基本用不上。
  • key - 排序所在类型的关键字,可用于快速的二进制搜索,基本用不上。
  • hash - 表明其为一个无符号或有符号的32或64位整形,散列HASH算法,用于JSON解析时其数值可以存在一个STRING但保存后为HASH的数值,一般用自己的算法,所以这个也基本用不到。
  • original_orde - 用于Table,一般Table会优化存放顺序,而这个关键字直接禁止这个行为,官方直接的说法是,没事别用他,属于基本用不上的类型。

Include

用于包含其他的schema文件。

include "mydefinitions.fbs";

此处和Cpp基本一致和PB也一样,就是包含那个schema文件,用于防止多处定义或需要引用的。

以上是目前使用频率最多的Schema的基本语法说明,更加详细的说明,可以戳这里看英文版

0x3 在代码中使用协议

在golang中使用之前生成的代码:

首先安装Golang的库:

go get -u github.com/google/flatbuffers/go

新建项目,复制schema/go下的文件到proto目录下,然后新建一个main.go

package main

import (
    ms "fbsample/proto/MyGame/Sample"
    "fmt"

    fb "github.com/google/flatbuffers/go"
)

func main() {
    // 添加一个monster
    builder := fb.NewBuilder(0) // 初始化size为0的对象

    // 写入背包数据
    ms.MonsterStartInventoryVector(builder, 5)
    for i := 4; i >= 0; i-- {
        builder.PrependByte(byte(i))
    }
    inv := builder.EndVector(5)

    // 写入路径数据
    ms.MonsterStartPathVector(builder, 3)
    for i := 3; i >= 0; i-- {
        builder.PrependUOffsetT(ms.CreateVec3(builder, 333, 444, 555))
    }
    path := builder.EndVector(3)

    // 写入怪物属性
    strName := builder.CreateString("勇者的名字")
    ms.MonsterStart(builder) // 开始写入数据
    ms.MonsterAddPos(builder, ms.CreateVec3(builder, 101, 102, 103))
    ms.MonsterAddHp(builder, 120) // 写入HP
    ms.MonsterAddName(builder, strName)
    ms.MonsterAddInventory(builder, inv)
    ms.MonsterAddPath(builder, path)
    b := ms.MonsterEnd(builder)

    builder.Finish(b) // 这里完成最终数据的写入

    // 读入数据
    newMs := ms.GetRootAsMonster(builder.Bytes, builder.Head())

    // 将数据读出来
    pos := newMs.Pos(nil)
    fmt.Println(newMs.Hp(), string(newMs.Name()), pos)
}

运行项目

go run main.go

C#教程(英文): 戳我

C++教程(英文): 戳我

0x4 结束语

代码可以看出来FlatBuffers是以牺牲反射的序列化与反序列化来加快打包效率和其数据包大小的,所以这个玩意本身属于仁者见仁的情况。

对于需要协议兼容性,有良好封装设计理念的人,对于此类代码应该相对容易的将其封装成为打包协议,所以对于移动端产品、游戏类产品,的确可以考虑一下使用flatbuffers