/fastbin

用来生成Go结构体二进制序列化和反序列化代码的小工具

Primary LanguageGo

NOTE:此工具还在持续开发中,可能会有较大改动。 介绍

这个小工具可以分析指定代码中的Go结构体,并生成对应二进制序列化和反序列化代码,它可以生成的Go代码符合encoding.BinaryMarshalerencoding.BinaryUnmarshaler接口要求。

另外支持更高效的序列化和反序列化方式,可配合github.com/funny/link内置的分包协议使用。

并可以加入其它编程语言代码生成的支持,可用于游戏项目的服务端和客户端通讯协议解析代码生成。

更多介绍:http://zhuanlan.zhihu.com/idada/20410055

title

Go代码生成

这个工具将为指定代码中的每个结构体生成以下方法:

import "github.com/funny/binary"

type FastBin interface {
	// 这个方法用于测量序列化后的数据长度
	// 用于在反序列化前一次性准备好足够大的内存空间
	// 请参考 github.com/funny/link 文档中分包协议效率的优化提示
	BinarySize() (n int)

	// 这个方法实现了 encoding.BinaryMarshaler 接口
	// 由于接口的要求是由内部返回[]byte,所以无法优化[]byte的重用
	// 建议在实际项目中避免使用
	MarshalBinary() (data []byte, err error)

	// 这个方法实现了 encoding.BinaryUnmarshaler 接口
	UnmarshalBinary(data []byte) error

	// 将结构体的内容序列化到BinaryWriter中
	MarshalWriter(buf binary.BinaryWriter)

	// 从BinaryReader中反序列化出结构体数据
	UnmarshalReader(r binary.BinaryReader)
}

由于MarshalBinary方法要求无参数,所以没有什么机会可以重用[]byte,非要做当然也可以,但是代码和效率都不会太好。

所以fastbin另外生成了MarshalWriter方法,可以从外部传入预备好的*binary.Buffer,这个Buffer必须空间够大,通常是先通过BinarySize()度量好消息长度后预备好Buffer,再传入MarshalWriter中。

实际项目中建议能重用Buffer的时候就尽量重用,可以减少不必要对象创建和内存申请。

Go项目中可以结合go generate命令使用,在需要生成代码的文件开头加上go generate指令:

//go:generate $GOPATH/bin/fastbin
package demo

type Test struct {
	Field1 int
	Field2 string
}

如果你的$GOPATH/bin$PATH环境变量里,可以用更简单的指令://go:generate fastbin

在需要生成代码的包的根目录或者项目根目录执行go generate ./...即可执行当前目录以及子目录下所有加了//go:generate标记的命令。

也可以在命令行单独指定需要生成的文件,例如:go generate demo.go

格式

基本格式:

  1. 按字段顺序执行序列化和反序列化
  2. 所有的多字节数值都以小端格式编码。

支持以下基本类型:

类型 字节数
int8, uint8, byte, bool 1
int16, uint16 2
int32, uint32, float32 4
int, uint, int64, uint64, float64 8
string, []byte 2 + N

支持指针,指针类型比普通类型额外多一个字节区分空指针,指针值为0时表示空指针,空指针的后续内容长度为0:

类型 字节数
*int8, *uint8, *byte, *bool 1 or 1 + 1
*int16, *uint16 1 or 1 + 2
*int32, *uint32, *float32 1 or 1 + 4
*int, *uint, *int64, *uint64, *float64 1 or 1 + 8

支持变长数组,变长数组采用2个字节存储数组元素个数:

类型 字节数
[]int8, []uint8, []byte, []bool, string 2 + N
[]int16, []uint16 2 + N * 2
[]int32, []uint32, []float32 2 + N * 4
[]int64, []uint64, []float64 2 + N * 8

支持定长数组,定长数组顺序循环序列化,不需要额外长度信息:

类型 字节数
[N]int8, [N]uint8, [N]byte, [N]bool N
[N]int16, [N]uint16 N * 2
[N]int32, [N]uint32, [N]float32 N * 4
[N]int64, [N]uint64, [N]float64 N * 8

支持结构体嵌套和自定义类型,基本类型以为的所有其它类型都通过MarshalBuffer``和UnmarshalBuffer`进行序列化和反序列化:

类型 字节数
MyType MyType.BinarySize()
*MyType 1 or 1 + MyType.BinarySize()
[]MyType 2 + sum(MyType.BinarySize())
[N]MyType sum(MyType.BinarySize())

支持多维数组等复杂数据结构:

类型 说明
[][]int 二维数组
[10][]*int 第一唯定长的二维数组
**int 指向指针的指针
*[][]int 指向二维数组的指针
*[10]*[]**int 指向定长的指针的指针的数组的指针的数组的指针

更详细的内容请参考生成后的代码:

关于体积和效率我按云风给sproto做的测试里的数据结构和数据做了测试。

结构如下:

type AddressBook struct {
	Person []Person
}

type Person struct {
	Name  string
	Id    int32
	Email string
	Phone []PhoneNum
}

type PhoneNum struct {
	Number string
	Type   int32
}

测试数据如下:

ab := AddressBook{[]Person{
	{"Alice", 10000, "", []PhoneNum{
		{"123456789", 1},
		{"87654321", 2},
	}},
	{"Bob", 20000, "", []PhoneNum{
		{"01234567890", 3},
	}},
}}

序列化后数据体积为76字节,执行1M次编码和1M次解码所需时间为:

Size: 76
Marshal 1M times: 125.32859ms
Unmarshal 1M times: 638.01296ms

反序列化过程因为有对象创建,所以开销较大,以后可以考虑加入对象池进行优化。

注:云风给sproto的测试是在lua里的,所以两者执行时间不具有可比性。

FAQ

客户端代码怎么办?

fastbin因为是给游戏项目用,所以设计时候就考虑了要支持多种语言的代码生成,结构上是比较简单容易扩展的。

添加其它语言的代码生成可以参考golang_gen.gogolang_tpl.go实现,欢迎大家提交扩展。

协议文档怎么办?

计划下个版本加入生成协议描述文档的模板,生成一份HTML文档出来,在浏览器上直接阅读。

用起来可能类似于godoc命令:fastbin -S :8080

更多的协议特性?

关于Protobuf的optional设置,可以用指针类型部分模拟,但并不完全。

fastbin的协议结构是严格的并且不向下兼容,因为我们目前项目中客户端都有热更新技术,所以这方面需求不强烈。

如果对fastbin用的二进制格式不满意,也可以替换成自己喜欢的格式,改模板就可以。

END

欢迎提交Issue和PR,欢迎加群讨论:188680931。