Protobuf即Protocol Buffers,是Google公司開發(fā)的一種跨語言和平臺的序列化數(shù)據(jù)結(jié)構(gòu)的方式,是一個靈活的、高效的用于序列化數(shù)據(jù)的協(xié)議。與XML和JSON格式相比,protobuf更小、更快、更便捷。而且Protobuf是跨語言的,并且自帶一個編譯器(protoc),只需要用protoc進行編譯,就可以編譯成Java、Python、C++、C#、Go等多種語言代碼,然后可以直接使用,不需要再寫其它代碼,自帶有解析的代碼。
一. Protocbuf編譯器的安裝
從https://github.com/protocolbuffers/protobuf/releases上下載最新的release包,并解壓到你的安裝目錄下,然后將其下面的bin目錄添加為系統(tǒng)環(huán)境變量,如mac操作系統(tǒng)下:
export PATH=$PATH:/usr/local/protobuf/bin
這樣就可以在任意地方的命令行窗口下使用protoc指令,如檢查protoc編譯器版本:
protoc --version
二. Protocbuf3語法
1. 版本聲明
syntax = "proto3";
.proto文件中非注釋非空的第一行必須使用Proto版本聲明,如果不使用proto3版本聲明,Protobuf編譯器默認使用proto2版本。
2. Package
package com.aervon.proto;
.proto文件中可以新增一個可選的package聲明符,用來防止不同的消息類型有命名沖突。包的聲明符會根據(jù)使用語言的不同影響生成的代碼:
A、對于C++語言,產(chǎn)生的類會被包裝在C++的命名空間中。
B、對于Java語言,包聲明符會變?yōu)閖ava的一個包,除非在.proto文件中提供了一個明確有java_package。
C、對于Go語言,包可以被用做Go包名稱,除非顯式的提供一個option go_package在.proto文件中。
3. Import
import "google/protobuf/timestamp.proto";
通過import聲明符可以引用其他.proto里的結(jié)構(gòu)數(shù)據(jù)體,如以上聲明后就可以使用google.protobuf.Timestamp了。
4. 消息定義
message Person {
string name = 1;
int32 id = 2 [default = 0];
string email = 3;
}
Protobuf中,消息即結(jié)構(gòu)化數(shù)據(jù)。其中變量的聲明結(jié)構(gòu)為:
字段規(guī)則 + 字段類型 + 字段名稱 + [=] + 標識符 + [默認值]
字段規(guī)則有:
required: 結(jié)構(gòu)體必須包含該字段一次
optional: 結(jié)構(gòu)體可以包含該字段零次或一次(不超過一次)
repeated: 該字段可以在格式良好的消息中重復(fù)任意多次(包括0),其中重復(fù)值的順序會被保留,相當于數(shù)組
PS: 在 proto3 中已經(jīng)為兼容性徹底拋棄 required
字段類型可以具有以下幾種類型,在編譯器作用下會自動生成類中的相應(yīng)類型:
| .proto Type | Notes | C++ Type | Java Type | Python Type | Go Type |
|---|---|---|---|---|---|
| double | double | double | float | *float64 | |
| float | float | float | float | *float32 | |
| int32 | 使用可變長度編碼。編碼負數(shù)的效率低 - 如果你的字段可能有負值,請改用 sint32 | int32 | int | int | *int32 |
| int64 | 使用可變長度編碼。編碼負數(shù)的效率低 - 如果你的字段可能有負值,請改用 sint64 | int64 | long | int/long | *int64 |
| uint32 | 使用可變長度編碼 | uint32 | int | int/long | *uint32 |
| uint64 | 使用可變長度編碼 | uint64 | long | int/long | *uint64 |
| sint32 | 使用可變長度編碼。有符號的 int 值。這些比常規(guī) int32 對負數(shù)能更有效地編碼 | int32 | int | int | *int32 |
| sint64 | 使用可變長度編碼。有符號的 int 值。這些比常規(guī) int64 對負數(shù)能更有效地編碼 | int64 | long | int/long | *int64 |
| fixed32 | 總是四個字節(jié)。如果值通常大于 228,則比 uint32 更有效。 | uint32 | int | int/long | *uint32 |
| fixed64 | 總是八個字節(jié)。如果值通常大于 256,則比 uint64 更有效。 | uint64 | long | int/long | *uint64 |
| sfixed32 | 總是四個字節(jié) | int32 | int | int | *int32 |
| sfixed64 | 總是八個字節(jié) | int64 | long | int/long | *int64 |
| bool | bool | boolean | bool | *bool | |
| string | 字符串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本 | string | String | str/unicode | *string |
| bytes | 可以包含任意字節(jié)序列 | string | ByteString | str | []byte |
標識符:
在消息定義中,每個字段都有唯一的一個數(shù)字標識符。標識符用來在消息的二進制格式中識別各個字段,一旦使用就不能夠再改變。最小的標識符可以從1開始,最大到2^29 - 1(536,870,911),不可以使用其中[19000-19999]( Protobuf協(xié)議實現(xiàn)中進行了預(yù)留,從FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的標識號。如果非要在.proto文件中使用預(yù)留標識符,編譯時就會報警。
PS: [1,15]內(nèi)的標識號在編碼的時候會占用一個字節(jié)。[16,2047]之內(nèi)的標識號則占用2個字節(jié)。所以應(yīng)該為頻繁出現(xiàn)的消息元素保留[1,15]內(nèi)的標識號。
默認值:
當一個消息被解析的時候,如果編碼消息里不包含一個特定的singular元素,被解析的對象所對應(yīng)的字段被設(shè)置為一個默認值,不同類型默認值如下:
| 變量類型 | 默認值 |
|---|---|
| string | 空string |
| bytes | 空bytes |
| bool | false |
| 數(shù)值類型 | 0 |
| 枚舉 | 第一個定義的枚舉值 |
| 消息類型(message) | 字段沒有被設(shè)置,確切的消息是根據(jù)語言確定的,通常情況下是對應(yīng)語言中空列表 |
| 標量消息字段 | 一旦消息被解析,就無法判斷字段是被設(shè)置為默認值還是根本沒有被設(shè)置,應(yīng)該在定義消息類型時注意 |
5. 添加注釋
為你的 .proto 文件添加注釋,可以使用 C/C++ 語法風(fēng)格的注釋 // 和 /* ... */ 。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
required string query = 1;
optional int32 page_number = 2; // Which page number do we want?
optional int32 result_per_page = 3; // Number of results to return per page.
}
6. Reserved 保留字段
如果你通過完全刪除字段或?qū)⑵渥⑨尩魜砀?message 類型,則未來一些用戶在做他們的修改或更新時就可能會再次使用這些字段編號。如果以后加載相同 .proto 的舊版本,這可能會導(dǎo)致一些嚴重問題,包括數(shù)據(jù)損壞,隱私錯誤等。確保不會發(fā)生這種情況的一種方法是指定已刪除字段的字段編號(有時也需要指定名稱為保留狀態(tài),英文名稱可能會導(dǎo)致 JSON 序列化問題)為 “保留” 狀態(tài)。如果將來的任何用戶嘗試使用這些字段標識符,protocol buffer 編譯器將會抱怨。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
PS: 你不能在同一 "reserved" 語句中將字段名稱和字段編號混合在一起指定。
7. 枚舉 Enumerations
在下面的例子中,我們添加了一個名為 Corpus 的枚舉,其中包含所有可能的值,之后定義了一個類型為 Corpus 枚舉的字段:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
你可以通過為不同的枚舉常量指定相同的值來定義別名。為此,你需要將 allow_alias 選項設(shè)置為true,否則 protocol 編譯器將在找到別名時生成錯誤消息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // 取消此行注釋將導(dǎo)致 Google 內(nèi)部的編譯錯誤和外部的警告消息
}
枚舉器常量必須在 32 位整數(shù)范圍內(nèi)。由于 enum 值在線上使用 varint encoding ,負值效率低,因此不推薦使用。你可以在 message 中定義 enums,如上例所示的那樣?;蛘邔⑵涠x在 message 外部 - 這樣這些 enum 就可以在 .proto 文件中的任何 message 定義中重用。你還可以使用一個 message 中聲明的 enum 類型作為不同 message 中字段的類型,使用語法 MessageType.EnumType 來實現(xiàn)。
當你在使用 enum 的 .proto 上運行 protocol buffer 編譯器時,生成的代碼將具有相應(yīng)的用于 Java 或 C++ 的 enum,或者用于創(chuàng)建集合的 Python 的特殊 EnumDescriptor 類。運行時生成的類中具有整數(shù)值的符號常量。
8. 擴展 Extensions
通過擴展,你可以聲明 message 中的一系列字段編號用于第三方擴展。擴展名是那些未由原始 .proto 文件定義的字段的占位符。這允許通過使用這些字段編號來定義部分或全部字段從而將其它 .proto 文件定義的字段添加到當前 message 定義中。我們來看一個例子:
message Foo {
// ...
extensions 100 to 199;
}
這表示 Foo 中的字段數(shù) [100,199] 的范圍是為擴展保留的。其他用戶現(xiàn)在可以使用指定范圍內(nèi)的字段編號在他們自己的 .proto 文件中為 Foo 添加新字段,例如:
extend Foo {
optional int32 bar = 126;
}
這會將名為 bar 且編號為 126 的字段添加到 Foo 的原始定義中。
當用戶的 Foo 消息被編碼時,其格式與用戶在 Foo 中常規(guī)定義新字段的格式完全相同。但是,在應(yīng)用程序代碼中訪問擴展字段的方式與訪問常規(guī)字段略有不同 - 生成的數(shù)據(jù)訪問代碼具有用于處理擴展的特殊訪問器。那么,舉個例子,下面就是如何在 C++ 中設(shè)置 bar 的值:
Foo foo;
foo.SetExtension(bar, 15);
類似地,F(xiàn)oo 類定義模板化訪問器 HasExtension(),ClearExtension(),GetExtension(),MutableExtension() 和 AddExtension()。它們都具有與正常字段生成的訪問器相匹配的語義。有關(guān)使用擴展的更多信息,請參閱所選語言的代碼生成參考。
PS:擴展可以是任何字段類型,包括 message 類型,但不能是 oneofs 或 maps。
另外,你也可以在另一種 message 類型內(nèi)部聲明擴展:
message Baz {
extend Foo {
optional int32 bar = 126;
}
}
在這種情況下,訪問此擴展的 C++ 代碼為:
Foo foo;
foo.SetExtension(Baz::bar, 15);
換句話說,唯一的影響是 bar 是在 Baz 的范圍內(nèi)定義。
如果你的編號約定可能涉及那些具有非常大字段編號的擴展,則可以使用 max 關(guān)鍵字指定擴展范圍至編號最大值:
message Foo {
extensions 1000 to max;
}
最大值為 229 - 1,或者 536,870,911。與一般選擇字段編號時一樣,你的編號約定還需要避免 19000 到 19999 的字段編號(FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber),因為它們是為 Protocol Buffers 實現(xiàn)保留的。你可以定義包含此范圍的擴展名范圍,但 protocol 編譯器不允許你使用這些編號定義實際擴展名。
9. Any類型
Any類型消息允許在沒有指定.proto定義的情況下使用消息作為一個嵌套類型。一個Any類型包括一個可以被序列化bytes類型的任意消息以及一個URL作為一個全局標識符和解析消息類型。
為了使用Any類型,需要導(dǎo)入import google/protobuf/any.proto。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
對于給定的消息類型的默認類型URL是type.googleapis.com/packagename.messagename。
不同語言的實現(xiàn)會支持動態(tài)庫以線程安全的方式去幫助封裝或者解封裝Any值。例如在java中,Any類型會有特殊的pack()和unpack()訪問器,在C++中會有PackFrom()和UnpackTo()方法。
10. Oneof
Oneof定義用來代表在實現(xiàn)的時候,該組屬性中有且只能有一個被定義,不能出現(xiàn)多個。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
上述定義中只能出現(xiàn)name或者sub_message的出現(xiàn),不能同時出現(xiàn),同時Oneof不能出現(xiàn)repeated域。重復(fù)傳遞值到Oneof多個域僅僅最后的會生效,其它的將被忽略掉。
11. Map
如果要創(chuàng)建一個關(guān)聯(lián)映射,Protobuf提供了一種快捷的語法:
map<key_type, value_type> map_field = N;
其中key_type可以是任意Integer或者string類型(除了floating和bytes的任意標量類型都可以),value_type可以是任意類型,但不能是map類型。例如,創(chuàng)建一個Project的映射,每個Projecct使用一個string作為key:
map<string, Project> projects = 3;
Map的字段可以是repeated。序列化后的順序和map迭代器的順序是不確定的,所以不要期望以固定順序處理Map。當為.proto文件產(chǎn)生生成文本格式的時候,map會按照key 的順序排序,數(shù)值化的key會按照數(shù)值排序。
從序列化中解析或者融合時,如果有重復(fù)的key則后一個key不會被使用。
PS: map語法序列化后等同于如下內(nèi)容,因此即使是不支持map語法的Protobuf實現(xiàn)也可以處理數(shù)據(jù):
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
12. 定義服務(wù)
如果想要將消息類型用在RPC(遠程方法調(diào)用)系統(tǒng)中,可以在.proto文件中定義一個RPC服務(wù)接口,Protobuf編譯器將會根據(jù)所選擇的不同語言生成服務(wù)接口代碼及stub。如要定義一個RPC服務(wù)并具有一個方法Search,Search方法能夠接收SearchRequest并返回一個SearchResponse,可以在.proto文件中進行如下定義:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
最直觀的使用Protobuf的RPC系統(tǒng)是gRPC,由谷歌開發(fā)的語言和平臺中的開源的PRC系統(tǒng),gRPC在使用Protobuf時非常有效,如果使用特殊的Protobuf插件可以直接從.proto文件中產(chǎn)生相關(guān)的RPC代碼。
如果不想使用gRPC,可以使用Protobuf用于自己的RPC實現(xiàn)。
三. 編譯Protoc文件
Protobuf提供了protoc編譯器,用于通過定義好的.proto文件來生成Java,Python,C++,Ruby,Objective-C,C#,Go等語言代碼。
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
1. 導(dǎo)入目錄設(shè)置
IMPORT_PATH聲明了一個.proto文件所在的解析import具體目錄。如果忽略該值,則使用當前目錄。如果有多個目錄則可以多次調(diào)用--proto_path,會順序的被訪問并執(zhí)行導(dǎo)入。-I=IMPORT_PATH是--proto_path的簡化形式。
2. 生成代碼指定
--cpp_out :在目標目錄DST_DIR中產(chǎn)生C++代碼
--java_out :在目標目錄DST_DIR中產(chǎn)生Java代碼
--python_out :在目標目錄 DST_DIR 中產(chǎn)生Python代碼
--go_out :在目標目錄 DST_DIR 中產(chǎn)生Go代碼
--ruby_out:在目標目錄 DST_DIR 中產(chǎn)生Ruby代碼
--javanano_out:在目標目錄DST_DIR中生成JavaNano
--objc_out:在目標目錄DST_DIR中產(chǎn)生Object代碼
--csharp_out:在目標目錄DST_DIR中產(chǎn)生Object代碼
--php_out:在目標目錄DST_DIR中產(chǎn)生Object代碼
3. 導(dǎo)入proto消息文件指定
必須指定一個或多個.proto文件作為輸入,多個.proto文件可以只指定一次。雖然文件路徑是相對于當前目錄的,每個文件必須位于其IMPORT_PATH下,以便每個文件可以確定其規(guī)范的名稱。
4. 生成編程語言相關(guān)代碼
當用Protobuf編譯器來運行.proto文件時,編譯器將生成所選擇語言的代碼,相應(yīng)語言的代碼可以操作在.proto文件中定義的消息類型,包括獲取、設(shè)置字段值,將消息序列化到一個輸出流中以及從一個輸入流中解析消息。
對C++語言,編譯器會為每個.proto文件生成一個.h文件和一個.cc文件,.proto文件中的每一個消息有一個對應(yīng)的類。
對Java語言,編譯器為每一個消息類型生成了一個.java文件以及一個特殊的Builder類(用來創(chuàng)建消息類接口的)。
對Go語言,編譯器會為每個消息類型生成了一個.pb.go文件。
對Ruby語言,編譯器會為每個消息類型生成了一個.rb文件。
【參考文章】