这个小工具可以分析指定代码中的Go结构体,并生成对应二进制序列化和反序列化代码,它可以生成的Go代码符合encoding.BinaryMarshaler
和encoding.BinaryUnmarshaler
接口要求。
另外支持更高效的序列化和反序列化方式,可配合github.com/funny/link
内置的分包协议使用。
并可以加入其它编程语言代码生成的支持,可用于游戏项目的服务端和客户端通讯协议解析代码生成。
更多介绍:http://zhuanlan.zhihu.com/idada/20410055
这个工具将为指定代码中的每个结构体生成以下方法:
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
。
基本格式:
- 按字段顺序执行序列化和反序列化
- 所有的多字节数值都以小端格式编码。
支持以下基本类型:
类型 | 字节数 |
---|---|
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里的,所以两者执行时间不具有可比性。
fastbin因为是给游戏项目用,所以设计时候就考虑了要支持多种语言的代码生成,结构上是比较简单容易扩展的。
添加其它语言的代码生成可以参考golang_gen.go
和golang_tpl.go
实现,欢迎大家提交扩展。
计划下个版本加入生成协议描述文档的模板,生成一份HTML文档出来,在浏览器上直接阅读。
用起来可能类似于godoc
命令:fastbin -S :8080
关于Protobuf的optional设置,可以用指针类型部分模拟,但并不完全。
fastbin的协议结构是严格的并且不向下兼容,因为我们目前项目中客户端都有热更新技术,所以这方面需求不强烈。
如果对fastbin用的二进制格式不满意,也可以替换成自己喜欢的格式,改模板就可以。
欢迎提交Issue和PR,欢迎加群讨论:188680931。