Protocol Buffers(protobuf)是一種語言無關,平臺無關,可擴展的用於序列化結構化數據的方式——相似XML,但比XML更靈活,更高效。雖然日常工做中常常用到protobuf,但不少時候只是停留在基本語法的使用上,不少高級特性和語法還掌握不全,在閱讀一些開源proto庫的時候,總會看到一些日常沒有使用過的語法,影響理解。git
本文基於Go語言,總結了全部的proto3
經常使用和不經常使用的語法和示例,助你全面掌握protobuf語法,加深理解,掃清源碼閱讀障礙。github
使用protobuf語法編寫xxx.proto
文件,而後將其編譯成可供特定語言識別和使用的代碼文件,供程序調用,這是protobuf的基本工做原理。golang
以Go語言爲例,使用官方提供的編譯器會將xxx.proto
文件編譯成xxx.pb.go
文件——一個普通的go代碼文件。
要使用protobuf,首先咱們須要下載protobuf編譯器——protoc,但Go語言並無被編譯器直接支持,而是經過插件的方式被編譯器引用,因此同時咱們還須要下載Go語言的編譯插件:json
protoc-$VERSION-$PLATFORM.zip
):github.com/protocolbuf…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)
}
複製代碼
quick start示例中展現的是最基礎的用法,下面咱們經過一個包含全部proto3
語法的示例,逐一講解protobuf的各項語法和功能。
示例代碼在這裏能夠找到:a_bit_of_everything.proto
在代碼根目錄下執行protoc --go_out=plugins=grpc:. a_bit_of_everything.proto
生成xxx.pb.go
文件。this
syntax = "proto3";
option go_package = "examplepb"; // 編譯後的golang包名
package example.everything; // proto包名
...
複製代碼
在示例文件的起始位置會看到go_package
和package
兩個關於包的聲明,但這兩個package
表達的意義並不相同,package example.everything;
代表的是當前.proto
文件所在的包名,跟Go語言相似,在相同的包名下,不能定義相同名稱的message
,enum
或是service
。 option go_package = "examplepb"
則定義了一個文件級別的option
,用於指定編譯後的golang包名。google
...
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參數
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 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
只是一種代碼規範。並不影響代碼行爲。
// 保留字段
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
容許多層嵌套,message
和enum
均可以嵌套。被嵌套的message
和enum
不只能夠在當前message
中使用,也能夠被其餘message
引用:
message OtherResponse {
SearchResponse.Result result = 1;
SearchResponse.Status status = 2;
}
複製代碼
除標量類型外,protobuf還提供了一些非標量類型,在本文中我把它們統稱爲複合類型。
複合類型並非官方劃分的類別。是本文爲了便於理解而概括總結的一個概念。
// 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:"-"`
}
複製代碼
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:"-"`
}
...
複製代碼
...
import "google/protobuf/any.proto";
...
message AnyMessage {
string message = 1;
google.protobuf.Any details = 2;
}
...
複製代碼
any
類型能夠包含一個不須要指定類型的任意的序列化消息。要使用any
類型,須要import google/protobuf/any.proto
。any
類型字段的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
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
}
複製代碼
相信大部的gopher在日常使用protobuf的過程當中都不多關注options
,80%的開發工做也不須要直接用到options
。但options是一個頗有用的功能,其大大提升了protobuf的擴展性,咱們有必要了解它。options
實際上是protobuf內置的一些message
類型,其分爲如下幾個級別:
protobuf提供一些內置的options
可供選擇,也提供了經過extend
關鍵字來擴展這些options
,達到增長自定義options
的目的。
在
proto2
語法中,extend
能夠做用於任何message
,但在proto3
語法中,extend
僅能做用於這些定義option
的message
——僅用於自定義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
,分別是:
如下將經過示例來逐一介紹這些級別的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 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…