Protobuf是google开发的一种跨语言和平台的序列化数据结构的方式,类似于XML但是更小更快而且更简单,只需要定义一次结构体,通过生成的源代码可以在不同的数据流和不同的语言平台上去读写数据结构。
最新的protobuf3支持更多的语言使用,比如go 、 object-c等等。另外proto2与proto3并非完全兼容,官方仍旧提供proto2的支持。Google内部有超过40000多个数据结构是通过protobuf来进行的定义,这些数据结构的序列化操作不仅仅用在RPC接口,同时也用于持久化存储数据。
protobuf3语法定义
Protobuf的定义存放在.proto文件中,记录数据结构包含的字段和属性,如下面所示定义了一个Person结构体。如果不包含首行版本声明的话,编译器将认为是proto2版本
syntax = "proto3";
/* Person用于定义联系人相关信息
* 包含人员的基本信息和联系信息 */
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
数字标签
每一个字段都有一个对应的数字标签,用于在消息的二进制格式中识别每一个属性。
该标签是在此定义中独一无二的,范围为1-536,870,911,另外19000-19999为protobuf协议实施需要的,不要占用。1-15占用小于等于1个字节的存储,因此为优化使用,我们可以把经常使用或者重复型元素设置在该范围。
每个字段都有缺省值,数值类型的为0,字符串类型的为空字符串,布尔类型为false,枚举类型为第一个枚举值,bytes类型为空bytes。
重复域可以包含0到多个重复内容,可以看成为动态数组。
保留域
如果删除了某一个字段,protobuf允许重新使用该数值作为新的属性的标签,但是为了保证向后兼容,读取旧的数据的时候不会出现问题,一般使用reserved来声明该数值为保留,不能被使用。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
客户端生成文件
在不同的编程语言环境下,我们使用客户端编译器去编译我们的protobuf文件到指定的编程语言对象,生成的是不同的文件。
比如C++对于每一个.proto文件生成的是一个.h文件和一个.cc文件,为每一个消息类型生成一个类。对于go语言则生成的是.pb.go文件,里面包含有消息的定义结构体以及常用的函数。python的话,编译器为每一个消息类型生成一个具有静态描述符的模块。使用metaclass去创建必要的python运行时数据访问类。
枚举类型
上面的代码中包含了一个嵌入式的枚举类型:
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
枚举类型中第一个值必须是0值,因为必须存在一个0值作为枚举类型的缺省值,并且这样也兼容proto2版本。同时可以声明allow_alias来设置具有相同值的枚举属性。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
同时枚举类型的删除也需要加入到reserved保留字段中,就像上面定义的foo,bar一样。
使用其他消息类型
对于相同文件中的属性定义,我们可以直接使用,比如SearchResponse中包含了多个Result结构。
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
对于跨文件的定义,我们可以直接使用import的方式来使用
import public "new.proto";
import "other.proto";
嵌套定义
上面的例子也可以通过嵌套定义的方式实现,这里直接将Result定义为SearchResponse的内部的消息结构。
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
但是Result定义并非私有的,其他的结构定义的时候也可以使用。只是需要包含父级别的名称,如下所示:
message OtherMessage {
SearchResponse.Result result = 1
}
Any类型
Any允许未定义具体类型的属性声明,如下为一个简单的使用实例。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
不同语言客户端在使用的时候具体的实现不同,且接口一般提供pack或者unpack()实现数据的写入和写出。
oneof定义
Oneof定义用来代表在实现的时候,该组属性中有且只能有一个被定义,不能出现多个。比如下面的定义中:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
上述定义中只能出现name或者sub_message的出现,不能同时出现,同时Oneof不能出现repeated域。重复传递值到Oneof多个域仅仅最后的会生效,其他的将被忽略掉。
Maps类型
Map也可以在protobuf中直接使用,只是这里面的key类型必须是整形或者string类型。value类型不能是其他的map类型。map类型不能是repeated类型。
map<string, Project> projects = 3;
服务定义
如果想要使用消息类型作为一个RPC系统,可以定义一个RPC服务接口,编译器将自动生成服务代码依赖于选择的语言。比如下面的定义了一个RPC服务接口用来传递搜索结果和返回搜索响应内容
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
最简单直接的使用RPC系统是gRPC, 该系统也是跨平台的开源RPC接口系统,可以直接生成相关的RPC代码(使用编译插件)
对比XML
相对于XML语言的序列化数据,Protobuf提供更简单的定义方式,序列速度是XML的20-100倍,所占的数据大小减少3-10倍,而且更容易集成到编程语言中使用。
安装编译器
前往官方github地址下载编译器解压缩后执行下面的命令即可
./configure
make && make install
执行下面的命令安装go proto 插件
go get -u github.com/golang/protobuf/protoc-gen-go
使用go读写protobuf实例
这里我们可以查看官网的protobuf-example,里面包含了使用不同的语言去读写protobuf序列化数据的一些示例。
首先定义基本的数据结构文件address.proto,其中我们使用package main
仅仅作为go语言中包的定义。Person定义人员信息,AddressBook定义人员联系方式集合,包含任意多的人员联系方式。
syntax = "proto3";
package main;
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
编写完成后就可以直接使用protoc命令来编译.proto文件
protoc -I=. --go_out=. addressbook.proto
该操作将会在当前目录生成一个addressbook.pb.go文件,如果打开该go文件可以看到,程序自动生成了预先定义的数据的struct结构体,该结构体支持一些基本的操作,比如获取每个字段的GetName(),GetEmail等,以及用于重置清空数据的Reset函数,Person转换后的结构体如下:
type Person struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Id int32 `protobuf:"varint,2,opt,name=id" json:"id,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email" json:"email,omitempty"`
Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones" json:"phones,omitempty"`
}
其中不仅仅支持protobuf的序列化,同时支持json数据序列化操作。
我们编写一个函数用于存储序列化的数据到本地文件,并直接读取文件获取文件中的内容,以确保数据的确保存成功了。下面是针对该包操作的主函数内容。
package main
import (
"io/ioutil"
"log"
proto "github.com/golang/protobuf/proto"
)
func main() {
fname := "address.out"
p := Person{
Id: 1234,
Name: "Mike",
Email: "mike@example.com",
Phones: []*Person_PhoneNumber{
{Number: "1234-2332", Type: Person_HOME},
},
}
book := &AddressBook{}
book.People = append(book.People, &p)
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book")
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Faile to write to file")
}
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file")
}
readBook := &AddressBook{}
if err = proto.Unmarshal(in, readBook); err != nil {
log.Fatalln("Fail to parse file")
} else {
if len(readBook.People) > 0 {
log.Printf("First Person's Name in address is %s", readBook.People[0].GetName())
}
}
}
go build 生成二进制文件后,直接执行后就可以看到当前目录上生成了一个address.out的存储文件,存储了之前定义的用户联系信息。
输出如下:
2018/04/21 18:41:03 First Person's Name in address is Mike
如果未来需要扩展或者修改字段定义,为了保证向后兼容性,需要确保满足以下的规则:
1. 不要改变已经存在的任何字段的tag数字编号
2. 可以删除字段
3. 增加新的字段需要使用未被展示用的tag编号(这个编号甚至不能是之前用过且被删除后的)
旧的数据中不包含的字段,在读取时候,按照缺省值进行读取操作。protobuf除了高效的序列化操作外,自动生成不同语言的代码的确为写程序提供了非常方便且高效的开发体验。