這個指南描述瞭如何使用Protocol buffer 語言去描述你的protocol buffer 數據, 包括 .proto文件符號和如何從.proto文件生成類。包含了proto2版本的protocol buffer語言:對於老版本的proto3 符號,請見Proto2 Language Guidephp
1.定義一個消息類型
先來看一個很是簡單的例子。假設你想定義一個「搜索請求」的消息格式,每個請求含有一個查詢字符串、你感興趣的查詢結果所在的頁數,以及每一頁多少條查詢結果。能夠採用以下的方式來定義消息類型的.proto文件了:java
syntax = "proto3"; message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; }
2.指定字段類型
在上面的例子中,全部字段都是標量類型:兩個整型(page_number和result_per_page),一個string類型(query)。固然,你也能夠爲字段指定其餘的合成類型,包括枚舉(enumerations)或其餘消息類型。python
3.分配標識號
在消息定義中,每一個字段都有惟一的一個數字標識符。這些標識符是用來在消息的二進制格式中識別各個字段的,一旦開始使用就不可以再改變。api
最小的標識號能夠從1開始,最大到2^29 - 1, or 536,870,911。不可使用其中的[19000-19999](從FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的標識號, Protobuf協議實現中對這些進行了預留。若是非要在.proto文件中使用這些預留標識號,編譯時就會報警。一樣你也不能使用早期保留的標識號。安全
注:[1,15]以內的標識號在編碼的時候會佔用一個字節。[16,2047]以內的標識號則佔用2個字節。因此應該爲那些頻繁出現的消息元素保留 [1,15]以內的標識號。ruby
4.指定字段規則
所指定的消息字段修飾符必須是以下之一:app
在proto3中,repeated的標量字段默認狀況下使用packed。ide
5.添加更多消息類型
在一個.proto文件中能夠定義多個消息類型,在定義多個相關的消息的時候,這一點特別有用——例如,若是想定義與SearchResponse消息類型對應的回覆消息格式的話,你能夠將它添加到相同的.proto文件中,如:ui
message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; } message SearchResponse { ... }
6.保留標識符(Reserved)
若是你經過刪除或者註釋全部字段,之後的用戶在更新這個類型的時候可能重用這些標識號。若是你使用舊版本代碼加載相同的.proto文件會致使嚴重的問題,包括數據損壞、隱私錯誤等等。如今有一種確保不會發生這種狀況的方法就是爲字段tag(reserved name可能會JSON序列化的問題)指定reserved標識符,protocol buffer的編譯器會警告將來嘗試使用這些字段標識符的用戶。this
message Foo { reserved 2, 15, 9 to 11; reserved "foo", "bar"; }
注:不要在同一行reserved聲明中同時聲明字段名字和tag number。
7.從.proto文件生成了什麼?
當用protocol buffer編譯器來運行.proto文件時,編譯器將生成所選擇語言的代碼,這些代碼能夠操做在.proto文件中定義的消息類型,包括獲取、設置字段值,將消息序列化到一個輸出流中,以及從一個輸入流中解析消息。
8.標量數值類型
一個標量消息字段能夠含有一個以下的類型——該表格展現了定義於.proto文件中的類型,以及與之對應的、在自動生成的訪問類中定義的類型:
.proto Type | Notes | C++ | Java | Python | Go | Ruby | C# | PHP |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | |
float | float | float | float | float32 | Float | float | float | |
int32 | 使用變長編碼,對於負值的效率很低,若是你的字段有可能有負值, 請使用sint64替代 |
int32 | int | int | int32 | Fixnum 或者 Bignum(根據須要) | int | integer |
uint32 | 使用變長編碼 | uint32 | int | int/long | uint32 | Fixnum 或者 Bignum(根據須要) | uint | integer |
uint64 | 使用變長編碼 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sint32 | 使用變長編碼,這些編碼在負值時比int32高效的多 | int32 | int | int | int32 | Fixnum 或者 Bignum(根據須要) | int | integer |
sint64 | 使用變長編碼,有符號的整型值。編碼時比一般的int64高效。 | int64 | long | int/long | int64 | Bignum | long | integer/string |
fixed32 | 老是4個字節,若是數值老是比老是比228大的話,這個類型會比uint32高效。 | uint32 | int | int | uint32 | Fixnum 或者 Bignum(根據須要) | uint | integer |
fixed64 | 老是8個字節,若是數值老是比老是比256大的話,這個類型會比uint64高效。 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sfixed32 | 老是4個字節 | int32 | int | int | int32 | Fixnum 或者 Bignum(根據須要) | int | integer |
sfixed64 | 老是8個字節 | int64 | long | int/long | int64 | Bignum | long | integer/string |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | 一個字符串必須是UTF-8編碼或者7-bit ASCII編碼的文本。 | string | String | str/unicode | string | String (UTF-8) | string | string |
bytes | 可能包含任意順序的字節數據。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string |
你能夠在文章Protocol Buffer 編碼中,找到更多「序列化消息時各類類型如何編碼」的信息。
(1).在java中,無符號32位和64位整型被表示成他們的整型對應形式,最高位被儲存在標誌位中。
(2).對於全部的狀況,設定值會執行類型檢查以確保此值是有效。
(3).64位或者無符號32位整型在解碼時被表示成爲ilong,可是在設置時可使用int型值設定,在全部的狀況下,值必須符合其設置其類型的要求。
(4).python中string被表示成在解碼時表示成unicode。可是一個ASCIIstring能夠被表示成str類型。
(5).Integer在64位的機器上使用,string在32位機器上使用。
9.默認值
當一個消息被解析的時候,若是編碼消息裏不包含一個特定的singular元素,被解析的對象鎖對應的字段被設置爲一個默認值,對於不一樣類型指定以下:
可重複字段的默認值是empty(一般狀況下是對應語言中空列表)。
注:對於標量消息字段,一旦消息被解析,就沒法判斷字段是被設置爲默認值(例如:例如boolean值是否被設置爲false)仍是根本沒有被設置,你應該在定義你的消息類型時很是注意。例如:好比你不該該定義boolean的默認值false做爲任何行爲的觸發方式,也應該注意若是一個標量消息字段被設置爲標誌位,這個值不會被序列化。
10.枚舉
當須要定義一個消息類型的時候,可能想爲一個字段指定某「預約義值序列」中的一個值。
例如:假設要爲每個SearchRequest消息添加一個 corpus字段,而corpus的值多是UNIVERSAL,WEB,IMAGES中的一個。 其實能夠很容易地實現這一點:經過向消息定義中添加一個枚舉(enum)而且爲每一個可能的值定義一個常量就能夠了。
在下面的例子中,在消息格式中添加了一個叫作Corpus的枚舉類型——它含有全部可能的值 ——以及一個類型爲Corpus的字段:
message SearchRequest { enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; } string query = 1; int32 page_number = 2; int32 result_per_page = 3; Corpus corpus = 4; }
如你所見,Corpus枚舉的第一個常量映射爲0:每一個枚舉類型必須將其第一個類型映射爲0,這是由於:
你能夠經過將不一樣的枚舉常量指定爲相同的值。若是這樣作你須要將allow_alias 選項設置爲true,不然編譯器會在別名的地方產生一個錯誤信息。
enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1; } enum EnumNotAllowingAlias { UNKNOWN = 0; //EnumNotAllowingAlias中沒有設置allow_alias STARTED = 1; // RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside. }
枚舉常量必須在32位整型值的範圍內。由於enum值是使用可變編碼方式的,對負數不夠高效,所以不推薦在enum中使用負數。
能夠在 一個消息定義的內部或外部定義枚舉,這些枚舉能夠在.proto文件中的任何消息定義裏重用。固然也能夠在一個消息中聲明一個枚舉類型,而在另外一個不一樣的消息中使用它——採用MessageType.EnumType的語法格式。
當對一個使用了枚舉的.proto文件運行protocol buffer編譯器的時候,生成的代碼中將有一個對應的enum(對Java或C++來講),或者一個特殊的EnumDescriptor類(對 Python來講),它被用來在運行時生成的類中建立一系列的整型值符號常量(symbolic constants)。
在反序列化的過程當中,沒法識別的枚舉值會被保存在消息中,雖然這種表示方式須要依據所使用語言而定。在那些支持開放枚舉類型超出指定範圍以外的語言中(例如C++和Go),爲識別的值會被表示成所支持的整型。在使用封閉枚舉類型的語言中(Java),使用枚舉中的一個類型來表示未識別的值,而且可使用所支持整型來訪問。在其餘狀況下,若是解析的消息被序列號,未識別的值將保持原樣。
11.使用其餘消息類型
你能夠將其餘消息類型用做字段類型。例如,假設在每個SearchResponse消息中包含Result消息,此時能夠在相同的.proto文件中定義一個Result消息類型,而後在SearchResponse消息中指定一個Result類型的字段,如:
message SearchResponse { repeated Result results = 1; } message Result { string url = 1; string title = 2; repeated string snippets = 3; }
在上面的例子中,Result消息類型與SearchResponse是定義在同一文件中的。若是想要使用的消息類型已經在其餘.proto文件中已經定義過了呢?
你能夠經過導入(importing)其餘.proto文件中的定義來使用它們。要導入其餘.proto文件的定義,你須要在你的文件中添加一個導入聲明,如:
import "myproject/other_protos.proto";
默認狀況下你只能使用直接導入的.proto文件中的定義. 然而, 有時候你須要移動一個.proto文件到一個新的位置, 能夠不直接移動.proto文件, 只需放入一個僞 .proto 文件在老的位置, 而後使用import public轉向新的位置。import public 依賴性會經過任意導入包含import public聲明的proto文件傳遞。例如:
//這是新的proto //All definitions are moved here
//這是舊的proto //這是全部客戶端正在導入的包 import public "new.proto"; import "other.proto";
//客戶端proto import "old.proto"; //如今你可使用新舊兩種包的proto定義了。
經過在編譯器命令行參數中使用-I/--proto_pathprotocal 編譯器會在指定目錄搜索要導入的文件。若是沒有給出標誌,編譯器會搜索編譯命令被調用的目錄。一般你只要指定proto_path標誌爲你的工程根目錄就好。而且指定好導入的正確名稱就好。
在你的proto3消息中導入proto2的消息類型也是能夠的,反之亦然,而後proto2枚舉不能夠直接在proto3的標識符中使用(若是僅僅在proto2消息中使用是能夠的)。
你能夠在其餘消息類型(多重嵌套)中定義、使用消息類型,在下面的例子中,Result消息就定義在SearchResponse消息內,如:
message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1; }
若是你想在它的父消息類型的外部重用這個消息類型,你須要以Parent.Type的形式使用它,如:
message SomeOtherMessage { SearchResponse.Result result = 1; }
12.更新一個消息類型
若是一個已有的消息格式已沒法知足新的需求——如,要在消息中添加一個額外的字段——可是同時舊版本寫的代碼仍然可用。不用擔憂!更新消息而不破壞已有代碼是很是簡單的,在更新時只要記住如下的規則便可。
13.Any類型
Any類型消息容許你在沒有指定他們的.proto定義的狀況下使用消息做爲一個嵌套類型。一個Any類型包括一個能夠被序列化bytes類型的任意消息,以及一個URL做爲一個全局標識符和解析消息類型。
爲了使用Any類型,你須要導入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。
不一樣語言的實現會支持動態庫以線程安全的方式去幫助封裝或者解封裝Any值。例如在java中,Any類型會有特殊的pack()
和unpack()
訪問器,在C++中會有PackFrom()
和UnpackTo()
方法。
14.Oneof
若是你的消息中有不少可選字段, 而且同時至多一個字段會被設置, 你能夠增強這個行爲,使用oneof特性節省內存.
Oneof字段就像可選字段,除了它們會共享內存,至多一個字段會被設置,設置其中一個字段會清除其它字段。 你可使用case()或者WhichOneof() 方法檢查哪一個oneof字段被設置, 看你使用什麼語言了.
爲了在.proto定義Oneof字段, 你須要在名字前面加上oneof關鍵字, 好比下面例子的test_oneof:
message SampleMessage { oneof test_oneof { string name = 4; SubMessage sub_message = 9; } }
而後你能夠增長oneof字段到 oneof 定義中. 你能夠增長任意類型的字段, 可是不能使用repeated 關鍵字.
在產生的代碼中, oneof字段擁有一樣的 getters 和setters, 就像正常的可選字段同樣. 也有一個特殊的方法來檢查到底那個字段被設置. 你能夠在相應的語言API指南中找到oneof API介紹.
Oneof 特性:
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); message.mutable_sub_message(); // Will clear name field. CHECK(!message.has_name());
SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Will delete sub_message sub_message->set_... // Crashes here
SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK(msg2.has_name());
向後兼容性問題
當增長或者刪除oneof字段時必定要當心. 若是檢查oneof的值返回None/NOT_SET, 它意味着oneof字段沒有被賦值或者在一個不一樣的版本中賦值了。 你不會知道是哪一種狀況,由於沒有辦法判斷若是未識別的字段是一個oneof字段。
Tag 重用問題:
15.Map
若是你但願建立一個關聯映射,protocol buffer提供了一種快捷的語法:
map<key_type, value_type> map_field = N;
其中key_type能夠是任意Integer或者string類型(因此,除了floating和bytes的任意標量類型都是能夠的)value_type能夠是任意類型。
例如,若是你但願建立一個project的映射,每一個Projecct使用一個string做爲key,你能夠像下面這樣定義:
map<string, Project> projects = 3;
向後兼容性問題
map語法序列化後等同於以下內容,所以即便是不支持map語法的protocol buffer實現也是能夠處理你的數據的:
message MapFieldEntry { key_type key = 1; value_type value = 2; } repeated MapFieldEntry map_field = N;
16.Package
固然能夠爲.proto文件新增一個可選的package聲明符,用來防止不一樣的消息類型有命名衝突(相似C++裏面的命名空間)。如:
package foo.bar;
message Open {
...
}
在其餘的消息格式定義中可使用包名+消息名的方式來定義字段的類型,如:
message Foo { ... required foo.bar.Open open = 1; ... }
包的聲明符會根據使用語言的不一樣影響生成的代碼。
包及名稱的解析
Protocol buffer語言中類型名稱的解析與C++是一致的:首先從最內部開始查找,依次向外進行,每一個包會被看做是其父類包的內部類。固然對於 (foo.bar.Baz)這樣以「.」分隔的意味着是從最外圍開始的。
ProtocolBuffer編譯器會解析.proto文件中定義的全部類型名。 對於不一樣語言的代碼生成器會知道如何來指向每一個具體的類型,即便它們使用了不一樣的規則。
17.定義服務(Service)
若是想要將消息類型用在RPC(遠程方法調用)系統中,能夠在.proto文件中定義一個RPC服務接口,protocol buffer編譯器將會根據所選擇的不一樣語言生成服務接口代碼及存根。如,想要定義一個RPC服務並具備一個方法,該方法可以接收 SearchRequest並返回一個SearchResponse,此時能夠在.proto文件中進行以下定義:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
最直觀的使用protocol buffer的RPC系統是gRPC,一個由谷歌開發的語言和平臺中的開源的PRC系統,gRPC在使用protocl buffer時很是有效,若是使用特殊的protocol buffer插件能夠直接爲您從.proto文件中產生相關的RPC代碼。
若是你不想使用gRPC,也可使用protocol buffer用於本身的RPC實現,你能夠從proto2語言指南中找到更多信息
18.JSON 映射
Proto3 支持JSON的編碼規範,使他更容易在不一樣系統之間共享數據,在下表中逐個描述類型。
若是JSON編碼的數據丟失或者其自己就是null,這個數據會在解析成protocol buffer的時候被表示成默認值。若是一個字段在protocol buffer中表示爲默認值,它會在轉化成JSON編碼的時候忽略掉以節省空間。具體實現能夠提供在JSON編碼中可選的默認值。
proto3 | JSON | JSON示例 | 注意 |
---|---|---|---|
message | object | {「fBar」: v, 「g」: null, …} | 產生JSON對象,消息字段名能夠被映射成lowerCamelCase形式,而且成爲JSON對象鍵,null被接受併成爲對應字段的默認值 |
enum | string | 「FOO_BAR」 | 枚舉值的名字在proto文件中被指定 |
map | object | {「k」: v, …} | 全部的鍵都被轉換成string |
repeated V | array | [v, …] | null被視爲空列表 |
bool | true, false | true, false | |
string | string | 「Hello World!」 | |
bytes | base64 string | 「YWJjMTIzIT8kKiYoKSctPUB+」 | |
int32, fixed32, uint32 | number | 1, -10, 0 | JSON值會是一個十進制數,數值型或者string類型都會接受 |
int64, fixed64, uint64 | string | 「1」, 「-10」 | JSON值會是一個十進制數,數值型或者string類型都會接受 |
float, double | number | 1.1, -10.0, 0, 「NaN」, 「Infinity」 | JSON值會是一個數字或者一個指定的字符串如」NaN」,」infinity」或者」-Infinity」,數值型或者字符串都是可接受的,指數符號也能夠接受 |
Any | object | {「@type」: 「url」, 「f」: v, … } | 若是一個Any保留一個特上述的JSON映射,則它會轉換成一個以下形式:{"@type": xxx, "value": yyy} 不然,該值會被轉換成一個JSON對象,@type 字段會被插入所指定的肯定的值 |
Timestamp | string | 「1972-01-01T10:00:20.021Z」 | 使用RFC 339,其中生成的輸出將始終是Z-歸一化啊的,而且使用0,3,6或者9位小數 |
Duration | string | 「1.000340012s」, 「1s」 | 生成的輸出老是0,3,6或者9位小數,具體依賴於所須要的精度,接受全部能夠轉換爲納秒級的精度 |
Struct | object | { … } | 任意的JSON對象,見struct.proto |
Wrapper types | various types | 2, 「2」, 「foo」, true, 「true」, null, 0, … | 包裝器在JSON中的表示方式相似於基本類型,可是容許nulll,而且在轉換的過程當中保留null |
FieldMask | string | 「f.fooBar,h」 | 見fieldmask.proto |
ListValue | array | [foo, bar, …] | |
Value | value | 任意JSON值 | |
NullValue | null | JSON null |
19.選項
定義.proto文件時可以標註一系列的option。Option並不改變整個文件聲明的含義,但卻可以影響特定環境下處理方式。完整的可用選項能夠在google/protobuf/descriptor.proto找到。
一些選項是文件級別的,意味着它能夠做用於最外範圍,不包含在任何消息內部、enum或服務定義中。一些選項是消息級別的,意味着它能夠用在消息定義的內部。固然有些選項能夠做用在字段、enum類型、enum值、服務類型及服務方法中。到目前爲止,並無一種有效的選項能做用於全部的類型。
以下就是一些經常使用的選項:
option java_package = "com.example.foo";
option java_outer_classname = "Ponycopter";
option optimize_for = CODE_SIZE;
int32 old_field = 6 [deprecated=true];
20.生成訪問類
能夠經過定義好的.proto文件來生成Java,Python,C++, Ruby, JavaNano, Objective-C,或者C# 代碼,須要基於.proto文件運行protocol buffer編譯器protoc。若是你沒有安裝編譯器,下載安裝包並遵守README安裝。對於Go,你還須要安裝一個特殊的代碼生成器插件。你能夠經過GitHub上的protobuf庫找到安裝過程
經過以下方式調用protocol編譯器:
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