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…

相關文章
相關標籤/搜索