google Protobuf
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,并于2008年对外开源。 Protobuf性能和效率大幅度优于JSON、XML等其他的结构化数据格式,是以二进制方式存储的,占用空间小,但也带来了可读性差的缺点。 我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具。
protobuf入门
protobuf安装
官方下载链接:https://github.com/protocolbuffers/protobuf/releases 如果是linux,可以直接wget
# 下载安装包
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.2/protoc-3.11.2-linux-x86_64.zip
# 解压到 /usr/local 目录下
$ sudo 7z x protoc-3.11.2-linux-x86_64.zip -o/usr/local
如果能正常显示版本,则表示安装成功。
$ protoc --version
libprotoc 3.11.2
###protoc-gen-go Go 中使用protobuf还需要安装 protoc-gen-go,这个工具用来将 .proto 文件转换为Go代码。
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
编写message
Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。 接下来,我们创建一个简单的示例 student.proto
syntax = "proto3";
package main;
// student.proto
message Student {
string name = 1;
bool male = 2;
repeated int32 scores = 3;
}
// 输出到当前目录
option go_package=".";
对上述代码的解析:
1.开头的syntax语句表示采用proto3的语法,这里是以proto3格式定义。你还可以指定为proto2。 如果没有指定,默认以proto2格式定义。
2.package,即包名声明符是可选的,用来防止不同的消息类型有命名冲突
3.消息类型 使用 message 关键字定义。name, male, scores 是该类型的 3 个字段,类型分别为 string, bool 和 []int32。repeated 表示字段可重复,即用来表示 Go 语言中的数组类型。
4.每个 = 号后面的数字为标识符,用来在消息体中识别各个字段。
###proto转化为go 通过以下命令生成相应的Go代码
$ protoc --go_out=. *.proto
$ ls
student.pb.go student.proto
其中go_out参数告知protoc编译器去加载对应的protoc-gen-go工具,然后通过该工具生成代码,生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
我们来看一下生成的go文件
// student.proto
type Student struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Male bool `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
Scores []int32 `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
}
func (x *Student) Reset() {
*x = Student{}
if protoimpl.UnsafeEnabled {
mi := &file_student_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Student) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Student) ProtoMessage() {}
func (x *Student) ProtoReflect() protoreflect.Message {
mi := &file_student_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Student.ProtoReflect.Descriptor instead.
func (*Student) Descriptor() ([]byte, []int) {
return file_student_proto_rawDescGZIP(), []int{0}
}
func (x *Student) GetName() string {
if x != nil {
return x.Name
}
return ""
}
其中ProtoMessage方法表示这是一个实现了proto.Message接口的方法。 此外Protobuf还为每个成员生成了一个Get方法,Get方法不仅可以处理空指针类型,而且可以和Protobuf第二版的方法保持一致(第二版的自定义默认值特性依赖这类方法)。
Protobuf 序列化实现详解
本小段将带大家详细了解protobuf的基本序列化实现机制。以下内容以protobufV3版本为例。
序列化实现
protobuf序列化的大致流程如下:
- 遍历结构体字段,读取字段的标签信息 如:
protobuf:"bytes,1,opt,name=name,proto3"
字段类型为 bytes 与 tag order 为 1. 这两个值会影响序列化的内容生成 - 根据字段类型,判断处理的方式。 一共定义6类. 以下是Go语言中定义的方式。可以分成明确长度类型与可变长度类型
VarintType Type = 0 // pb类型: bool enum, int32, sint32, uint32, int64, sint64, sint64, uint64 Fixed32Type Type = 5 // pb类型: sfix32 fix32 float Fixed64Type Type = 1 // pb类型: sfix64 sfix64 double BytesType Type = 2 // pb类型: byte数组,string, message结构体, repeat 类型 StartGroupType Type = 3 // 较早期能力,使用较少 ,本文不再介绍 EndGroupType Type = 4 // 较早期能力,使用较少 ,本文不再介绍
明确长度类型
VarintType, Fixed32Type, Fixed64Type 为明确长度类型, pb写入内容格式如下:
tag(8字节) | 值(长度由Go语言类型确定, 后面附有对照表格) |
---|---|
uint64 | value |
例如:以下字段定义
age int32 `protobuf:"int32,1,opt,name=age,proto3" `
pb序列化后,是一个12字节长度的byte数组
变长类型
BytesType 为变长类型, b写入内容格式如下:
tag(8字节) | 长度(8字节) | 值(长度由前面的字段确定) |
---|---|---|
uint64 | uint64 | value |
例如:以下字段定义 写入pb后,注是一个12字节(Tag 8字节+内容4字节)长度的byte数组
name string `protobuf:"bytes,1,opt,name=name,proto3" `
假如 name=”abc” pb序列化后,是一个19字节(Tag 8字节+长度值 8字节+内容3字节)长度的byte数组
- 生成tag值计算过程
protobuf为了节省空间,想把 tag order与 字段类型保存到一个值内, 思路是 把uint64类型的, 最未3位用于存储类型,其它61 bit用于存储tag order 值。
以下计算Tag的代码实现:
// EncodeTag encodes the field Number and wire Type into its unified form.
func EncodeTag(num Number, typ Type) uint64 {
return uint64(num)<<3 | uint64(typ&7)
}
- 如果是结构体嵌套,则重复执行上面的过程即可。
反序列化实现
了解上面序列化过程后,理解反序列化是非常简单了,大致流程如下:
- 读取第一个8字节内容(Tag),如果为0,则表示结束
-
反解Tag, 获得tag order与类型,
反解Tag代码如下:
// EncodeTag encodes the field Number and wire Type into its unified form.
func DecodeTag(x uint64) (Number, Type) {
// NOTE: MessageSet allows for larger field numbers than normal.
if x>>3 > uint64(math.MaxInt32) {
return -1, 0
}
return Number(x >> 3), Type(x & 7)
}
- 根据tag order, 类型读取值的内容
- 如果是变长类型,则再读8个字节,读取长度值,根据长度取[]byte值
- 如果是明确长度类型,则需要根据order 读取 生成pb 结构体字段对应order的Go语言字段类型, 根据字段类型,计算读取的长度, 根据长度取[]byte值
- 通过反射对[]byte值写入到指定字段中。如果是结构体,则重复前二步操作即可。
小结
- protobuf 之所有空间小且快因为只保存了tag(tag order+类型)与值的内容。解析上不需要任何语法辅助。(直接字段段拼接,不用任何分隔符)
- protobuf的可读性不好,因为没有tag与具体的转换类型,是无法解析里面的内容
map类型序列化
protobuf V3 版本开始支持map类型,但他的定义比较特别的,所以单独讲一下它的思路.
map生成的字段如下例所示:
type DataMessage struct {
mymap map[string]int32 `protobuf:"bytes,2,rep,name=mymap,proto3" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}
map 属于 BytesType 类型。
- 如果map的key与value 内容是 定长类型,序列化描述如下
tag(8字节) | 某个key value空间长度(8字节) | key tag | key value | value tag | value |
---|---|---|---|---|---|
uint64 | uint64 | uint64 | key value | uint64 | value |
备注: 如果map中有多个key,则需要重复上面的内容组合。
- 如果map的key与value可内容是变长类型,序列化描述如下
tag(8字节) | 某个key value空间长度(8字节) | key tag | key值长度 | key value | value tag | value 值长度 | value |
---|---|---|---|---|---|---|---|
uint64 | uint64 | uint64 | uint64 | key value | uint64 | uint64 | value |
备注: 如果map中有多个key,则需要重复上面的内容组合。
Go语言的这一块代码实现在 codec_map.go文件中
func appendMap(b []byte, mapv reflect.Value, mapi *mapInfo, f *coderFieldInfo, opts marshalOptions) ([]byte, error) {
if mapv.Len() == 0 {
return b, nil
}
if opts.Deterministic() {
return appendMapDeterministic(b, mapv, mapi, f, opts)
}
iter := mapRange(mapv) // 遍历map
for iter.Next() {
var err error
b = protowire.AppendVarint(b, f.wiretag) // 计算map字段的tag, 写入数组
b, err = appendMapItem(b, iter.Key(), iter.Value(), mapi, f, opts) // 写入当前key,value的长度,以及单Key的tag, 值 ,以及value的tag 与值
if err != nil {
return b, err
}
}
return b, nil
}
补充材料
pb类型与Go语言类型的对照关系:
pb类型 | 说明 | Go类型 | 长度 |
---|---|---|---|
double | float64 | 8 | |
float | float32 | 4 | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int32 | 4 |
uint32 | 使用变长编码 | uint32 | 4 |
uint64 | 使用变长编码 | uint64 | 8 |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | 4 |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int64 | 8 |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | uint32 | 4 |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | uint64 | 4 |
sfixed32 | 总是4个字节 | int32 | 4 |
sfixed64 | 总是8个字节 | int64 | 8 |
bool | bool | 1 | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | string | 变长 |
bytes | 可能包含任意顺序的字节数据。 | []byte | 变长 |