Protobuf语法全解析

Protocol Buffers(protobuf)是一种语言无关,平台无关,可扩展的用于序列化结构化数据的方式——相似XML,但比XML更灵活,更高效。虽然日常工做中常常用到protobuf,但不少时候只是停留在基本语法的使用上,不少高级特性和语法还掌握不全,在阅读一些开源proto库的时候,总会看到一些日常没有使用过的语法,影响理解。git

本文基于Go语言,总结了全部的proto3经常使用和不经常使用的语法和示例,助你全面掌握protobuf语法,加深理解,扫清源码阅读障碍。github

Quick Start

使用protobuf语法编写xxx.proto文件,而后将其编译成可供特定语言识别和使用的代码文件,供程序调用,这是protobuf的基本工做原理。golang

以Go语言为例,使用官方提供的编译器会将xxx.proto文件编译成xxx.pb.go文件——一个普通的go代码文件。
要使用protobuf,首先咱们须要下载protobuf编译器——protoc,但Go语言并无被编译器直接支持,而是经过插件的方式被编译器引用,因此同时咱们还须要下载Go语言的编译插件:json

  1. 下载合适环境的编译器(protoc-$VERSION-$PLATFORM.zip):github.com/protocolbuf…
  2. 下载安装Go语言编译插件:go install google.golang.org/protobuf/cmd/protoc-gen-go
    安装完毕后,咱们准备以下文件$SRC_DIR/quick_start.proto:
syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
复制代码

执行编译器命令:protoc --go_out=$DST_DIR $SRC_DIR/quick_start.proto。 该命令将编译$SRC_DIR/quick_start.proto文件,而且将其基于Go语言的编译输出结果保存到文件$DST_DIR/quick_start.qb.go中:数组

