由于工程項目中擬采用一種簡便高效的數據交換格式,百度了一下發現除了采用 xml、JSON 還有 ProtoBuf(Google 出品),趕緊去瞄了一下。花了一個周末的時間把它走馬觀花的學習了一下,順便將官方的指南翻譯了出來。
首先申明,哥們兒英語高中水平,借助了必應詞典勉強將其意譯了出來,如果你發現翻譯中有紕漏,請一定不要告訴我~
怕有誤人子弟之嫌,先貼上官方文檔的地址,本譯文僅供參考:
https://developers.google.com/protocol-buffers/docs/proto3
Proto3 語言指南
- 定義消息類型
- 標準類型
- 默認值
- 枚舉
- 使用其它消息類型
- 嵌套
- 更新消息類型
- Any
- Oneof
- Map 類型
- 包
- 定義服務
- JSON 映射
- 選項
- 創建您的類
本指南描述如何使用 ProtoBuf 語言規范來組織你的.proto 文件,以及如何編譯.proto 文件來生成相應的操作類。它涵蓋了proto3 語法,如果你想查看老版本 proto2 的相關信息,請參考《Proto2 語言指南》
這是一個參考指南---通過一個例子一步一步地介紹本文檔描述的 proto3 語言特性,請根據你選擇的編程語言參考基礎教程。
定義消息類型
讓我們先來看一個簡單的例子。假設你想定義一個搜索請求的消息格式,它包含一個查詢字符串、一個你感興趣的特定頁號、以及每頁結果數。下面就是這個.proto 文件所定義的消息類型。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 文件的第一行指定你正在使用 proto3 語法:如果你不這么做 protocol buffer 編譯器會假設您使用的是 proto2 。 這一行不允許存在空白字符或注釋。
- 這個 SearchRequest 消息定義了三個字段(名稱/值對),每一條 SearchRequest 消息類型的數據都包含這三個字段定義的數據。每個字段包含一個名稱和類型。
指定字段類型
在上面的例子中,所有的字段都是 標準 類型:二個整形(page_number 和 resulet_per_page)和一個字符串類型(query)。然而,你也可以用復雜類型來定義字段,包括 枚舉 和其它消息類型。
指定標簽
通過上面的例子你可以看到,這里每個字段都定義了一個唯一的數值標簽。 這些唯一的數值標簽用來標識 二進制消息 中你所定義的字段,一旦定義了編譯后就無法修改。需要特別提醒的是標簽 1–15 標識的字段編碼僅占用 1 個字節(包括字段類型和標識標簽),更多詳細的信息請參考 ProtoBuf 編碼 。 數值標簽 16–2047 標識的字段編碼占用 2 個字節。因此,你應該將標簽 1–15 留給那些在你的消息類型中使用頻率高的字段。記得預留一些空間(標簽 1–15)給將來可能添加的高頻率字段。
最小的數值標簽是 1, 最大值是 2 29 - 1, 即 536,870,911。 你不能使用的標簽范圍還有:19000–19999
( FieldDescriptor::kFirstReservedNumber – FieldDescriptor::kLastReservedNumber ),這些是 ProtoBuf 系統預留的,如果你在你的.proto 文件中使用了其中的數值標簽,protoc 編譯器會報錯。同樣地,你不能使用保留字段中 reserved 關鍵字定義的標簽。
定義字段的規則
消息的字段可以是一下情況之一:
- 單數(默認):該字段可以出現 0 或 1 次(不能大于 1 次)。
- 可重復(repeated):該字段可以出現任意次(包含 0)。 可重復字段數值的順序是系統預定義的。
由于一些歷史原因,默認情況下,數值類型的可重復(repeated)字段的編碼性能沒有想象中的好,你應該在其后用特殊選項 [packed=true] 來申
明以獲得更高效的編碼。 例如:
repeated int32 samples = 4 [packed=true];
你能夠在 ProtoBuf 編碼 中查閱更多的關于 packed 關鍵字的信息
添加更多的消息類型
同一個.proto 文件中可以定義多個消息類型。這在定義多個相關的消息時非常有用。例如,如果你想針對用于搜索查詢的 SearchRequest 消息定義
一個保存查找結果的 SearchResponse 消息,你可以把它們放在同一個.proto 文件中:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
添加注釋
在.proto 文件中,使用 C/C++格式的注釋語法 // syntax
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
保留字段
如果你通過直接刪除或注釋一個字段的方式 更新 了一個消息結構,將來別人在更新這個消息的時候可能會重復使用標簽。如果他們以后加載舊版
本的相同的.proto 文件,可能會導致嚴重的問題。包括數據沖突、 隱秘的 bug 等等。為了保證這種情況不會發生,當你想刪除一個字段的時候,
可以使用 reserved 關鍵字來申明該字段的標簽(和/或名字,這在 JSON 序列化的時候也會產生問題)。 將來如果有人使用了你使用 reserved
關鍵字定義的標簽或名字,編譯器就好報錯。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意:你不能同時在一條 reserved 語句中申明標簽和名字。
.proto 文件編譯生成了什么?
當你使用 protoc 編譯器 編譯一個.proto 文件的時候,編譯器會根據你選擇的語言和你在這個.proto 文件定義的消息類型生成代碼,,這些代碼的
功能包括:字段值的 getter,setter,消息序列化并寫入到輸出流,從輸入流接反序列化讀取消息等。
對于 C++語言,編譯器會根據定義的.proto 文件編譯生成一個.h 頭文件和一個.cc 源碼實現文件。
4
- 對于 Java 語言,編譯器會為每一個消息類型創建一個帶類的.java 文件,同時這個 java 文件中包含用來創建該消息類型的實例的特殊 Builder 構造
類。 - Python 語言有點不一樣---編譯器在你定義的.proto 文件中創建一個包含靜態描述符的模塊,每個消息類型對應一個靜態描述符,在 Python 程序解
釋運行的時候,會根據靜態描述符用一個元類去創建相應的數據訪問類。 - 對于 Go 語言,針對每一個定義的消息類型編譯器會創建一個帶類型的.pb.go 文件。
- 對于 Ruby 語言,編譯器會創建一個帶 Ruby 模塊的.rb 文件,其中包含了所有你定義的消息類型。
- 對于 JavaNano,編譯器會創建 Java 語言類似的輸出文件,但是沒有 Builder 構造類。
- 對于 Ojective-C,編譯器會創建一個 pbobjc.h 和一個 pbobjc.m 文件,為每一個消息類型都創建一個類來操作。
- 對于 C#語言,編譯器會為每一個.proto 文件創建一個.cs 文件,為每一個消息類型都創建一個類來操作。
針對所選擇的不同的編程語言,你能夠在后續的教程中找到更多的關于操作它們的編程接口(proto3 版本的即將推出)。 更詳細
的針對特定編程語言的 API 操作細節,請參考 API 參考 。
標準類型
.proto文件中消息結構里用于定義字段的標準數據類型如下表所示,后面幾列是.proto文件中定義的標準類型編譯轉換后在編程語言中的類型對照。
如果你想了解這些數據類型在序列化的時候如何編碼,請參考 ProtoBuf 編碼 。
[1] 在 Java 中,無符號的 32 位整形和 64 位整形都是用的相應的有符號整數表示,最高位儲存的是符號標志。
[2] 在任何情況下,給字段賦值都會執行類型檢查,以確保所賦的值是有效的。
5
[3] 默認情況下 64 位整數或 32 位無符號整數通常在編碼的時候都是用 long 類型表示,但是你可以在設定字段的時候指定位 int 類型。 在任何情況
下,這個值必須匹配所設定的數據類型。參考[2]
[4] Python 中字符串通常編碼為 Unicode,但是如果給定的字符串是 ASCII 類型,可以設置位 str 類型(可能會有變化)
默認值
當一個消息被解析的時候,如果在編碼后的消息結構中某字段沒有初始值,相應的字段在被解析的對象中會被設置默認值。這些默認值都是類型相關的。
- 字符串默認值為空字符串。
- 字節類型默認值是空字節。
- 布爾類型默認值為 false。
- 數值類型默認值位 0。
- 枚舉 類型默認值是第一個枚舉元素,它必須為 0。
- 消息類型字段默認值為 null。
可重復類型字段的默認值為(相應編程語言中的)空列表。
需要提醒的是:對于標準數據類型的字段,當消息被解析的時候你是沒有辦法顯示地設定默認值的(例如布爾類型是否默認設置為 false),記住當你定義自己的消息類型的時候不要設置它的默認值。例如,不要在你的消息類型中定義一個表示開關變量的布爾類型字段,如果你不希望它默認初始化為 false 的話。 還要注意的是,在序列化的時候,如果標準類型的字段的值等于它的默認值,這個值是不會存儲到介質上的。
枚舉
當你定義一個消息的時候,你可能會希望某個字段在預定的取值列表里面取值。 例如,假設你想為 SearchRequest 消息定義一個 corpus字段,它的取值可能是 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或者 VIDEO。你只需要簡單的利用 enum 關鍵字定義一個枚舉類型,它的每一個可能的取值都是常量。
在下面的例子中,我們定義了一個名為 Corpus 的枚舉類型,并用它定義了一個字corpus。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
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;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
枚舉常量的取值范圍是 32 位整數值的范圍。 由于枚舉的值采用 varient 編碼 方式,負數編碼效率低所以不推薦。枚舉類型可以定義在消息結構體內部(上例所示),也可以定義在外部。如果定義在了外部,同一個.proto 文件中的所有消息都能重用這個枚舉類型。當然,你也可以用
MessageType.EnumType 語法格式在一個消息結構內部引用其它消息結構體內部定義的枚舉類型來定義字段。
當你用 protoc 編譯器編譯一個包含枚舉類型的.proto 文件時,對于 Java 或 C++編譯生成的代碼中會包含相應的枚舉類型,對于 Python 語言會生成
一個特殊的 EnumDescriptor 類,在 Python 運行時會生成一系列整形的符號常量供程序使用。
在消息反序列化的時候,無法識別的枚舉類型會被保留在消息中,但是這種枚舉類型如何復現依賴于所使用的編程語言。對于支持開放式枚舉類型的編程語言,枚舉類型取值超出特定的符號范圍,例如 C++和 Go 語言,未知的枚舉值在復現的時候簡單地以基礎型整數形式存儲。對于支持封閉式枚舉類型的編程語言,例如 Java,未知的枚舉值可以通過特殊的訪問函數讀取。在任何情況下,只要消息被序列化,無法識別的枚舉值也會跟著被序列化。
欲詳細了解枚舉類型如何在消息類型內工作,請根據你選擇的編程語言,參考 生成代碼參考
使用其它消息類型
你可以使用其它消息類型來定義字段。 假如你想在每一個 SearchResponse 消息里面定義一個 Result 消息類型的字段,你只需要同一個.proto文件中定義 Result 消息,并用它來定義 SearchResponse 中的一個字段即可。
message SearchResponse {
repeated Result result = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
導入定義
在上面的例子中,Result 消息類型和 SearchResponse 消息類型是定義在同一個文件中的,如果你想用另外一個.proto 文件中定義的消息類型來定義字段該怎么做呢?
你可以導入其它.proto 文件中已經定義的消息,來定義該消息類型的字段。為了導入其它.proto 文件中的定義,你需要在你的.proto 文件頭部申明import 語句:
import "myproject/other_protos.proto";
默認情況下,你只能使用直接導入的.proto 文件中的定義。然而,有時候你可能需要移動一個.proto 文件到一個新的位置。如果直接移動這個.proto文件,你需要一次更新所有引用了這個.proto 文件的所有調用站點。你可以將文件移動之后(譯注:下面例子中的 new.proto),在原來的位置創建一個虛擬文件(譯注:下面例子中的 old.proto),在其中用 import public 申明指向新位置的.proto 文件(譯注:這樣就實現了跨文件引用,而不需要更新調用站點里面的代碼了)。通過 import public 申明的導入依賴關系可以被任何調用了包含該 import public 語句的調用者間接繼承。(譯注:這段話繞來繞去就是說,默認情況下,a 中 import b,b 中 import c,a 只能引用 b 里面的定義,而不能引用 c 里面的定義;如果你想 a 跨文件導入引用 c 里面的定義,就要在 b 中申明 import public c,這樣 a 既能引用 b 里面的定義,又能引用 c 里面的定義了) 例如:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
protoc 編譯器通過命令行參數 -I 或 --proto_path 指定導入文件的搜索查找目錄 。如果沒有指定該參數,默認在編譯器所在目錄查找。 通常,你需要設定 --proto_path 參數為你的項目根目錄,使用全名稱目錄路徑指定導入文件搜索路徑。
使用 proto2 消息類型
你在 proto3 中可以引用 proto2 消息類型,反之亦可。 然而,proto2 語法格式的枚舉類型,不可以在 proto3 中引用。
消息嵌套
你可以在一個消息結構內部定義另外一個消息類型,如下例所示---Result 消息類型定義在 SearchResponse 消息體內部。
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果你想在它的父消息外部復用這個內部定義的消息類型,可以采用 Parent.Type 語法格式:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
只要你愿意,消息可以嵌套任意層。
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
更新一個消息類型
如果現有的消息類型無法滿足你的需要---例如,你想為這個消息添加一個字段,但是你又想沿用原來的代碼格式,不用擔心!
你可以非常簡單地就能在不破壞現有代碼的基礎上更新這個消息類型。只需要遵循以下原則:
- 不要修改現有字段的數值標簽。
- 如果你為一個消息添加了字段,所有已經序列化調者依然可以通過舊的消息格式解析新消息生成的代碼。 你需要注意的是這些元素的 默認值 ,以便新代碼可以真確地與舊代碼生成的消息交互。 同理,舊代碼也可以解析新代碼生成的消息:原二進制程序在解析新的消息時,會忽略新添加的字段。 注意:所有未知字段在消息反序列化的時候會被自動拋棄,所以當消息傳遞給新代碼時,新字段不可用。(這和 proto2 不一樣,proto2 中位置字段也會跟消息一起序列化)
- 字段可以被移除,只要你不再在你更新后的消息里面使用被刪除字段的數值標簽即可。 或許你想重命名一個字段,通過添加前綴"OBSOLETE_"或者通過 reserved 關鍵字申明原數值標簽,以免將來別人重用這個數值。
- int32, uint32, int64, uint64, 和 bool 類型是相互兼容的,這意味著你可以改變這類字段的數值類型,而不必擔心破壞了向前或向后兼容性。 如果數值從傳輸介質上解析的時候,不匹配相應的數值類型,其效果相當于你在 C++里面做了類型轉換(例如,一個 64 位的整數被解析為 32 位整數時,它會被轉換為 32 位整數)。
- sint32 和 sint64 互相兼容,但是它們不和其它整數類型兼容。
- 只要 bytes 類型是有效的 UTF-8 格式,它就和 string 類型兼容。
- 內嵌的消息類型和包含該消息類型編碼后的字節內容的 bytes 類型兼容。
- fxied32 與 sfixed32 兼容,同樣的,fixed64 與 sfixed64 兼容。
Any
Any 類型允許你在沒有某些消息類型的.proto 定義時,像使用內嵌的消息類型一樣使用它來定義消息類型的字段。一個 Any 類型的消息是一個包含任意字節數的序列化消息,擁有一個 URL 地址作為全局唯一標識符來解決消息的類型。為了使用 Any 類型的消息,你需要import google/protobuf/any.proto
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated Any details = 2;
}
給定 Any 消息類型的默認 URL 是: type.googleapis.com/packagename.messagename。
不同的語言實現都會支持運行庫幫助通過類型安全的方式來封包或解包 Any 類型的消息。在 Java 語言中,Any 類型有專門的訪問函數 pack()和unpack()。在 C++中對應的是 PackFrom()和 PackTo()方法。
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
if (detail.IsType<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
當前,Any 類型的運行時庫還在開發中。
如果你已經熟悉 proto2 語法 ,Any 類型就是替代了 proto2 中的 extensions 。
Oneof
如果你的消息中定義了很多字段,而且最多每次只能有一個字段被設置賦值,那么你可以利用 Oneof 特性來實現這種行為并能節省內存。
Oneof 字段除了擁有常規字段的特性之外,所有字段共享一片 oneof 內存,而且每次最多只能有一個字段被設置賦值。設置 oneof組中的任意一個成員的值時,其它成員的值被自動清除。 你可以用 case()或 WhickOneof()方法檢查 oneof 組中哪個成員的值被設置了,具體選擇哪個方法取決于你所使用的編程語言。
使用 Oneof
在.proto 文件中定義 oneof 需要用到 oneof 關鍵字,其后緊跟的是 oneof 的名字。下例中的 oneof 名字是 test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后你就可以往 oneof 定義中添加 oneof 的字段了。 你可以使用任何類型的字段,但是不要使用 repeated 可重復字段。
在編譯后生成的代碼中,oneof 字段和常規字段一樣擁有 setter 或 getter 操作函數。根據你所選擇的編程語言,你能夠找到一個特殊方法函數用來檢查是哪一個 oneof 被賦值了。 更多詳情請參考 API 參考 。
Oneof 特性
- 設置 oneof 組中某一個成員的值時,其它成員的值被自動清除。因此,當你的 oneof 組中有多個成員的時候,只有最有一個被賦
值的字段擁有自己的值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
- 如果解析器發現多個 oneof 組的成員被存儲在介質上,只有最后一個成員被解析到消息中。
- oneof 字段不能是 repeated 可重復的。
- oneof 字段可以使用反射函數。
- 如果你正在使用 C++,確認沒有代碼造成內存崩潰。 在下面的例子中,代碼會造成內存崩潰。因為 sub_message 在調用 set_name("name")的時候已經被清除.
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
- 在 C++中,如果你調用 swap()方法交換兩個擁有 oneof 的消息,被交換的消息會擁有對方的 oneof 實例:在下面的例子中,msg1 會擁有一個sub_message,而 msg2 擁有 name。
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。 沒有辦法區分它們,因此沒法區分傳輸介質上的一個未知字段是不是 oneof 成
員。(譯注:on the wire,我翻譯為傳輸介質不知道準確否?)
標簽重用的問題
- 將字段移進或移出 oneof 組: 在消息序列化和解析之后,可能會丟失某些信息(某些字段會被清除)。
- 刪除一個 oneof 字段之后又添加回去: 在消息序列化和解析之后,可能會清除當前為 oneof 設置的值。
- 分割或合并 oneof: 這種情況和移動常規字段到 oneof 組的類似。
Map
如果你想定義 map 類型的數據,ProtoBuf 提供非常便捷的語法:
map<key_type, value_type> map_field = N;
其中,key_type 可以任意整數類型或字符串類型(除浮點類型或 bytes 類型意外的任意 標準類型 )。value_Type 可以是任意類型。
例如,你可以創建一個 map 類型的 projects,關聯一個 string 類型和一個 Project 消息類型(譯注:作為鍵-值對),用 map 定義如下:
map<string, Project> projects = 3;
map 類型的字段不可重復(不能用 repeated 修飾)。 需要注意的是:map 類型字段的值在傳輸介質上的順序和迭代器的順序是未定義的,你不能指望 map 類型的字段內容按指定順序排列。
proto3 目前支持的所有語言都 map 類型的操作 API。 欲知詳情,請根據你所選擇的編程語言閱讀 API 參考 中相關內容。
向后兼容性
在傳輸介質上,下面的代碼等效于 map 語法的實現。因此,即使目前 ProtoBuf 不支持 map 可重復的特性依然可以用下面這種(變通的)方式來處理:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
包
你可以在.proto 文件中選擇使用 package 說明符,避免 ProtoBuf 消息類型之間的名稱沖突。(譯注:這個和 java 里面包的概念以及 C++中命名空間的作用一樣)
package foo.bar;
message Open { ...}
你可以在消息類型內部使用包描述符的引用來定義字段:
message Foo {
...
foo.bar.Open open = 1;
...
}
包描述符對編譯后生成的代碼的影響依賴于你所選擇的編程語言:
- 在 C++中,生成的類被包裝在以包描述符命名的命名空間中。 例如,Open 類將會出現在命名空間 foo::bar 中。
- 在 Java 里面,除非你在.proto 文件中顯示聲明了 option java_package,否則這個包名會被 Java 直接采用。
- 在 Python 里面,包名被直接忽略了,因為 Python 模塊的組織依據的是 Python 文件的在文件系統中的存放位置。
- 在 Go 中,除非你在.proto 文件中顯示聲明了 option go_package,否則這個包名會被 Go 作為包名使用。
- 在 Ruby 里面,所生成的類被包裹在嵌套的 Ruby 命名空間中,包名被轉換為 Ruby 大寫樣式(第一個字母大寫,如果第一個不是字母字符,添加PB_前綴)。例如,Open 類會出現在命名空間 Foo::Bar 中。
- 在 JavaNano 中,除非你在.proto 文件中顯示聲明了 option java_package,否則這個包名會被作為 Java 包名使用。
包和名稱解析
ProtoBuf 解析包和名稱的方式與 C++語言類似:最內層的最先被查找,然后是次內層,以此類推。每個包對于其父包來說都是“內部”的。以一個'.'(英文句點)開頭的包和名稱 (例如 .foo.bar.Baz)表示從最外層開始查找。
protoc 編譯器通過解析被導入的.proto 文件來解析所有的類型名稱。 任何一種被支持的語言的代碼生成器都知道如何正確地引用每一種類型,即使該語言具有不同的作用域規則。
定義服務
如果想在 RPC(遠程過程調用)系統中使用自定義的消息類型,你可以在.proto 文件中定義一個 RPC 服務,protoc 編譯器就會根據你選擇的編程語言生成服務器接口和存根代碼。例如,如果你想定義一個 RPC 服務,擁有一個方法來根據你的 SearchRequest 返回SearchResponse
,可以在你的.proto 文件中這樣定義:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
與 ProtoBuf 最佳搭配的 RPC 系統是 gRPC :一個 Google 開發的平臺無關語言無關的開源 RPC 系統。gRPC 和 ProtoBuf 能夠非常完美的配合,你可以使用專門的 ProtoBuf 編譯插件直接從.proto 文件生成相關 RPC 代碼。
如果你不想使用 gRPC,你也可以用自己的 RPC 來實現和 ProtoBuf 協作。 更多的關于RPC 的信息請參考 Proto2 語言指南 。
現在也有很多第三方的采用 ProtoBuf 的 RPC 項目在開展中。 我們已知的這類項目列表,請參考 第三方插件 WIKI 主頁 。
JSON 映射
Proto3 支持標準的 JSON 編碼,在不同的系統直接共享數據變得簡單。下表列出的是基礎的類型對照。
在 JSON 編碼中,如果某個值被設置為 null 或丟失,在映射為 ProtoBuf 的時候會轉換為相應的 默認值 。 在 ProtoBuf 中如果一個字段是默認值,在映射為 JSON 編碼的時候,這個默認值會被忽略以節省空間。可以通過選項設置,使得 JSON 編碼輸出中字段帶有默認值。
選項
你可以在.proto 文件中可以聲明若干選項。 選項不會改變整個聲明的含意,但可能會影響在特定的上下文中它的處理方式。 完整的可用選項列表在文件google/protobuf/descriptor.proto 中定義。
一些選項是文件級的,它們應該聲明在最頂級的作用域范圍內,不要在消息、枚舉或服務定義中使用它們。一些選項是消息級的,應該在消息結構內聲明。一些選項是字段級的,它們應該聲明在字段定義語句中。選項也可以聲明在枚舉類型、枚舉值、服務類型、服務方法中。然而,目前沒有任何有用的選項采用這種方式。
下面列出最常用的一些選項:
- java_package (文件級選項):這個選項聲明你想生成的 Java 類擁有的包名。 如果沒有在.proto 文件中顯示地聲明 java_package 選項,默認采用 proto 的包名(在.proto 文件中用 package 關鍵字定義)作為生成的 Java 類的包名。 然而,通常情況下 proto 包名不是好的 Java 格式的包名,proto 包名一般不會是以 Java 包名所期望的反向域名的格式命名。如果沒有生成 Java 代碼,這個選項沒有任何作用。
option java_package = "com.example.foo";
- java_outer_classname (文件級選項):這個選項定義了你想要編譯生成的 Java 輸出類的最外層類名(也是文件名)。 如果沒有在.proto文件中定義 java_outer_classname 選項,默認將.proto 文件名轉換為駝峰格式的文件名(例如 foo_bar.proto 編譯生成 FooBar.java) 。如果沒有生成 Java 代碼,這個選項沒有任何作用。
option java_outer_classname = "Ponycopter";\
- optimize_for (文件級選項): 它的取值可以是 SPEED,CODE_SIZE,或 LITE_RUNTIME。 這個選項按如下方式影響 C++和 Java 的代碼生成
器(也可能影響第三方的生成器): - SPEED --- 默認值: protoc 編譯器會根據你定義的消息類型生成序列化、解析和執行其它常規操作的代碼。這些代碼是高度優化了的。
- CODE_SIZE:protoc 編譯器會采用共享、反射技術來實現序列化、解析和其它操作的代碼,以生成最小的類。 因此,所生成的代碼比采用選項 SPEED 的要小得多,但是操作性能降低了。類的公共成員函數依然是一樣的(用 SPEED 優化選項也是如此)。 這種模式是最有用的應用情況是:程序包含很多.proto 文件,但并不追求所有模塊都有極快的速度。
- LITE_RUNTIME:protoc 編譯器生成的類,僅依賴"lite"運行時庫 (libprotobuf-lite , 而不是 libprotobuf)。"lite"運行時庫比完整庫 (約一個數量級小) 小得多,但省略了某些功能,如描述符和反射。這對于運行在像手機這樣的有空間限制的平臺上的應用程序特別有用。 protoc 編譯器仍將為所有方法生成最快速的代碼,這和在 SPEED 模式一樣。生成的類將為每一種語言實現 MessageLite版本的接口,只提供了完整的 Message 接口的一個子集的實現。選項 optimize_for = CODE_SIZE;
- cc_enable_arenas(文件級選項): 為生成的 C++代碼 啟用 arena 內存管理功能。(譯注:Arena Allocation,是一種 GC 優化技術,它可以有效地減少因內存碎片導致的 Full GC,從而提高系統的整體性能。)
- objc_class_prefix(文件級選項):這個選項用來設置編譯器從.proto 文件生成的類和枚舉類型的名稱前綴。這個選型沒有默認值。您應該使用 3–5 個大寫字母作為前綴來自 Apple 的建議 。 請注意:所有的 2 個字母的前綴由蘋果公司保留使用。
- packed (字段級選項): 這個選項 如果被設置為 true ,對于基本數值類型的可重復字段可以獲得更緊湊的編碼。使用此選項,沒有負面影響。然而,請注意,在 2.3.0 版本之前是用此選項解析器會忽略被打包的數據。因此,更改現有字段為 packed,會破壞數據傳輸的兼容性。對于 2.3.0 及以后的版本,這種改變是安全的并且解析器能夠同時接受這兩種格式的打包字段的數據,但要小心,如果你要處理舊的程序使用了舊版本的 protobuf。
repeated int32 samples = 4 [packed=true];
- deprecated (字段選項): 這個選項如果設置為 true ,表示該字段已被廢棄,你不應該在后續的代碼中使用它。 在大多數語言中這沒有任何實際的影響。在 Java 中,它會變@Deprecated 注釋。將來,其他特定于語言的代碼生成器在為被標注為 deprecated 的字段生成操作函數的時候,編譯器在嘗試使用該字段的代碼時發出警告。如果不希望將來有人使用使用這個字段,請考慮用 reserved 關鍵字 聲明該字段。
int32 old_field = 6 [deprecated=true];
自定義選項
ProtoBuf 允許你使用自定義選項。這是一個 高級的功能,大多數人不需要。如果你認為你需要創建自定義選項,請參見 Proto2 語言指南 中 更詳細的信息。請注意,創建自定義選項使用 extensions 關鍵字 ,只能用在 proto3 中的創建自定義選項。
編譯創建類
要想從.proto 文件 生成 Java,Python,C++、 Go、 Ruby,JavaNano,Objective-C 或 C#代碼,你需要在.proto 文件中定義自己的消息類型,并且使用 protoc 編譯器來編譯它。如果你還沒有安裝編譯器,請下載安裝包并按照自述文件中的說明來執行安裝。
對于 Go 語言,還需要為編譯器安裝特殊的代碼生成器插件: 請訪問它在 Github 上的代碼倉儲 golang/protobuf ,下載并按照 安裝提示操作。
編譯器的調用格式如下:
--javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
- IMPORT_PATH 指在.proto 文件中解析 import 指令時的查找目錄路徑。如果省略,則使用當前目錄。通過多次傳遞參數--proto_path,可以實現在多個導入目錄中按順序查找。-I=IMPORT_PATH 是 --proto_path 的縮寫形式。
- 您可以提供一個或多個 輸出指令 :
- --cpp_out 在目錄 DST_DIR 中 生成 C++代碼中 。更多細節,請參考 C++代碼的生成。
- --java_out 在目錄 DST_DIR 中 生成 Java 代碼。 更多細節,請參考 Java 代碼的生成。
- --python_out 在目錄 DST_DIR 中 生成 Python 代碼 。更多細節,請參考 Python 代碼的生成。
- --go_out 在目錄 DST_DIR 中 生成 Go 代碼 。 Go 代碼的生成參考文檔即將推出!
- --ruby_out 在目錄 DST_DIR 中生成 Ruby 代碼。Ruby 代碼的生成參考文檔即將推出!
- --javanano_out 在目錄 DST_DIR 中 生成 JavaNano 代碼 。 JavaNano 的代碼生成器有很多選項,你可以自定義編譯輸出,欲知詳情請參考代碼生成器的自述文件 。 JavaNano 代碼的生成參考文檔即將推出!
- --objc_out 在目錄 DST_DIR 中生成 Objective-C 代碼。Objective-C 代碼的生成參考文檔即將推出!
- --csharp_out 在目錄 DST_DIR 中生成 C#代碼。C# 生成代碼的參考文檔即將推出!
額外福利,如果 DST_DIR 以.zip 或 .jar 結尾,編譯器會將代碼輸出寫入到給定名稱的單個 ZIP 格式壓縮文檔中。.jar 輸出也將根據 Java JAR 規范的要求提供 manifest 清單文件。請注意,如果輸出路徑中具有與編譯輸出文件同名的文件,編譯器將直接覆蓋它而不會保存為另外一個名字。 - 您必須提供一個或多個.proto 文件作為編譯輸入。多個.proto 文件可以同時編譯。雖然文件是相對于當前目錄來命名的,每個文件至少要在一個 IMPORT_PATH 指定的路徑范圍內,這樣編譯器可以為其確定規范的名稱。