....
type SearchRequest struct {
	Query                string   `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"`
	PageNumber           int32    `protobuf:"varint,2,opt,name=page_number,json=pageNumber,proto3" json:"page_number,omitempty"`
	ResultPerPage        int32    `protobuf:"varint,3,opt,name=result_per_page,json=resultPerPage,proto3" json:"result_per_page,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}
....
复制代码

在程序中引入生成文件quick_start.qb.go所在的包,就能够用protobuf的方式对结构体进行序列化和反序列化。
序列化:bash

req := &pb.SearchRequest{} //此处pb是 quick_start.qb.go 所在包的别名
// ...

// 序列化结构体,写入文件
out, err := proto.Marshal(req)
if err != nil {
        log.Fatalln("Failed to encode search request :", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
        log.Fatalln("Failed to write search request:", err)
}
复制代码

反序列化:ui

// 从文件读取消息,并将其反序列化成结构体
in, err := ioutil.ReadFile(fname)
if err != nil {
        log.Fatalln("Error reading file:", err)
}
book := &pb.SearchRequest{}
if err := proto.Unmarshal(in, book); err != nil {
        log.Fatalln("Failed to parse search request:", err)
}
复制代码

A Bit of Everything

quick start示例中展现的是最基础的用法,下面咱们经过一个包含全部proto3语法的示例,逐一讲解protobuf的各项语法和功能。
示例代码在这里能够找到:a_bit_of_everything.proto
在代码根目录下执行protoc --go_out=plugins=grpc:. a_bit_of_everything.proto生成xxx.pb.go文件。this

package

syntax = "proto3";
option go_package = "examplepb";  // 编译后的golang包名
package example.everything; // proto包名
...
复制代码

在示例文件的起始位置会看到go_packagepackage两个关于包的声明,但这两个package表达的意义并不相同,package example.everything;代表的是当前.proto文件所在的包名,跟Go语言相似,在相同的包名下,不能定义相同名称的messageenum或是serviceoption go_package = "examplepb" 则定义了一个文件级别的option,用于指定编译后的golang包名。google

import

...
import "google/protobuf/any.proto";
import "google/protobuf/descriptor.proto";
//import "other.proto";
...
复制代码

import用于引入其余的proto文件,当在当前文件中要使用其余proto文件的定义时,须要将其import进来,而后能够经过相似packageName.MessageName的方式来引用须要的内容,跟Go语言的import十分相似。执行编译protoc的时候,须要加上-I参数来指定import文件的路径,例如: protoc -I $GOPATH/src --go_out=. a_bit_of_everything.proto编码

示例中引入的any.proto和descriptor.proto已经内置到protoc中,故编译本示例不须要加-I参数

标量类型 (Scalar Value Types)

proto类型 Go类型 备注
double float64
float float
int32 int32 编码负数值相对低效
int64 int64 编码负数值相对低效
uint32 uint32
uint64 uint64
sint32 int32 当值为负数时候,编码比int32更高效
sint64 int64 当值为负数时候,编码比int64更高效
fixed32 uint32 当值老是大于2^28时,编码比uint32更高效
fixed64 uint64 当值老是大于2^56时,编码比uint32更高效
sfixed32 int32
sfixed64 int64
bool bool
string string 只能是utf-8编码或者7-bit ASCII文本,且长度不得大于2^32
bytes []byte 不大于2^32的任意长度字节序列

message消息

// 普通的message
message SearchRequest {
    string query = 1;
    int32 page_number = 2;
    int32 result_per_page = 3;
}
复制代码

message能够包含多个字段声明,每一个字段声明须要包含字段类型,字段名称和一个惟一序号。字段类型能够是标量,枚举或是其余message类型。惟一序号用于标识该字段在消息二进制编码中位置。

还能够用repeated来修饰字段类型,详见下文repeated说明。

枚举类型

...
// 枚举 enum
enum Status {
    STATUS_UNSPECIFIED = 0;
    STATUS_OK  = 1;
    STATUS_FAIL= 2;
    STATUS_UNKNOWN = -1; // 不推荐有负数
}
...
复制代码

经过enum关键字定义枚举类型,在protobuf中,枚举是一个int32类型。第一个枚举值必须从0开始,若是不但愿在代码中使用0值,能够将第一个值用XXX_UNSPECIFIED做为占位符。因为enum类型其实是用protobuf的int32类型的编码方式编码,故不推荐在枚举类型中使用负数。

XXX_UNSPECIFIED只是一种代码规范。并不影响代码行为。

保留字段 (Reserved Fields) & 保留枚举值(Reserved Values)

// 保留字段
message ReservedMessage {
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
    // string abc = 2; // 编译报错
    // string foo = 3; // 编译报错
}
// 保留枚举
enum ReservedEnum {
    reserved 2, 15, 9 to 11, 40 to max;
    reserved "FOO", "BAR";
    // FOO = 0; // 编译报错 
    F = 0;
}
复制代码

若是咱们将某message中的字段删除了,后面更新可能会从新使用这些字段。当新旧两种proto定义都在线上运行时,编解码可能会发生错误。例若有新旧两个版本的Foo:

// old version
message Foo {
    string a = 1;
}
复制代码
// new version
message Foo {
    int32 a = 1;
}
复制代码

若是使用新版本的proto来解析旧版本的消息,就会发生错误,由于新版本proto会尝试将a解析成int32,但实际上旧版本proto是按照string类型来对a进行编码的。protobuf经过提供reserved关键字来避免新旧版本冲突的问题:

// new version
message Foo {
    reserved 1; // 标记第一个字段是保留的
    int32 a = 2; // 序号从2开始,就不会与旧版本的string类型a冲突了
}
复制代码

嵌套

// nested 嵌套message
message SearchResponse {
    message Result {
        string url = 1 ;
        string title = 2;
    }
    enum Status {
        UNSPECIFIED = 0;
        OK  = 1;
        FAIL= 2;
    }
    Result results = 1;
    Status status = 2;
}
复制代码

message容许多层嵌套,messageenum均可以嵌套。被嵌套的messageenum不只能够在当前message中使用,也能够被其余message引用:

message OtherResponse {
    SearchResponse.Result result = 1;
    SearchResponse.Status status = 2;
}
复制代码

复合类型

除标量类型外,protobuf还提供了一些非标量类型,在本文中我把它们统称为复合类型。

复合类型并非官方划分的类别。是本文为了便于理解而概括总结的一个概念。

repeated

// repeated
message RepeatedMessage {
    repeated SearchRequest requests = 1;
    repeated Status status = 2;
    repeated int32 number = 3;
}
复制代码

repeated能够做用在message中的变量类型上。只有标量类型枚举类型message类型能够被repeated修饰。repeated表示当前修饰变量能够被重复任意次(包括0次),其实就是表示当前修饰类型的一个变长数组,也就是Go语言中的slice

// repeated
type RepeatedMessage struct {
	Requests             []*SearchRequest `protobuf:"bytes,1,rep,name=requests,proto3" json:"requests,omitempty"`
	Status               []Status         `protobuf:"varint,2,rep,packed,name=status,proto3,enum=example.everything.Status" json:"status,omitempty"`
	Number               []int32          `protobuf:"varint,3,rep,packed,name=number,proto3" json:"number,omitempty"`
	XXX_NoUnkeyedLiteral struct{}         `json:"-"`
	XXX_unrecognized     []byte           `json:"-"`
	XXX_sizecache        int32            `json:"-"`
}

复制代码

map

message MapMessage{
    map<string, string> message = 1;
    map<string, SearchRequest> request = 2;
}
复制代码

除了slice,固然还有map。其中key的类型能够是除去double,float,bytes之外的标量类型,value的类型能够是任意标量类型,枚举类型和message类型。protobuf的map编译成Go语言后也是用map来表示:

...
// map
type MapMessage struct {
	Message              map[string]string         `protobuf:"bytes,1,rep,name=message,proto3" json:"message,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
	Request              map[string]*SearchRequest `protobuf:"bytes,2,rep,name=request,proto3" json:"request,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
	XXX_NoUnkeyedLiteral struct{}                  `json:"-"`
	XXX_unrecognized     []byte                    `json:"-"`
	XXX_sizecache        int32                     `json:"-"`
}
...
复制代码

any

...
import "google/protobuf/any.proto";
...
message AnyMessage {
    string message = 1;
    google.protobuf.Any details = 2;
}
...
复制代码

any类型能够包含一个不须要指定类型的任意的序列化消息。要使用any类型,须要import google/protobuf/any.protoany类型字段的encode/decode交由各语言的运行时各自实现,例如在Go语言中能够这样读写any类型的字段:

...
import "github.com/golang/protobuf/ptypes"
...
func getSetAny() {
	fmt.Println("getSetAny")
	req := &examplepb.SearchRequest{
	    Query: "query",
	}
	// 将SearchRequest打包成Any类型
	a, err := ptypes.MarshalAny(req)
	if err != nil {
	    log.Println(err)
	    return
	}
	// 赋值
	anyMsg := &examplepb.AnyMessage{
	    Message: "any message",
	    Details: a,
	}
	
	req = &examplepb.SearchRequest{}
	// 从Any类型中还原proto消息
	err = ptypes.UnmarshalAny(anyMsg.Details, req)
	if err != nil {
	    log.Println(err)
	}
	fmt.Println(" any:", req)
}
复制代码

one of

// one of
message OneOfMessage {
    oneof test_oneof {
        string m1 = 1;
        int32 m2 =2;
    }
}
复制代码

若是某消息包含多个字段,但这些字段同一时间最多只容许一个被设置时,能够经过oneof来保证这样的行为。对oneof中任意一个字段设值,都会将其余字段清空。例如对上述的例子,test_oneof字段要么是string类型的m1,要么是int32类型的m2。在Go语言中读写oneof的示例以下:

func getSetOneof() {
	fmt.Println("getSetOneof")
	oneof := &examplepb.OneOfMessage{
		// 同一时间只能设值一个值
		TestOneof: &examplepb.OneOfMessage_M1{
			M1: "this is m1",
		},
	}
	fmt.Println(" m1:", oneof.GetM1())  // this is m1
	fmt.Println(" m2:", oneof.GetM2()) // 0
}
复制代码

options & extensions

相信大部的gopher在日常使用protobuf的过程当中都不多关注options,80%的开发工做也不须要直接用到options。但options是一个颇有用的功能,其大大提升了protobuf的扩展性,咱们有必要了解它。options实际上是protobuf内置的一些message类型,其分为如下几个级别:

  • 文件级别(file-level options)
  • 消息级别(message-level options)
  • 字段级别(field-level options)
  • service级别(service options)
  • method级别(method options)

protobuf提供一些内置的options可供选择,也提供了经过extend关键字来扩展这些options,达到增长自定义options的目的。

proto2语法中,extend能够做用于任何message,但在proto3语法中,extend仅能做用于这些定义optionmessage——仅用于自定义option

options不会改变声明的总体含义(例如声明的是int32就是int32,不会由于一个option改变了其声明类型),但可能会影响在特定状况下处理它的方式。例如咱们可使用内置的deprecated option将某字段标记为deprecated

message Msg {
    string foo = 1;
    string bar = 2 [deprecated = true]; //标记为deprecated。
}
复制代码

当咱们须要编写自定义protoc插件时,能够经过自定义options为编译插件提供额外信息。举个例子,假设我要开发一个proto的校验插件,其生成xxx.Validate()方法来校验消息的合法性,我能够经过自定义options来提供生成代码的必要信息:

message Msg {
    // required是自定义options,表示foo字段必须非空
    string foo = 1; [required = true]; 
}
复制代码

内置options的定义能够在github.com/protocolbuf…找到,每种级别的options都对应一个message,分别是:

  • FileOptions —— 文件级别
  • MessageOptions —— 消息级别
  • FieldOptions —— 字段级别
  • ServiceOptions —— service级别
  • MethodOptions —— method级别

如下将经过示例来逐一介绍这些级别的options,以及如何扩展这些options

文件级别

...
option go_package = "examplepb";  // 编译后的golang包名
...
message extObj {
    string foo_string= 1;
    int64 bar_int=2;
}
// file options
extend google.protobuf.FileOptions {
    string file_opt_string = 1001;
    extObj file_opt_obj = 1002;
}
option (example.everything.file_opt_string) = "file_options";
option (example.everything.file_opt_obj) = {
    foo_string: "foo"
    bar_int:1
};
复制代码

go_package 毫无疑问是protobuf内置提供的,用于指定编译后的golang包名。除了使用内置的外,能够经过extend字段来扩展内置的FileOptions,例如在上述例子中,咱们新增了两个新的option——string类型的file_opt_string和extObj类型的file_opt_obj。并经过option关键字设置了两个文件级别的options。在Go语言中,咱们能够这样读取这些options:

func getFileOptions() {
	fmt.Println("file options:")
	msg := &examplepb.MessageOption{}
	md, _ := descriptor.MessageDescriptorProto(msg)
	stringOpt, _ := proto.GetExtension(md.Options, examplepb.E_FileOptString)
	objOpt, _ := proto.GetExtension(md.Options, examplepb.E_FileOptObj)
	fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString)
	fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt)
	fmt.Println(" string:", *stringOpt.(*string))
}
复制代码

打印结果:

file options:
	obj.foo_string: foo
	obj.bar_int 1
	string: file_options
复制代码

消息级别

// message options
extend google.protobuf.MessageOptions {
    string msg_opt_string = 1001;
    extObj msg_opt_obj = 1002;
}
message MessageOption {
    option (example.everything.msg_opt_string) = "Hello world!";
    option (example.everything.msg_opt_obj) = {
        foo_string: "foo"
        bar_int:1
    };
    string foo = 1;
}
复制代码

与文件级别大同小异,再也不赘述。Go语言读取示例:

func getMessageOptions() {
	fmt.Println("message options:")
	msg := &examplepb.MessageOption{}
	_, md := descriptor.MessageDescriptorProto(msg)
	objOpt, _ := proto.GetExtension(md.Options, examplepb.E_MsgOptObj)
	stringOpt, _ := proto.GetExtension(md.Options, examplepb.E_MsgOptString)
	fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString)
	fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt)
	fmt.Println(" string:", *stringOpt.(*string))
}

复制代码

字段级别

// field options
extend google.protobuf.FieldOptions {
    string field_opt_string = 1001;
    extObj field_opt_obj = 1002;
}
message FieldOption {
    // 自定义的option
    string foo= 1 [(example.everything.field_opt_string) = "abc",(example.everything.field_opt_obj) = {
        foo_string: "foo"
        bar_int:1
    }];
    // protobuf内置的option
    string bar = 2 [deprecated = true];
}
复制代码

字段级别的option定义方式不使用option关键字,格式为:用[]包裹的用逗号分隔的k=v形式的数组。在Go语言中,咱们能够这样读取这些option:

func getFieldOptions() {
	fmt.Println("field options:")
	msg := &examplepb.FieldOption{}
	_, md := descriptor.MessageDescriptorProto(msg)
	stringOpt, _ := proto.GetExtension(md.Field[0].Options, examplepb.E_FieldOptString)
	objOpt, _ := proto.GetExtension(md.Field[0].Options, examplepb.E_FieldOptObj)
	fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString)
	fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt)
	fmt.Println(" string:", *stringOpt.(*string))
}
复制代码

应用项目参考:github.com/mwitkow/go-… go-proto-validators是一个用于生成能够校验proto消息合法性的proto编译插件,其使用字段级别的option来定义校验规则。

service和method级别

// service & method options
extend google.protobuf.ServiceOptions {
    string srv_opt_string = 1001;
    extObj srv_opt_obj = 1002;
}
extend google.protobuf.MethodOptions {
    string method_opt_string = 1001;
    extObj method_opt_obj = 1002;
}
service ServiceOption {
    option (example.everything.srv_opt_string) = "foo";
    rpc Search (SearchRequest) returns (SearchResponse) { option (example.everything.method_opt_string) = "foo";
        option (example.everything.method_opt_obj) = {
            foo_string: "foo"
            bar_int: 1
        };
    };
}
复制代码

service和method级别的option也是经过option关键字来定义,与文件级别和消息级别option相似,再也不赘述。Go语言读取示例:

func getServiceOptions() {
	fmt.Println("service options:")
	msg := &examplepb.MessageOption{}
	md, _ := descriptor.MessageDescriptorProto(msg)
	srv := md.Service[1] // ServiceOption
	stringOpt, _ := proto.GetExtension(srv.Options, examplepb.E_SrvOptString)
	fmt.Println(" string:", *stringOpt.(*string))
}
func getMethodOptions() {
	fmt.Println("method options:")
	msg := &examplepb.MessageOption{}
	md, _ := descriptor.MessageDescriptorProto(msg)
	srv := md.Service[1] // ServiceOption
	objOpt, _ := proto.GetExtension(srv.Method[0].Options, examplepb.E_MethodOptObj)
	stringOpt, _ := proto.GetExtension(srv.Method[0].Options, examplepb.E_MethodOptString)
	fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString)
	fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt)
	fmt.Println(" string:", *stringOpt.(*string))
}
复制代码

应用项目参考:github.com/grpc-ecosys…
grpc-gateway经过为rpc的method自定义option,来表达由grpc到http的转换关系,经过文件级别和service级别的option来控制生成swagger的行为。

参考

developers.google.cn/protocol-bu…
developers.google.cn/protocol-bu…
github.com/mwitkow/go-…
github.com/grpc-ecosys…

相关文章
相关标签/搜索