前言
翻譯查閱外網資料過程中遇到的比較優秀的文章和資料,一是作為技術參考以便日后查閱,二是訓練英文能力。
此文翻譯自 Protocol Buffers 官方文檔 Developer Guide 部分
protocol buffers 介紹
protocol buffers 是一種語言無關、平台無關、可擴展的序列化結構數據的方法,它可用於通信協議、數據存儲等。protocol buffers 是一種靈活,高效,自動化機制的結構數據序列化方法-可類比 XML,但是比 XML 更小、更快、更為簡單。你可以定義數據的結構,然后使用特殊生成的源代碼輕松的在各種數據流中使用各種語言進行編寫和讀取結構數據。你甚至可以更新數據結構,而不破壞根據舊數據結構編譯而成並且已部署的程序。
protocol buffers 如何工作的
你可以通過在 .proto 文件中定義 protocol buffer message 類型,來指定你想如何對序列化信息進行結構化。每一個 protocol buffer message 是一個信息的小邏輯記錄,包含了一系列的 name-value 對。這里有一個非常基礎的 .proto 文件樣例,它定義了一個包含 "person" 相關信息的 message:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
正如你所見,message 格式很簡單 - 每種 message 類型都有一個或多個具有唯一編號的字段,每個字段都有一個名稱和一個值類型,其中值類型可以是數字(整數或浮點數),布爾值,字符串,原始字節,甚至其它 protocol buffer message 類型,這意味着允許你分層次地構建數據。你可以指定 optional 字段,required 字段和 repeated 字段。 你可以在 Protocol Buffer 語言指南 中找到有關編寫 .proto 文件的更多信息。
一旦定義了 messages,就可以在 .proto 文件上運行 protocol buffer 編譯器來生成指定語言的數據訪問類。這些類為每個字段提供了簡單的訪問器(如 name()和 set_name()),以及將整個結構序列化為原始字節和解析原始字節的方法 - 例如,如果你選擇的語言是 C++,則運行編譯器上面的例子將生成一個名為 Person 的類。然后,你可以在應用程序中使用此類來填充,序列化和檢索 Person 的 messages。於是你可以寫一些這樣的代碼:
Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);
之后,你可以重新讀取解析你的 message
fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
你可以在 message 格式中添加新字段,而不會破壞向后兼容性;舊的二進制文件在解析時只是忽略新字段。因此,如果你的通信協議使用 protocol buffers 作為其數據格式,則可以擴展協議而無需擔心破壞現有代碼。
你可以在 API 參考部分 中找到使用生成的 protocol buffer 代碼的完整參考,你可以在 協議緩沖區編碼 中找到更多關於如何對 protocol buffer messages 進行編碼的信息。
為什么不使用 XML?
對於序列化結構數據,protocol buffers 比 XML 更具優勢。Protocol buffers:
- 更簡單
- 小 3 ~ 10 倍
- 快 20 ~ 100 倍
- 更加清晰明確
- 自動生成更易於以編程方式使用的數據訪問類
例如,假設你想要為具有姓名和電子郵件的人建模。在XML中,你需要:
<person>
<name>John Doe</name>
<email>jdoe@example.com</email>
</person>
而相對應的 protocol buffer message(參見 protocol buffer 文本格式)是:
# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
name: "John Doe"
email: "jdoe@example.com"
}
當此消息被編碼為 protocol buffer 二進制格式 時(上面的文本格式只是為了調試和編輯的方便而用人類可讀的形式表示),它可能是 28 個字節長,需要大約 100-200 納秒來解析。如果刪除空格,XML版本至少為 69 個字節,並且需要大約 5,000-10,000 納秒才能解析。
此外,比起 XML,操作 protocol buffer 更為容易:
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
而使用 XML,你必須執行以下操作:
cout << "Name: "
<< person.getElementsByTagName("name")->item(0)->innerText()
<< endl;
cout << "E-mail: "
<< person.getElementsByTagName("email")->item(0)->innerText()
<< endl;
但是,protocol buffers 並不總是比 XML 更好的解決方案 - 例如,protocol buffers 不是使用標記(例如 HTML)對基於文本的文檔建模的好方法,因為你無法輕松地將結構與文本交錯。此外,XML 是人類可讀的和人類可編輯的;protocol buffers,至少它們的原生格式,並不具有這樣的特點。XML 在某種程度上也是自我描述的。只有擁有 message 定義(.proto文件)時,protocol buffer 才有意義。
介紹 proto3
我們最新的版本3 release ,它引入了新的語言版本 - Protocol Buffers 語言版本3(又稱 proto3),並且添加了現有語言版本(又稱 proto2)的一些新功能。Proto3 簡化了 Protocol Buffers 語言,既易於使用,又可以在更廣泛的編程語言中使用:這個版本允許你使用 Java,C ++,Python,Java Lite,Ruby,JavaScript,Objective-C 和 C# 生成 protocol buffer 代碼。此外,你可以使用最新的 Go protoc 插件為 Go 生成 proto3 代碼,該插件可從 github 庫 golang/protobuf 獲得。更多語言正在籌備中。
請注意,兩種語言版本的 API 不完全兼容。為避免給現有用戶帶來不便,我們將繼續在新版本的 protocol buffers 中支持以前的語言版本。
你可以在 發行說明 中看到與當前默認版本的主要差異,並在 Proto3 語法指引 中了解proto3 語法)。proto3 的完整文檔即將推出!
(如果名稱 proto2 和 proto3 看起來有點令人困惑,那是因為當我們最初開源 protocol buffers 時,它實際上是 Google 的第二個語言版本 - 也稱為 proto2。這也是為什么我們的開源版本從 v2.0.0 開始)。
一點點歷史
Protocol buffers 最初是在 Google 開發的,用於處理索引服務器請求/響應協議。在 protocol buffer 之前,有一種請求和響應的格式,它手動進行編組/解組,並支持許多版本的協議。這導致了一些非常丑陋的代碼,例如:
if (version == 3) {
...
} else if (version > 4) {
if (version == 5) {
...
}
...
}
明確格式化的協議也使新協議版本的推出變得復雜,因為開發人員必須確保請求的發起者和處理請求的實際服務器之間的所有服務器都能理解新協議,然后才能切換開關以開始使用新協議。
協議緩沖區旨在解決這些問題:
- 可以輕松引入新字段,中間服務器不需要檢查數據,可以簡單地解析它並傳遞數據而無需了解所有字段。
- 格式更具自我描述性,可以用各種語言處理(C ++,Java 等)
但是,用戶仍然需要手寫自己的解析代碼。
隨着系統的發展,它獲得了許多其他功能和用途:
- 自動生成的序列化和反序列化代碼避免了手動解析的需要。
- 除了用於短期 RPC(遠程過程調用)請求之外,人們還開始使用 protocol buffers 作為一種方便的自描述格式,用於持久存儲數據(例如在 Bigtable 中)。
- 服務器 RPC 接口開始被聲明為協議文件的一部分,protocol 編譯器生成存根類,用戶可以使用服務器接口的實際實現來覆蓋這些類。
Protocol buffers 現在是 Google 的數據通用語言 - 在撰寫本文時,Google 代碼樹中有 12183 個 .proto 文件,其中一共定義了 48162 種不同的 message 類型。它們既可用於 RPC 系統,也可用於各種存儲系統中的數據持久存儲。
語法指引(proto2)
本指南介紹如何使用 protocol buffer 語言來構造 protocol buffer 數據,包括 .proto 文件語法以及如何從 .proto 文件生成數據訪問類。它涵蓋了 protocol buffer 語言的 proto2 版本:有關較新的 proto3 語法的信息,請參閱 Proto3 語法指引。
這是一個參考指南,有關使用本文檔中描述的許多功能的分步示例,請參閱各種語言對應的具體 教程。
定義一個 Message 類型
首先讓我們看一個非常簡單的例子。假設你要定義一個搜索請求的 message 格式,其中每個搜索請求都有一個查詢字符串,你感興趣的特定結果頁數(第幾頁)以及每頁的結果數。下面就是定義這個請求的 .proto 文件:
message SearchRequest {
required string query = 1; // 查詢字符串
optional int32 page_number = 2; // 第幾頁
optional int32 result_per_page = 3; // 每頁的結果數
}
SearchRequest message 定義指定了三個字段(名稱/值對),每個字段對應着要包含在 message 中的數據,每個字段都有一個名稱和類型。
指定字段類型
在上面的示例中,所有字段都是 標量類型:兩個整數(page_number 和 result_per_page)和一個字符串(query)。但是,你還可以為字段指定復合類型,包括 枚舉 和其它的 message 類型。
分配字段編號
如你所見,message 定義中的每個字段都有唯一編號。這些數字以 message 二進制格式 標識你的字段,並且一旦你的 message 被使用,這些編號就無法再更改。請注意,1 到 15 范圍內的字段編號需要一個字節進行編碼,編碼結果將同時包含編號和類型(你可以在 Protocol Buffer 編碼 中找到更多相關信息)。16 到 2047 范圍內的字段編號占用兩個字節。因此,你應該為非常頻繁出現的 message 元素保留字段編號 1 到 15。請記住為將來可能添加的常用元素預留出一些空間。
你可以指定的最小字段數為 1,最大字段數為 2的29次方 - 1 或 536,870,911。你也不能使用 19000 到 19999 范圍內的數字(FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber),因為它們是為 Protocol Buffers 的實現保留的 - 如果你使用這些保留數字之一,protocol buffer 編譯器會抱怨你的 .proto。同樣,你也不能使用任何以前定義的 保留 字段編號。
住:不能使用任何以前定義的保留字段編號” 指的是使用 reserved 關鍵字聲明的保留字段。
指定字段規則
你指定的 message 字段可以是下面幾種情況之一:
- required: 格式良好的 message 必須包含該字段一次。
- optional: 格式良好的 message 可以包含該字段零次或一次(不超過一次)。
- repeated: 該字段可以在格式良好的消息中重復任意多次(包括零)。其中重復值的順序會被保留。
由於一些歷史原因,標量數字類型的 repeated 字段不能盡可能高效地編碼。新代碼應使用特殊選項 [packed = true] 來獲得更高效的編碼。例如:
repeated int32 samples = 4 [packed=true];
你可以在 Protocol Buffer 編碼 中找到更多有關 packed 編碼的信息。
對 required 的使用永遠都應該非常小心。如果你希望在某個時刻停止寫入或發送 required 字段,則將字段更改為可選字段將會有問題 - 舊讀者會認為沒有此字段的郵件不完整,可能會無意中拒絕或刪除它們。你應該考慮為 buffers 編寫特定於應用程序的自定義驗證的例程。谷歌的一些工程師得出的結論是,使用 required 弊大於利;他們更喜歡只使用 optional 和 repeated。但是,這種觀點並未普及。
注:在 proto3 中已經為兼容性徹底拋棄 required。
添加更多 message 類型
可以在單個 .proto 文件中定義多種 message 類型。這在你需要定義多個相關 message 的時候會很有用 - 例如,如果要定義與搜索請求相應的搜索回復 message - SearchResponse message,則可以將其添加到相同的 .proto:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
message SearchResponse {
...
}
組合 messages 會導致膨脹雖然可以在單個 .proto 文件中定義多種 messages 類型(例如 message,enum 和 service),但是當在單個文件中定義了大量具有不同依賴關系的 messages 時,它也會導致依賴性膨脹。建議每個 .proto 文件包含盡可能少的 message 類型。
添加注釋
為你的 .proto 文件添加注釋,可以使用 C/C++ 語法風格的注釋 // 和 /* ... */ 。
/* 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.
}
Reserved 保留字段
如果你通過完全刪除字段或將其注釋掉來更新 message 類型,則未來一些用戶在做他們的修改或更新時就可能會再次使用這些字段編號。如果以后加載相同 .proto 的舊版本,這可能會導致一些嚴重問題,包括數據損壞,隱私錯誤等。確保不會發生這種情況的一種方法是指定已刪除字段的字段編號為 “保留” 狀態。如果將來的任何用戶嘗試使用這些字段標識符,protocol buffer 編譯器將會抱怨。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注:不能在同一 "reserved" 語句中將字段名稱和字段編號混合在一起指定。
你的 .proto 文件將生成什么?
當你在 .proto 上運行 protocol buffer 編譯器時,編譯器將會生成所需語言的代碼,這些代碼可以操作文件中描述的 message 類型,包括獲取和設置字段值、將 message 序列化為輸出流、以及從輸入流中解析出 message。
- 對於 C++,編譯器從每個 .proto 生成一個 .h 和 .cc 文件,其中包含文件中描述的每種 message 類型對應的類。
- 對於 Java,編譯器為每個 message 類型生成一個 .java 文件(類),以及用於創建 message 類實例的特殊 Builder 類。
- Python 有點不同 - Python 編譯器生成一個模塊,其中包含 .proto 中每種 message 類型的靜態描述符,然后與元類一起使用以創建必要的 Python 數據訪問類。
- 對於 Go,編譯器會生成一個 .pb.go 文件,其中包含對應每種 message 類型的類型。
你可以按照所選語言的教程了解更多有關各種語言使用 API 的信息。有關更多 API 詳細信息,請參閱相關的 API 參考。
標量值類型
標量 message 字段可以具有以下幾種類型之一 - 該表顯示 .proto 文件中指定的類型,以及自動生成的類中的相應類型:
| .proto | Notes | C++ | Java | Python | Go |
|---|---|---|---|---|---|
| double | double | double | float | *float64 | |
| float | float | float | float | *float32 | |
| int32 | 使用可變長度編碼。編碼負數的效率低 - 如果你的字段可能有負值,請改用 sint32 | int32 | int | int | *int32 |
| int64 | 使用可變長度編碼。編碼負數的效率低 - 如果你的字段可能有負值,請改用 sint64 | int64 | long | int/long[3] | *int64 |
| uint32 | 使用可變長度編碼 | uint32 | int[1] | int/long[3] | *uint32 |
| uint64 | 使用可變長度編碼 | uint64 | long[1] | int/long[3] | *uint64 |
| sint32 | 使用可變長度編碼。有符號的 int 值。這些比常規 int32 對負數能更有效地編碼 | int32 | int | int | *int32 |
| sint64 | 使用可變長度編碼。有符號的 int 值。這些比常規 int64 對負數能更有效地編碼 | int64 | long | int/long[3] | *int64 |
| fixed32 | 總是四個字節。如果值通常大於 228,則比 uint32 更有效。 | uint32 | int[1] | int/long[3] | *uint32 |
| fixed64 | 總是八個字節。如果值通常大於 256,則比 uint64 更有效。 | uint64 | long[1] | int/long[3] | *uint64 |
| sfixed32 | 總是四個字節 | int32 | int | int | *int32 |
| sfixed64 | 總是八個字節 | int64 | long | int/long[3] | *int64 |
| bool | bool | boolean | bool | *bool | |
| string | 字符串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本 | string | String | str/unicode[4] | *string |
| bytes | 可以包含任意字節序列 | string | ByteString | str | []byte |
在 Protocol Buffer 編碼 中你可以找到有關序列化 message 時這些類型如何被編碼的詳細信息。
[1] 在 Java 中,無符號的 32 位和 64 位整數使用它們對應的帶符號表示,第一個 bit 位只是簡單的存儲在符號位中。
[2] 在所有情況下,設置字段的值將執行類型檢查以確保其有效。
[3] 64 位或無符號 32 位整數在解碼時始終表示為 long,但如果在設置字段時給出 int,則可以為int。在所有情況下,該值必須適合設置時的類型。見 [2]。
[4] Python 字符串在解碼時表示為 unicode,但如果給出了 ASCII 字符串,則可以是 str(這條可能會發生變化)。
Optional 可選字段和默認值
如上所述,message 描述中的元素可以標記為可選 optional。格式良好的 message 可能包含也可能不包含被聲明為可選的元素。解析 message 時,如果 message 不包含 optional 元素,則解析對象中的相應字段將設置為該字段的默認值。可以將默認值指定為 message 描述的一部分。例如,假設你要為 SearchRequest 的 result_per_page 字段提供默認值10。
optional int32 result_per_page = 3 [default = 10];
如果未為 optional 元素指定默認值,則使用特定於類型的默認值:對於字符串,默認值為空字符串。對於 bool,默認值為 false。對於數字類型,默認值為零。對於枚舉,默認值是枚舉類型定義中列出的第一個值。這意味着在將值添加到枚舉值列表的開頭時必須小心。有關如何安全的更改定義的指導,請參閱 更新 Message 類型 部分(見下面的 更新 message 類型)。
枚舉 Enumerations
在定義 message 類型時,你可能希望其中一個字段只有一個預定義的值列表。例如,假設你要為每個 SearchRequest 添加語料庫字段,其中語料庫可以是 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或 VIDEO。你可以通過向 message 定義添加枚舉來簡單地執行此操作 - 具有枚舉類型的字段只能將一組指定的常量作為其值(如果你嘗試提供不同的值,則解析器會將其視為一個未知的領域)。在下面的例子中,我們添加了一個名為 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 選項設置為true,否則 protocol 編譯器將在找到別名時生成錯誤消息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // 取消此行注釋將導致 Google 內部的編譯錯誤和外部的警告消息
}
枚舉器常量必須在 32 位整數范圍內。由於 enum 值在線上使用 varint encoding ,負值效率低,因此不推薦使用。你可以在 message 中定義 enums,如上例所示的那樣。或者將其定義在 message 外部 - 這樣這些 enum 就可以在 .proto 文件中的任何 message 定義中重用。你還可以使用一個 message 中聲明的 enum 類型作為不同 message 中字段的類型,使用語法 MessageType.EnumType來實現。
當你在使用 enum 的 .proto 上運行 protocol buffer 編譯器時,生成的代碼將具有相應的用於 Java 或 C++ 的 enum,或者用於創建集合的 Python 的特殊 EnumDescriptor 類。運行時生成的類中具有整數值的符號常量。
有關如何在應用程序中使用 enums 的更多信息,請參閱相關語言的 代碼生成指南
保留值
如果你通過完全刪除枚舉條目或將其注釋掉來更新枚舉類型,則未來用戶可能在對 message 做出自己的修改或更新時重復使用這些數值。如果以后加載相同 .proto 的舊版本,這可能會導致嚴重問題,包括數據損壞,隱私錯誤等。確保不會發生這種情況的一種方法是指定已刪除字段的字段編號為 “保留” 狀態。如果將來的任何用戶嘗試使用這些字段標識符,protocol buffer 編譯器將會抱怨。你可以使用 max 關鍵字指定保留的數值范圍一直到最大值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
注:你不能在同一 "reserved" 語句中將字段名稱和字段編號混合在一起指定。
使用其他 Message 類型
你可以使用其他 message 類型作為字段類型。例如,假設你希望在每個 SearchResponse 消息中包含 Result message - 為此,你可以在同一 .proto 中定義 Result message 類型,然后在SearchResponse 中指定 Result 類型的字段:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
導入定義 Importing Definitions
在上面的示例中,Result message 類型在與 SearchResponse 相同的文件中定義 - 如果要用作字段類型的 message 類型已在另一個 .proto 文件中定義,該怎么辦?
你可以通過導入來使用其他 .proto 文件中的定義。要導入另一個 .proto 的定義,可以在文件頂部添加一個 import 語句:
import "myproject/other_protos.proto";
默認情況下,你只能使用直接導入的 .proto 文件中的定義。但是,有時你可能需要將 .proto 文件移動到新位置。現在,你可以在舊位置放置一個虛擬 .proto 文件,以使用 import public 概念將所有導入轉發到新位置,而不是直接移動 .proto 文件並在一次更改中更新所有調用點。導入包含 import public 語句的 proto 的任何人都可以傳遞依賴導入公共依賴項。例如:
// 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";
// 你可以使用 old.proto 和 new.proto 中的定義,但無法使用 other.proto
使用命令 -I/--proto_path 讓 protocol 編譯器在指定的一組目錄中搜索要導入的文件。如果沒有給出這個命令選項,它將查找調用編譯器所在的目錄。通常,你應將 --proto_path 設置為項目的根目錄,並對所有導入使用完全限定名稱。
使用 proto3 Message 類型
可以導入 proto3 message 類型並在 proto2 message 中使用它們,反之亦然。但是,proto2 枚舉不能用於 proto3 語法。
嵌套類型 Nested Types
你可以在其他 message 類型中定義和使用 message 類型,如下例所示 - 此處結果消息在SearchResponse 消息中定義:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果要在其父消息類型之外重用此消息類型,請將其稱為 Parent.Type:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
你可以根據需要深入的嵌套消息:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
Groups
請注意,此功能已棄用,在創建新消息類型時不應使用 - 請改用嵌套消息類型。
Groups 是在 message 定義中嵌套信息的另一種方法。例如,指定包含許多結果的SearchResponse 的另一種方法如下:
message SearchResponse {
repeated group Result = 1 {
required string url = 2;
optional string title = 3;
repeated string snippets = 4;
}
}
group 只是將嵌套 message 類型和字段組合到單個聲明中。在你的代碼中,你可以將此消息視為具有名為 result 的 Result 類型字段(前一名稱轉換為小寫,以便它不與前者沖突)。因此,此示例完全等同於上面的 SearchResponse,但 message 具有不同的編碼結果。
注:此功能已棄用,這里只為盡可能保留原文內容。
更新 message 類型
如果現有的 message 類型不再滿足你的所有需求 - 例如,你希望 message 格式具有額外的字段 - 但你仍然希望使用舊格式創建代碼,請不要擔心!在不破壞任何現有代碼的情況下更新 message 類型非常簡單。請記住以下規則:
- 請勿更改任何現有字段的字段編號。
- 你添加的任何新字段都應該是
optional或repeated。這意味着使用“舊”消息格式的代碼序列化的任何消息都可以由新生成的代碼進行解析,因為它們不會缺少任何required元素。你應該為這些元素設置合理的默認值,以便新代碼可以正確地與舊代碼生成的 message 進行交互。同樣,你的新代碼創建的 message 可以由舊代碼解析:舊的二進制文件在解析時只是忽略新字段。但是未丟棄這個新字段(未知字段),如果稍后序列化消息,則將新字段(未知字段)與其一起序列化 - 因此,如果將消息傳遞給新代碼,則新字段仍然可用。 - 只要在更新的 message 類型中不再使用字段編號,就可以刪除非必填字段。你可能希望重命名該字段,可能添加前綴 "OBSOLETE_",或者將字段編號保留(Reserved),以便將來你的
.proto的用戶不會不小心重用這個編號。 - 只要類型和編號保持不變,非必填字段就可以轉換為擴展 extensions,反之亦然。
int32,uint32,int64,uint64和bool都是兼容的 - 這意味着你可以將字段從這些類型更改為另一種類型,而不會破壞向前或向后兼容性。如果從中解析出一個不符合相應類型的數字,你將獲得與在 C++ 中將該數字轉換為該類型時相同的效果(例如,如果將 64 位數字作為 int32 讀取,它將被截斷為 32 位)。sint32和sint64彼此兼容,但與其他整數類型不兼容。- 只要字節是有效的 UTF-8,
string和bytes就是兼容的。 - 如果字節包含 message 的編碼版本,則嵌入 message 與
bytes兼容。 fixed32與sfixed32兼容,fixed64與sfixed64兼容。optional與repeated兼容。給定重復字段的序列化數據作為輸入,期望該字段為optional的客戶端將采用最后一個輸入值(如果它是基本類型字段)或合並所有輸入元素(如果它是 message 類型字段)。- 更改默認值通常是正常的,只要你記住永遠不會通過網絡發送默認值。因此,如果程序接收到未設置特定字段的消息,則程序將看到該程序的協議版本中定義的默認值。它不會看到發件人代碼中定義的默認值。
enum與int32,uint32,int64和uint64兼容(注意,如果它們不適合,值將被截斷),但要注意 message 反序列化時客戶端代碼對待它們將有所不同。值得注意的是,當 message 被反序列化時,將丟棄無法識別的enum值,這使得字段的has..訪問器返回 false 並且其 getter 返回enum定義中列出的第一個值,或者如果指定了一個默認值則返回默認值。在 repeated 枚舉字段的情況下,任何無法識別的值都將從列表中刪除。但是,整數字段將始終保留其值。因此,在有可能接收超出范圍的枚舉值時,對整數升級為enum這一操作需要非常小心。- 在當前的 Java 和 C++ 實現中,當刪除無法識別的
enum值時,它們與其他未知字段一起存儲。請注意,如果此數據被序列化,然后由識別這些值的客戶端重新解析,則會導致奇怪的行為。在 optional 可選字段的情況下,即使在反序列化原始 message 之后寫入新值,舊值仍然可以被客戶端識別。在 repeated 字段的情況下,舊值將出現在任何已識別和新添加的值之后,這意味着順序將不被保留。 - 將單個
optional值更改為 newoneof的成員是安全且二進制兼容的。如果你確定沒有代碼一次設置多個,則將多個optional字段移動到新的oneof中可能是安全的。但是將任何字段移動到現有的oneof是不安全的。
擴展 Extensions
通過擴展,你可以聲明 message 中的一系列字段編號用於第三方擴展。擴展名是那些未由原始 .proto 文件定義的字段的占位符。這允許通過使用這些字段編號來定義部分或全部字段從而將其它 .proto 文件定義的字段添加到當前 message 定義中。我們來看一個例子:
message Foo {
// ...
extensions 100 to 199;
}
這表示 Foo 中的字段數 [100,199] 的范圍是為擴展保留的。其他用戶現在可以使用指定范圍內的字段編號在他們自己的 .proto 文件中為 Foo 添加新字段,例如:
extend Foo {
optional int32 bar = 126;
}
這會將名為 bar 且編號為 126 的字段添加到 Foo 的原始定義中。
當用戶的 Foo 消息被編碼時,其格式與用戶在 Foo 中常規定義新字段的格式完全相同。但是,在應用程序代碼中訪問擴展字段的方式與訪問常規字段略有不同 - 生成的數據訪問代碼具有用於處理擴展的特殊訪問器。那么,舉個例子,下面就是如何在 C++ 中設置 bar 的值:
Foo foo;
foo.SetExtension(bar, 15);
類似地,Foo 類定義模板化訪問器 HasExtension(),ClearExtension(),GetExtension(),MutableExtension() 和 AddExtension()。它們都具有與正常字段生成的訪問器相匹配的語義。有關使用擴展的更多信息,請參閱所選語言的代碼生成參考。
請注意,擴展可以是任何字段類型,包括 message 類型,但不能是 oneofs 或 maps。
嵌套擴展
你可以在另一種 message 類型內部聲明擴展:
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
在這種情況下,訪問此擴展的 C++ 代碼為:
Foo foo;
foo.SetExtension(Baz::bar, 15);
換句話說,唯一的影響是 bar 是在 Baz 的范圍內定義。
注意: 這是一個常見的混淆源:在一個 message 類型中聲明嵌套的擴展塊並不意味着外部類型和擴展類型之間存在任何關系。特別是,上面的例子並不意味着 Baz 是 Foo 的任何子類。這意味着符號欄是在 Baz 范圍內聲明的;它僅僅只是一個靜態成員而已。
一種常見的模式是在擴展的字段類型范圍內定義擴展 - 例如,這里是 Baz 類型的 Foo 擴展,其中擴展名被定義為 Baz 的一部分:
message Baz {
extend Foo {
optional Baz foo_ext = 127;
}
...
}
注:實際上就是要對某個 message A 擴展一個字段 B(B 類型),那么可以將這條擴展語句寫在 message B 的定義里。
但是,並不是必須要在類型內才能定義該類型的擴展字段。你也可以這樣做:
message Baz {
...
}
// 該定義甚至可以移到另一個文件中
extend Foo {
optional Baz foo_baz_ext = 127;
}
實際上,這種語法可能是首選的,以避免混淆。如上所述,嵌套語法經常被不熟悉擴展的用戶誤認為是子類。
選擇擴展字段編號
確保兩個用戶不使用相同的字段編號向同一 message 類型添加擴展名非常重要 - 如果擴展名被意外解釋為錯誤類型,則可能導致數據損壞。你可能需要考慮為項目定義擴展編號的約定以防止這種情況發生。
如果你的編號約定可能涉及那些具有非常大字段編號的擴展,則可以使用 max 關鍵字指定擴展范圍至編號最大值:
message Foo {
extensions 1000 to max;
}
最大值為 2的29次方 - 1,或者 536,870,911。
與一般選擇字段編號時一樣,你的編號約定還需要避免 19000 到 19999 的字段編號(FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber),因為它們是為 Protocol Buffers 實現保留的。你可以定義包含此范圍的擴展名范圍,但 protocol 編譯器不允許你使用這些編號定義實際擴展名。
Oneof
如果你的 message 包含許多可選字段,並且最多只能同時設置其中一個字段,則可以使用 oneof 功能強制執行此行為並節省內存。
Oneof 字段類似於可選字段,除了 oneof 共享內存中的所有字段,並且最多只能同時設置一個字段。設置 oneof 的任何成員會自動清除所有其他成員。你可以使用特殊的 case() 或 WhichOneof() 方法檢查 oneof 字段中當前是哪個值(如果有)被設置,具體方法取決於你選擇的語言。
使用 Oneof
要在 .proto 中定義 oneof,請使用 oneof 關鍵字,后跟你的 oneof 名稱,在本例中為 test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后,將 oneof 字段添加到 oneof 定義中。你可以添加任何類型的字段,但不能使用 required,optional 或 repeated 關鍵字。如果需要向 oneof 添加重復字段,可以使用包含重復字段的 message。
在生成的代碼中,oneof 字段與常規 optional 方法具有相同的 getter 和 setter。你還可以使用特殊方法檢查 oneof 中的值(如果有)。你可以在相關的 API 參考中找到有關所選語言的 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 不支持擴展
- oneof 不能使用 repeated
- 反射 API 適用於 oneof 字段
- 如果你使用的是 C++,請確保你的代碼不會導致內存崩潰。以下示例代碼將崩潰,因為已通過調用 set_name() 方法刪除了 sub_message。
```c
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
-
同樣在 C++中,如果你使用 Swap() 交換了兩條 oneofs 消息,則每條消息將以另一條消息的 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 的值返回 None/NOT_SET,則可能意味着 oneof 尚未設置或已設置為 oneof 的另一個字段。這種情況是無法區分的,因為無法知道未知字段是否是 oneof 成員。
#### 標簽重用問題
- **將 optional 可選字段移入或移出 oneof**:在序列化和解析 message 后,你可能會丟失一些信息(某些字段將被清除)。但是,你可以安全地將單個字段移動到新的 oneof 中,並且如果已知只有一個字段被設置,則可以移動多個字段。
- **刪除 oneof 字段並將其重新添加回去**:在序列化和解析 message 后,這可能會清除當前設置的 oneof 字段。
- **拆分或合並 oneof**:這與移動常規的 optional 字段有類似的問題。
## Maps
如果要在數據定義中創建關聯映射,protocol buffers 提供了一種方便快捷的語法:
```cpp
map<key_type, value_type> map_field = N;
...其中 key_type 可以是任何整數或字符串類型(任何標量類型除浮點類型和 bytes)。請注意,枚舉不是有效的 key_type。value_type 可以是除 map 之外的任何類型。
因此,舉個例子,如果要創建項目映射,其中每個 "Project" message 都與字符串鍵相關聯,則可以像下面這樣定義它:
map<string, Project> projects = 3;
生成的 map API 目前可用於所有 proto2 支持的語言。你可以在相關的 API 參考 中找到有關所選語言的 map API 的更多信息。
Maps 特性
- maps 不支持擴展
- maps 不能是 repeated、optional、required
- map 值的格式排序和 map 迭代排序未定義,因此你不能依賴於特定順序的 map 項
- 生成 .proto 的文本格式時,maps 按鍵排序。數字鍵按數字排序
- 當解析或合並時,如果有重復的 map 鍵,則使用最后看到的鍵。從文本格式解析 map 時,如果存在重復鍵,則解析可能會失敗
向后兼容性
map 語法等效於以下內容,因此不支持 map 的 protocol buffers 實現仍可處理你的數據:
message MapFieldEntry {
optional key_type key = 1;
optional value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持 maps 的 protocol buffers 實現都必須生成和接受上述定義所能接受的數據。
Packages
你可以將 optional 可選的包說明符添加到 .proto 文件,以防止 protocol message 類型之間的名稱沖突。
package foo.bar;
message Open { ... }
然后,你可以在定義 message 類型的字段時使用包說明符:
message Foo {
...
required foo.bar.Open open = 1;
...
}
package 影響生成的代碼的方式取決於你所選擇的語言:
- 在 C++ 中,生成的類包含在 C++ 命名空間中。例如,Open 將位於命名空間 foo::bar 中。
- 在 Java 中,除非在 .proto 文件中明確提供選項 java_package,否則該包將用作 Java 包
- 在 Python 中,package 指令被忽略,因為 Python 模塊是根據它們在文件系統中的位置進行組織的
請注意,即使 package 指令不直接影響生成的代碼,但是例如在 Python 中,仍然強烈建議指定 .proto 文件的包,否則可能導致描述符中的命名沖突並使 proto 對於其他語言不方便。
Packages 和名稱解析
protocol buffer 語言中的類型名稱解析與 C++ 類似:首先搜索最里面的范圍,然后搜索下一個范圍,依此類推,每個包被認為是其父包的 “內部”。一個領先的 '.'(例如 .foo.bar.Baz)意味着從最外層的范圍開始。
protocol buffer 編譯器通過解析導入的 .proto 文件來解析所有類型名稱。每種語言的代碼生成器都知道如何使用相應的語言類型,即使它具有不同的范圍和規則。
定義服務
如果要將 message 類型與 RPC(遠程過程調用)系統一起使用,則可以在 .proto 文件中定義 RPC 服務接口,protocol buffer 編譯器將使用你選擇的語言生成服務接口代碼和存根。因此,例如,如果要定義一個 RPC 服務,其中具有一個獲取 SearchRequest 並返回 SearchResponse 的方法,可以在 .proto 文件中定義它,如下所示:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
默認情況下,protocol 編譯器將生成一個名為 SearchService 的抽象接口和相應的 “存根” 實現。存根轉發所有對 RpcChannel 的調用,而 RpcChannel 又是一個抽象接口,你必須根據自己的 RPC 系統自行定義。例如,你可以實現一個 RpcChannel,它將 message 序列化並通過 HTTP 將其發送到服務器。換句話說,生成的存根提供了一個類型安全的接口,用於進行基於 protocol-buffer 的 RPC 調用,而不會將你鎖定到任何特定的 RPC 實現中。所以,在 C++ 中,你可能會得到這樣的代碼:
using google::protobuf;
protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;
void DoSearch() {
// You provide classes MyRpcChannel and MyRpcController, which implement
// the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
channel = new MyRpcChannel("somehost.example.com:1234");
controller = new MyRpcController;
// The protocol compiler generates the SearchService class based on the
// definition given above.
service = new SearchService::Stub(channel);
// Set up the request.
request.set_query("protocol buffers");
// Execute the RPC.
service->Search(controller, request, response, protobuf::NewCallback(&Done));
}
void Done() {
delete service;
delete channel;
delete controller;
}
所有服務類還實現了 Service 接口,它提供了一種在編譯時不知道方法名稱或其輸入和輸出類型的情況下來調用特定方法的方法。在服務器端,這可用於實現一個可以注冊服務的 RPC 服務器。
using google::protobuf;
class ExampleSearchService : public SearchService {
public:
void Search(protobuf::RpcController* controller,
const SearchRequest* request,
SearchResponse* response,
protobuf::Closure* done) {
if (request->query() == "google") {
response->add_result()->set_url("http://www.google.com");
} else if (request->query() == "protocol buffers") {
response->add_result()->set_url("http://protobuf.googlecode.com");
}
done->Run();
}
};
int main() {
// You provide class MyRpcServer. It does not have to implement any
// particular interface; this is just an example.
MyRpcServer server;
protobuf::Service* service = new ExampleSearchService;
server.ExportOnPort(1234, service);
server.Run();
delete service;
return 0;
}
如果你不想插入自己現有的 RPC 系統,現在可以使用 gRPC: 一個由谷歌開發的與語言和平台無關的開源 RPC 系統。gRPC 特別適用於 protocol buffers,並允許你使用特殊的 protocol buffers 編譯器插件直接從 .proto 文件生成相關的 RPC 代碼。但是,由於使用 proto2 和 proto3 生成的客戶端和服務器之間存在潛在的兼容性問題,我們建議你使用 proto3 來定義 gRPC 服務。你可以在 Proto3 語言指南 中找到有關 proto3 語法的更多信息。如果你確實希望將 proto2 與 gRPC 一起使用,則需要使用 3.0.0 或更高版本的 protocol buffers 編譯器和庫。
除了 gRPC 之外,還有許多正在進行的第三方項目,用於開發 Protocol Buffers 的 RPC 實現。有關我們了解的項目的鏈接列表,請參閱 第三方附加組件維基頁面。
選項 Options
.proto 文件中的各個聲明可以使用許多選項進行注釋。選項不會更改聲明的整體含義,但可能會影響在特定上下文中處理它的方式。可用選項的完整列表在 google/protobuf/descriptor.proto 中定義。
一些選項是文件級選項,這意味着它們應該在頂級范圍內編寫,而不是在任何消息,枚舉或服務定義中。一些選項是 message 消息級選項,這意味着它們應該寫在 message 消息定義中。一些選項是字段級選項,這意味着它們應該寫在字段定義中。選項也可以寫在枚舉類型、枚舉值、服務類型和服務方法上,但是,目前在這幾個項目上並沒有任何有用的選項。
以下是一些最常用的選項:
-
java_package(文件選項):要用於生成的 Java 類的包。如果 .proto 文件中沒有給出顯式的 java_package 選項,那么默認情況下將使用 proto 包(使用 .proto 文件中的 “package” 關鍵字指定)。但是,proto 包通常不能生成好的 Java 包,因為 proto 包不會以反向域名開頭。如果不生成Java 代碼,則此選項無效。
option java_package = "com.example.foo";
- java_outer_classname(文件選項):要生成的最外層 Java 類(以及文件名)的類名。如果 .proto 文件中沒有指定顯式的 java_outer_classname,則通過將 .proto 文件名轉換為 camel-case 來構造類名(因此 foo_bar.proto 變為 FooBar.java)。如果不生成 Java 代碼,則此選項無效。
```protobuf
option java_outer_classname = "Ponycopter";
-
optimize_for(文件選項):可以設置為
SPEED,CODE_SIZE 或 LITE_RUNTIME。這會以下列方式影響 C++和 Java 的代碼生成器(可能還有第三方生成器):
- SPEED(默認值):protocol buffer 編譯器將生成用於對 message 類型進行序列化,解析和執行其他常見操作的代碼。此代碼經過高度優化。
-
CODE_SIZE:protocol buffer 編譯器將生成最少的類,並依賴於基於反射的共享代碼來實現序列化,解析和各種其他操作。因此,生成的代碼將比使用 SPEED 小得多,但操作會更慢。類仍將實現與 SPEED 模式完全相同的公共 API。此模式在包含大量 .proto 文件的應用程序中最有用,並且不需要所有這些文件都非常快。
- LITE_RUNTIME:protocol buffer 編譯器將生成僅依賴於 “lite” 運行時庫(libprotobuf-lite 而不是libprotobuf)的類。精簡版運行時比整個庫小得多(大約小一個數量級),但省略了描述符和反射等特定功能。這對於在移動電話等受限平台上運行的應用程序尤其有用。編譯器仍將生成所有方法的快速實現,就像在 SPEED 模式下一樣。生成的類將僅實現每種語言的 MessageLite 接口,該接口僅提供完整 Message 接口的方法的子集。
option optimize_for = CODE_SIZE;
- `cc_generic_services`,`java_generic_services`,`py_generic_services`(文件選項):protocol buffer 編譯器應根據服務定義判斷是否生成 C++,Java 和 Python 抽象服務代碼。由於遺留原因,這些默認為 “true”。但是,從版本 2.3.0(2010年1月)開始,RPC 實現最好提供 [代碼生成器插件](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.compiler.plugin.pb) 生成更具體到每個系統的代碼,而不是依賴於 “抽象” 服務。
```protobuf
// This file relies on plugins to generate service code.
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
cc_enable_arenas(文件選項):為 C++ 生成的代碼啟用 arena allocation- message_set_wire_format(消息選項):如果設置為 true,則消息使用不同的二進制格式,旨在與 Google 內部使用的舊格式兼容,即 MessageSet。Google 以外的用戶可能永遠不需要使用此選項。必須嚴格按如下方式聲明消息:
message Foo {
option message_set_wire_format = true;
extensions 4 to max;
}
-
packed(字段選項):如果在基本數字類型的重復字段上設置為true,則一個更緊湊的編碼 被使用。使用此選項沒有任何缺點。但請注意,在版本 2.3.0 之前,在不期望的情況下接收打包數據的解析器將忽略它。因此,在不破壞兼容性的情況下,無法將現有字段更改為打包格式。在 2.3.0 及更高版本中,此更改是安全的,因為可打包字段的解析器將始終接受這兩種格式,但如果你必須使用舊的 protobuf 版本處理舊程序,請務必小心。repeated int32 samples = 4 [packed=true]; -
deprecated(field option):如果設置為true,表示該字段已棄用,新代碼不應使用該字段。在大多數語言中,這沒有實際效果。在 Java 中,這變成了@Deprecated注釋。將來,其他特定於語言的代碼生成器可能會在字段的訪問器上生成棄用注釋,這將導致在編譯嘗試使用該字段的代碼時發出警告。如果任何人都未使用該字段,並且你希望阻止新用戶使用該字段,請考慮使用reserved替換字段聲明。
optional int32 old_field = 6 [deprecated=true];
### 自定義選項
Protocol Buffers 甚至允許你定義和使用自己的選項。請注意,這是 **高級功能**,大多數人不需要。由於選項是由 `google/protobuf/descriptor.proto`(如 `FileOptions` 或 `FieldOptions`)中定義的消息定義的,因此定義你自己的選項只需要擴展這些消息。例如:
```protobuf
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
這里我們通過擴展 MessageOptions 定義了一個新的 message 級選項。然后,當我們使用該選項時,必須將選項名稱括在括號中以指示它是擴展名。我們現在可以在 C++ 中讀取 my_option 的值,如下所示:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
這里,MyMessage::descriptor()->options() 返回 MyMessage 的 MessageOptions protocol message。從中讀取自定義選項就像閱讀任何其他擴展。
同樣,在 Java 中我們會寫:
String value = MyProtoFile.MyMessage.getDescriptor().getOptions().getExtension(MyProtoFile.myOption);
在 Python 中它將是:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
.Extensions[my_proto_file_pb2.my_option]
可以在 Protocol Buffers 語言中為每種結構自定義選項。這是一個使用各種選項的示例:
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002;
}
extend google.protobuf.EnumOptions {
optional bool my_enum_option = 50003;
}
extend google.protobuf.EnumValueOptions {
optional uint32 my_enum_value_option = 50004;
}
extend google.protobuf.ServiceOptions {
optional MyEnum my_service_option = 50005;
}
extend google.protobuf.MethodOptions {
optional MyMessage my_method_option = 50006;
}
option (my_file_option) = "Hello world!";
message MyMessage {
option (my_message_option) = 1234;
optional int32 foo = 1 [(my_field_option) = 4.5];
optional string bar = 2;
}
enum MyEnum {
option (my_enum_option) = true;
FOO = 1 [(my_enum_value_option) = 321];
BAR = 2;
}
message RequestType {}
message ResponseType {}
service MyService {
option (my_service_option) = FOO;
rpc MyMethod(RequestType) returns(ResponseType) {
// Note: my_method_option has type MyMessage. We can set each field
// within it using a separate "option" line.
option (my_method_option).foo = 567;
option (my_method_option).bar = "Some string";
}
}
請注意,如果要在除定義它之外的包中使用自定義選項,則必須在選項名稱前加上包名稱,就像對類型名稱一樣。例如:
// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
option (foo.my_option) = "Hello world!";
}
最后一件事:由於自定義選項是擴展名,因此必須為其分配字段編號,就像任何其他字段或擴展名一樣。在上面的示例中,我們使用了 50000-99999 范圍內的字段編號。此范圍保留供個別組織內部使用,因此你可以自由使用此范圍內的數字用於內部應用程序。但是,如果你打算在公共應用程序中使用自定義選項,則務必確保你的字段編號是全局唯一的。要獲取全球唯一的字段編號,請發送請求以向 protobuf全球擴展注冊表 添加條目。通常你只需要一個擴展號。你可以通過將多個選項放在子消息中來實現一個擴展號聲明多個選項:
message FooOptions {
optional int32 opt1 = 1;
optional string opt2 = 2;
}
extend google.protobuf.FieldOptions {
optional FooOptions foo_options = 1234;
}
// usage:
message Bar {
optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
// alternative aggregate syntax (uses TextFormat):
optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
另請注意,每種選項類型(文件級別,消息級別,字段級別等)都有自己的數字空間,例如,你可以使用相同的數字聲明 FieldOptions 和 MessageOptions 的擴展名。
生成你的類
要生成 Java,Python 或 C++代碼,你需要使用 .proto 文件中定義的 message 類型,你需要在 .proto 上運行 protocol buffer 編譯器 protoc。如果尚未安裝編譯器,請 下載軟件包 並按照 README 文件中的說明進行操作。
Protocol 編譯器的調用如下:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
- IMPORT_PATH 指定在解析導入指令時查找 .proto 文件的目錄。如果省略,則使用當前目錄。可以通過多次傳遞 --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 生成的代碼 。
為了方便起見,如果 DST_DIR 以 .zip 或 .jar 結尾,編譯器會將輸出寫入到具有給定名稱的單個 ZIP 格式的存檔文件。.jar 輸出還將根據 Java JAR 規范的要求提供清單文件。請注意,如果輸出存檔已存在,則會被覆蓋;編譯器不夠智能,無法將文件添加到現有存檔中。
- 你必須提供一個或多個 .proto 文件作為輸入。可以一次指定多個 .proto 文件。雖然文件是相對於當前目錄命名的,但每個文件必須駐留在其中一個 IMPORT_PATH 中,以便編譯器可以確定其規范名稱。
語言指導(proto3)
翻譯自:https://developers.google.com/protocol-buffers/docs/proto3
本指導描述了如何使用 protocol buffer 語言來構建 protocol buffer 數據,包括 .proto 文件語法和如何基於該 .proto 文件生成數據訪問類。本文是涵蓋 protocol buffer 語言 proto3 版本的內容,若需要 proto2 版本的信息,請參考 Proto2 Language Guide 。
本文是語言指導——關於文中描述內容的分步示例,請參考所選編程語言的對應 tutorial
定義一個消息類型
我們先看一個簡單示例。比如說我們想定義個關於搜索請求的消息,每個搜索請求包含一個查詢字符串,一個特定的頁碼,和每頁的結果數量。下面是用於定義消息類型的 .proto 文件:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 文件的第一行指明了我們使用的是 proto3 語法:若不指定該行 protocol buffer 編譯器會認為是 proto2 。該行必須是文件的第一個非空或非注釋行。
SearchRequest消息定義了三個字段(名稱/值對),字段就是每個要包含在該類型消息中的部分數據。每個字段都具有名稱和類型 。
指定字段類型
上面的例子中,全部字段都是標量類型:兩個整型(page_number 和 result_per_page)和一個字符串型(query)。同樣,也可以指定復合類型的字段,包括枚舉型和其他消息類型。
分配字段編號
正如你所見,消息中定義的每個字段都有一個唯一編號。字段編號用於在消息二進制格式中標識字段,同時要求消息一旦使用字段編號就不應該改變。注意一點 1 到 15 的字段編號需要用 1 個字節來編碼,編碼同時包括字段編號和字段類型( 獲取更多信息請參考 Protocol Buffer Encoding )。16 到 2047 的字段變化使用 2 個字節。因此應將 1 到 15 的編號用在消息的常用字段上。注意應該為將來可能添加的常用字段預留字段編號。
最小的字段編號為 1,最大的為 2^29 - 1,或 536,870,911。注意不能使用 19000 到 19999 (FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的字段編號,因為是 protocol buffer 內部保留的——若在 .proto 文件中使用了這些預留的編號 protocol buffer 編譯器會發出警告。同樣也不能使用之前預留的字段編號。
指定字段規則
消息的字段可以是一下規則之一:
- singular , 格式良好的消息可以有 0 個或 1 個該字段(但不能多於 1 個)。這是 proto3 語法的默認字段規則。
- repeated ,格式良好的消息中該字段可以重復任意次數(包括 0 次)。重復值的順序將被保留。
在 proto3 中,標量數值類型的重復字段默認會使用 packed 壓縮編碼。
更多關於 packed 壓縮編碼的信息請參考 Protocol Buffer Encoding 。
增加更多消息類型
單個 .proto 文件中可以定義多個消息類型。這在定義相關聯的多個消息中很有用——例如要定義與搜索消息SearchRequest 相對應的回復消息 SearchResponse,則可以在同一個 .proto 文件中增加它的定義:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
增加注釋
使用 C/C++ 風格的 // 和 /* ... */ 語法在 .proto 文件添加注釋。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
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.
}
保留字段
在采取徹底刪除或注釋掉某個字段的方式來更新消息類型時,將來其他用戶再更新該消息類型時可能會重用這個字段編號。后面再加載該 .ptoto 的舊版本時會引發好多問題,例如數據損壞,隱私漏洞等。一個防止該問題發生的辦法是將刪除字段的編號(或字段名稱,字段名稱會導致在 JSON 序列化時產生問題)設置為保留項 reserved。protocol buffer 編譯器在用戶使用這些保留字段時會發出警告。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意,不能在同一條 reserved 語句中同時使用字段編號和名稱。
.proto 文件會生成什么?
當 protocol buffer 編譯器作用於一個 .proto 文件時,編輯器會生成基於所選編程語言的關於 .proto 文件中描述消息類型的相關代碼 ,包括對字段值的獲取和設置,序列化消息用於輸出流,和從輸入流解析消息。
- 對於 C++, 編輯器會針對於每個
.proto文件生成.h和.cc文件,對於每個消息類型會生成一個類。 - 對於 Java, 編譯器會生成一個
.java文件和每個消息類型對應的類,同時包含一個特定的Builder類用於構建消息實例。 - Python 有些不同 – Python 編譯器會對於 .proto 文件中每個消息類型生成一個帶有靜態描述符的模塊,以便於在運行時使用 metaclass 來創建必要的 Python 數據訪問類。
- 對於 Go, 編譯器會生成帶有每種消息類型的特定數據類型的定義在
.pb.go文件中。 - 對於 Ruby,編譯器會生成帶有消息類型的 Ruby 模塊的
.rb文件。 - 對於Objective-C,編輯器會針對於每個
.proto文件生成pbobjc.h和pbobjc.m.文件,對於每個消息類型會生成一個類。 - 對於 C#,編輯器會針對於每個
.proto文件生成.cs文件,對於每個消息類型會生成一個類。 - 對於 Dart,編輯器會針對於每個
.proto文件生成.pb.dart文件,對於每個消息類型會生成一個類。
可以參考所選編程語言的教程了解更多 API 的信息。更多 API 詳細信息,請參閱相關的 API reference 。
標量數據類型
消息標量字段可以是以下類型之一——下表列出了可以用在 .proto 文件中使用的類型,以及在生成代碼中的相關類型:
| .proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | Float | double | float | double | |
| float | float | float | float | float32 | Float | float | float | double | |
| int32 | 使用變長編碼。負數的編碼效率較低——若字段可能為負值,應使用 sint32 代替。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| int64 | 使用變長編碼。負數的編碼效率較低——若字段可能為負值,應使用 sint64 代替。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| uint32 | 使用變長編碼。 | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
| uint64 | 使用變長編碼。 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
| sint32 | 使用變長編碼。符號整型。負值的編碼效率高於常規的 int32 類型。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| sint64 | 使用變長編碼。符號整型。負值的編碼效率高於常規的 int64 類型。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| fixed32 | 定長 4 字節。若值常大於2^28 則會比 uint32 更高效。 | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
| fixed64 | 定長 8 字節。若值常大於2^56 則會比 uint64 更高效。 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
| sfixed32 | 定長 4 字節。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| sfixed64 | 定長 8 字節。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
| string | 包含 UTF-8 和 ASCII 編碼的字符串,長度不能超過 2^32 。 | string | String | str/unicode[4] | string | String (UTF-8) | string | string | String |
| bytes | 可包含任意的字節序列但長度不能超過 2^32 。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string | List
|
可以在 Protocol Buffer Encoding 中獲取更多關於消息序列化時類型編碼的相關信息。
[1] Java 中,無符號 32 位和 64 位整數使用它們對應的符號整數表示,第一個 bit 位僅是簡單地存儲在符號位中。
[2] 所有情況下,設置字段的值將執行類型檢查以確保其有效。
[3] 64 位或無符號 32 位整數在解碼時始終表示為 long,但如果在設置字段時給出 int,則可以為 int。在所有情況下,該值必須適合設置時的類型。見 [2]。
[4] Python 字符串在解碼時表示為 unicode,但如果給出了 ASCII 字符串,則可以是 str(這條可能會發生變化)。
[5] Integer 用於 64 位機器,string 用於 32 位機器。
默認值
當解析消息時,若消息編碼中沒有包含某個元素,則相應的會使用該字段的默認值。默認值依據類型而不同:
- 字符串類型,空字符串
- 字節類型,空字節
- 布爾類型,false
- 數值類型,0
- 枚舉類型,第一個枚舉元素
- 內嵌消息類型,依賴於所使用的編程語言。參考 generated code guide 獲取詳細信息。
對於可重復類型字段的默認值是空的( 通常是相應語言的一個空列表 )。
注意一下標量字段,在消息被解析后是不能區分字段是使用默認值(例如一個布爾型字段是否被設置為 false )賦值還是被設置為某個值的。例如你不能通過對布爾值等於 false 的判斷來執行一個不希望在默認情況下執行的行為。同時還要注意若一個標量字段設置為默認的值,那么是不會被序列化以用於傳輸的。
查看 generated code guide 來獲得更多關於編程語言生成代碼的內容。
枚舉
定義消息類型時,可能需要某字段值是一些預設值之一。例如當需要在 SearchRequest 消息類型中增加一個 corpus 字段, corpus 字段的值可以是 UNIVERSAL, WEB,IMAGES, LOCAL, NEWS, PRODUCTS 或 VIDEO。僅僅需要在消息類型中定義帶有預設值常量的 enum 類型即可完成上面的定義。
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 的常量作為第一個元素。原因是:
- 必須有一個 0 值,才可以作為數值類型的默認值。
- 0 值常量必須作為第一個元素,是為了與 proto2 的語義兼容就是第一個元素作為默認值。
將相同的枚舉值分配給不同的枚舉選項常量可以定義別名。要定義別名需要將 allow_alisa 選項設置為 true,否則 protocol 編譯器當發現別名定義時會報錯。
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 位整數的范圍內。因為枚舉值在傳輸時采用的是 varint 編碼,同時負值無效因而不建議使用。可以如上面例子所示,將枚舉定義在消息類型內,也可以將其定義外邊——這樣該枚舉可以用在 .proto 文件中定義的任意的消息類型中以便重用。還可以使用 MessageType.EnumType 語法將枚舉定義為消息字段的某一數據類型。
使用 protocol buffer 編譯器編譯 .proto 中的枚舉時,對於 Java 或 C 會生成相應的枚舉類型,對於 Python 會生成特定的 EnumDescriptor 類用於在運行時創建一組整型值符號常量即可。
反序列化時,未識別的枚舉值會被保留在消息內,但如何表示取決於編程語言。若語言支持開放枚舉類型允許范圍外的值時,這些未識別的枚舉值簡單的以底層整型進行存儲,就像 C++ 和 Go。若語言支持封閉枚舉類型例如 Java,一種情況是使用特殊的訪問器(譯注:accessors)來訪問底層的整型。無論哪種語言,序列化時的未識別枚舉值都會被保留在序列化結果中。
更多所選語言中關於枚舉的處理,請參考 generated code guide 。
保留值
在采取徹底刪除或注釋掉某個枚舉值的方式來更新枚舉類型時,將來其他用戶再更新該枚舉類型時可能會重用這個枚舉數值。后面再加載該 .ptoto 的舊版本時會引發好多問題,例如數據損壞,隱私漏洞等。一個防止該問題發生的辦法是將刪除的枚舉數值(或名稱,名稱會導致在 JSON 序列化時產生問題)設置為保留項 reserved。protocol buffer 編譯器在用戶使用這些特定數值時會發出警告。可以使用 max 關鍵字來指定保留值的范圍到最大可能值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
注意不能在 reserved 語句中混用字段名稱和數值。
使用其他消息類型
消息類型也可作為字段類型。例如,我們需要在 SearchResponse 消息中包含 Result 消息——想要做到這一點,可以將 Result 消息類型的定義放在同一個 .proto 文件中同時在 SearchResponse 消息中指定一個 Result 類型的字段:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
導入定義
前面的例子中,我們將 Result 消息定義在了與 SearchResponse 相同的文件中——但若我們需要作為字段類型使用的消息類型已經定義在其他的 .proto 文件中了呢?
可以通過導入操作來使用定義在其他 .proto 文件中的消息定義。在文件的頂部使用 import 語句完成導入其他 .proto 文件中的定義:
import "myproject/other_protos.proto";
默認情況下僅可以通過直接導入 .proto 文件來使用這些定義。然而有時會需要將 .proto 文件移動位置。可以通過在原始位置放置一個偽 .proto 文件使用 import public 概念來轉發對新位置的導入,而不是在發生一點更改時就去更新全部對舊文件的導入位置。任何導入包含 import public 語句的 proto 文件就會對其中的 import public 依賴產生傳遞依賴。例如:
// new.proto
// 全部定義移動到該文件
// old.proto
// 這是在客戶端中導入的偽文件
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// 可使用 old.proto 和 new.proto 中的定義,但不能使用 other.proto 中的定義
protocol 編譯器會使用命令行參數 -I/--proto_path 所指定的目錄集合中檢索需要導入的文件。若沒有指定,會在調用編譯器的目錄中檢索。通常應該將 --proto_path 設置為項目的根目錄同時在 import 語句中使用全限定名。
使用 proto2 類型
可以在 proto3 中導入 proto2 定義的消息類型,反之亦然。然而,proto2 中的枚舉不能直接用在 proto3 語法中(但導入到 proto2 中 proto3 定義的枚舉是可用的)。
嵌套類型
可以在一個消息類型中定義和使用另一個消息類型,如下例所示—— 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;
}
支持任意深度的嵌套:
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;
}
}
}
消息類型的更新
如果現有的消息類型不再滿足您的所有需求——例如,需要擴展一個字段——同時還要繼續使用已有代碼,別慌! 在不破壞任何現有代碼的情況下更新消息類型非常簡單。僅僅遵循如下規則即可:
- 不要修改任何已有字段的字段編號
- 若是添加新字段,舊代碼序列化的消息仍然可以被新代碼所解析。應該牢記新元素的默認值以便於新代碼與舊代碼序列化的消息進行交互。類似的,新代碼序列化的消息同樣可以被舊代碼解析:舊代碼解析時會簡單的略過新字段。參考未知字段獲取詳細信息。
- 字段可被移除,只要不再使用移除字段的字段編號即可。可能還會對字段進行重命名,或許是增加前綴
OBSOLETE_,或保留字段編號以保證后續不能重用該編號。 int32,uint32,int64,uint64, 和bool是完全兼容的——意味着可以從這些字段其中的一個更改為另一個而不破壞前后兼容性。若解析出來的數值與相應的類型不匹配,會采用與 C++ 一致的處理方案(例如,若將 64 位整數當做 32 位進行讀取,則會被轉換為 32 位)。sint32和sint64相互兼容但不與其他的整型兼容。stringandbytes在合法 UTF-8 字節前提下也是兼容的。- 嵌套消息與
bytes在 bytes 包含消息編碼版本的情況下也是兼容的。 fixed32與sfixed32兼容,fixed64與sfixed64兼容。enum與int32,uint32,int64,和uint64兼容(注意若值不匹配會被截斷)。但要注意當客戶端反序列化消息時會采用不同的處理方案:例如,未識別的 proto3 枚舉類型會被保存在消息中,但是當消息反序列化時如何表示是依賴於編程語言的。整型字段總是會保持其的值。- 將一個單獨值更改為新
oneof類型成員之一是安全和二進制兼容的。 若確定沒有代碼一次性設置多個值那么將多個字段移入一個新oneof類型也是可行的。將任何字段移入已存在的oneof類型是不安全的。
未知字段
未知字段是解析結構良好的 protocol buffer 已序列化數據中的未識別字段的表示方式。例如,當舊程序解析帶有新字段的數據時,這些新字段就會成為舊程序的未知字段。
本來,proto3 在解析消息時總是會丟棄未知字段,但在 3.5 版本中重新引入了對未知字段的保留機制以用來兼容 proto2 的行為。在 3.5 或更高版本中,未知字段在解析時會被保留同時也會包含在序列化結果中。
Any 類型
Any 類型允許我們將沒有 .proto 定義的消息作為內嵌類型來使用。一個 Any 包含一個類似 bytes 的任意序列化消息,以及一個 URL 來作為消息類型的全局唯一標識符。要使用 Any 類型,需要導入 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() 方法:
// 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.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
當前處理 Any 類型的運行庫正在開發中
若你已經熟悉了 proto2 語法,Any 類型的位於 extensions 部分。
Oneof
若一個含有多個字段的消息同時大多數情況下一次僅會設置一個字段,就可以使用 oneof 特性來強制該行為同時節約內存。
Oneof 字段除了全部字段位於 oneof 共享內存以及大多數情況下一次僅會設置一個字段外與常規字段類似。對任何oneof 成員的設置會自動清除其他成員。可以通過 case() 或 WhichOneof() 方法來檢測 oneof 中的哪個值被設置了,這個需要基於所選的編程語言。
使用 oneof
使用 oneof 關鍵字在 .proto 文件中定義 oneof,同時需要跟隨一個 oneof 的名字,就像本例中的 test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后將字段添加到 oneof 的定義中。可以增加任意類型的字段,但不能使用 repeated 字段。
在生成的代碼中,oneof 字段和常規字段一致具有 getters 和 setters 。同時也會獲得一個方法以用於檢測哪個值被設置了。更多所選編程語言中關於 oneof 的 API 可以參考 API reference 。
Oneof 特性
- 設置 oneof 的一個字段會清除其他字段。因此入設置了多次 oneof 字段,僅最后設置的字段生效。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // 會清理 name 字段
CHECK(!message.has_name());
- 若解析器在解析得到的數據時碰到了多個 oneof 的成員,最后一個碰到的是最終結果。
- oneof 不能是
repeated。 - 反射 API 可作用於 oneof 字段。
- 若將一個 oneof 字段設為了默認值(就像為 int32 類型設置了 0 ),那么 oneof 字段會被設置為 "case",同時在序列化編碼時使用。
- 若使用 C++ ,確認代碼不會造成內存崩潰。以下的示例代碼就會導致崩潰,因為
sub_message在調用set_name()時已經被刪除了。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // 會刪除 sub_message
sub_message->set_... // 此處會崩潰
- 同樣在 C++ 中,若
Swap()兩個 oneof 消息,那么消息會以另一個消息的 oneof 的情況:下例中,msg1會是sub_message1而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 的成員。
標記重用問題
- 移入或移出 oneof 字段: 消息序列化或解析后,可能會丟失一些信息(某些字段將被清除)。然而,可以安全地將單個字段移入新的 oneof 中,同樣若確定每次操作只有一個字段被設置則可以移動多個字段。
- 刪除一個 oneof 字段並又將其加回: 消息序列化和解析后,可能會清除當前設置的 oneof 字段。
- 拆分或合並 oneof:這與移動常規字段有類似的問題。
Map 映射表
若需要創建關聯映射表作為定義的數據的一部分,protocol buffers 提供了方便的快捷語法:
map<key_type, value_type> map_field = N;
key_type 處可以是整型或字符串類型(其實是除了 float 和 bytes 類型外任意的標量類型)。注意枚舉不是合法的 key_type 。value_type 是除了 map 外的任意類型。
例如,若需要創建每個項目與一個字符串 key 相關聯的映射表,可以采用下面的定義:
map<string, Project> projects = 3;
- 映射表字段不能為
repeated - 映射表的編碼和迭代順序是未定義的,因此不能依賴映射表元素的順序來操作。
- 當基於 .proto 生成文本格式時,映射表的元素基於 key 來排序。數值型的 key 基於數值排序。
- 當解析或合並時,若出現沖突的 key 以最后一個 key 為准。當從文本格式解析時,若 key 沖突則會解析失敗。
- 若僅僅指定了映射表中某個元素的 key 而沒有指定 value,當序列化時的行為是依賴於編程語言。在 C++,Java,和 Python 中使用類型的默認值來序列化,但在有些其他語言中可能不會序列化任何東西。
生成的映射表 API 當前可用於全部支持 proto3 的編程語言。在 API reference 中可以獲取更多關於映射表 API 的內容。
向后兼容問題
映射表語法與以下代碼是對等的,因此 protocol buffers 的實現即使不支持映射表也可以正常處理數據:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射表的 protocol buffers 實現都必須同時處理和接收上面代碼的數據定義。
包
可以在 .proto 文件中使用 package 指示符來避免 protocol 消息類型間的命名沖突。
package foo.bar;
message Open { ... }
這樣在定義消息的字段類型時就可以使用包指示符來完成:
message Foo {
...
foo.bar.Open open = 1;
...
}
包指示符的處理方式是基於編程語言的:
- C++ 中生成的類位於命名空間中。例如,
Open會位於命名空間foo::bar中。 - Java 中,使用 Java 的包,除非在 .proto 文件中使用
option java_pacakge做成明確的指定。 - Python 中,package 指示符被忽略,這是因為 Python 的模塊是基於文件系統的位置來組織的。
- Go 中,作為 Go 的包名來使用,除非在 .proto 文件中使用
option java_pacakge做成明確的指定。 - Ruby 中,生成的類包裹於 Ruby 的命名空間中,還要轉換為 Ruby 所需的大小寫風格(首字母大寫;若首字符不是字母,則使用
PB_前綴)。例如,Open會位於命名空間Foo::Bar中。 - C# 中作為命名空間來使用,同時需要轉換為 PascalCase 風格,除非在 .proto 使用
option csharp_namespace中明確的指定。例如,Open會位於命名空間Foo.Bar中。
包和名稱解析
protocol buffer 中類型名稱解析的工作機制類似於 C++ :先搜索最內層作用域,然后是次內層,以此類推,每個包被認為是其外部包的內層。前導點(例如,.foo.bar.Baz)表示從最外層作用域開始。
protocol buffer 編譯器會解析導入的 .proto 文件中的全部類型名稱。基於編程語言生成的代碼也知道如何去引用每種類型,即使編程語言有不同的作用域規則。
定義服務
若要在 RPC (Remote Procedure Call,遠程過程調用)系統中使用我們定義的消息類型,則可在 .proto 文件中定義這個 RPC 服務接口,同時 protocol buffer 編譯器會基於所選編程語言生成該服務接口代碼。例如,若需要定義一個含有可以接收 SearchRequest 消息並返回 SearchResponse 消息方法的 RPC 服務,可以在 .proto 文件中使用如下代碼定義:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
最直接使用 protocal buffer 的 RPC 系統是 gRPC :一款 Google 開源,語言和平台無關的 RPC 系統。gRPC 對 protocol buffer 的支持非常好同時允許使用特定的 protocol buffer 編譯器插件來基於 .proto 文件生成相關的代碼。
若不想使用 gRPC,同樣可以在自己的 RPC 實現上使用 protocol buffer。可以在 Proto2 Language Guide 處獲得更多關於這方面的信息。
同樣也有大量可用的第三方使用 protocol buffer 的項目。對於我們了解的相關項目列表,請參考 third-party add-ons wiki page 。
JSON 映射
Proto3 支持 JSON 的規范編碼,這使得系統間共享數據變得更加容易。下表中,將逐類型地描述這些編碼。
若 JSON 編碼中不存在某個值或者值為 null,當將其解析為 protocol buffer 時會解析為合適的默認值。若 procol buffer 中使用的是字段的默認值,則默認情況下 JSON 編碼會忽略該字段以便於節省空間。實現上應該提供一個選項以用來將具有默認值的字段生成在 JSON 編碼中。
| proto3 | JSON | JSON 示例 | 說明 |
|---|---|---|---|
| message | object | {"fooBar": v, "g": null,…} |
生成 JSON 對象。消息字段名映射為對象的 lowerCamelCase(譯著:小駝峰) 的 key。若指定了 json_name 選項,則使用該選項值作為 key。解析器同時支持 lowerCamelCase 名稱(或 json_name 指定名稱)和原始 proto 字段名稱。全部類型都支持 null 值,是當做對應類型的默認值來對待的。 |
| enum | string | "FOO_BAR" |
使用 proto 中指定的枚舉值的名稱。解析器同時接受枚舉名稱和整數值。 |
| map<K,V> | object | `{"k": v, …} | 所有的 key 被轉換為字符串類型。 |
| repeated V | array | [v, …] |
null 被解釋為空列表 []。 |
| bool | true, false | true, false |
|
| string | string | "Hello World!" |
|
| bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON 值是使用標准邊界 base64 編碼的字符串。不論標准或 URL 安全還是攜帶邊界與否的 base64 編碼都支持。 |
| int32, fixed32, uint32 | number | 1, -10, 0 |
JSON 值是 10 進制數值。數值或字符串都可以支持。 |
| int64, fixed64, uint64 | string | "1", "-10" |
JSON 值是 10 進制字符串。數值或字符串都支持。 |
| 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}。否則,會被轉換到一個對象中,同時會插入一個 "@type" 元素用以指明實際的類型。 |
| Timestamp | string | "1972-01-01T10:00:20.021Z" |
采用 RFC 3339 格式,其中生成的輸出總是 Z規范的,並使用 0、3、6 或 9 位小數。除 “Z” 以外的偏移量也可以。 |
| Duration | string | "1.000340012s", "1s" |
根據所需的精度,生成的輸出可能會包含 0、3、6 或 9 位小數,以 “s” 為后綴。只要滿足納秒精度和后綴 “s” 的要求,任何小數(包括沒有)都可以接受。 |
| Struct | object |
{ … } |
任意 JSON 對象。參見 struct.proto. |
| Wrapper types | various types | 2, "2", "foo", true,"true", null, 0, … |
包裝器使用與包裝的原始類型相同的 JSON 表示,但在數據轉換和傳輸期間允許並保留 null。 |
| FieldMask | string | "f.fooBar,h" |
參見field_mask.proto。 |
| ListValue | array | [foo, bar, …] |
|
| Value | value | Any JSON value | |
| NullValue | null | JSON null | |
| Empty | object | {} | 空 JSON 對象 |
JSON 選項
proto3 的 JSON 實現可以包含如下的選項:
- 省略使用默認值的字段:默認情況下,在 proto3 的 JSON 輸出中省略具有默認值的字段。該實現可以使用選項來覆蓋此行為,來在輸出中保留默認值字段。
- 忽略未知字段:默認情況下,proto3 的 JSON 解析器會拒絕未知字段,同時提供選項來指示在解析時忽略未知字段。
- 使用 proto 字段名稱代替 lowerCamelCase 名稱: 默認情況下,proto3 的 JSON 編碼會將字段名稱轉換為 lowerCamelCase(譯著:小駝峰)形式。該實現提供選項可以使用 proto 字段名代替。Proto3 的 JSON 解析器可同時接受 lowerCamelCase 形式 和 proto 字段名稱。
- 枚舉值使用整數而不是字符串表示: 在 JSON 編碼中枚舉值是使用枚舉值名稱的。提供了可以使用枚舉值數值形式來代替的選項。
選項
.proto 文件中的單個聲明可以被一組選項來設置。選項不是用來更改聲明的含義,但會影響在特定上下文下的處理方式。完整的選項列表定義在 google/protobuf/descriptor.proto 中。
有些選項是文件級的,意味着可以卸載頂級作用域,而不是在消息、枚舉、或服務的定義中。有些選項是消息級的,意味着需寫在消息的定義中。有些選項是字段級的,意味着需要寫在字段的定義內。選項還可以寫在枚舉類型,枚舉值,服務類型,和服務方法上;然而,目前還沒有任何可用於以上位置的選項。
下面是幾個最常用的選項:
java_package(文件選項):要用在生成 Java 代碼中的包。若沒有在 .proto 文件中對java_package選項做設置,則會使用 proto 作為默認包(在 .proto 文件中使用 "package" 關鍵字設置)。 然而,proto 包通常不是合適的 Java 包,因為 proto 包通常不以反續域名開始。若不生成 Java 代碼,則此選項無效。
option java_package = "com.example.foo";
- java_multiple_files (文件選項):導致將頂級消息、枚舉、和服務定義在包級,而不是在以 .proto 文件命名的外部類中。
option java_multiple_files = true;
- java_outer_classname(文件選項):想生成的最外層 Java 類(也就是文件名)。若沒有在 .proto 文件中明確指定
java_outer_classname選項,類名將由 .proto 文件名轉為 camel-case 來構造(因此foo_bar.proto會變為FooBar.java)。若不生成 Java 代碼,則此選項無效。
option java_outer_classname = "Ponycopter";
- optimize_for (文件選項): 可被設為
SPEED,CODE_SIZE,或LITE_RUNTIME。這會影響 C++ 和 Java 代碼生成器(可能包含第三方生成器) 的以下幾個方面: - SPEED (默認): protocol buffer 編譯器將生成用於序列化、解析和消息類型常用操作的代碼。生成的代碼是高度優化的。
- CODE_SIZE :protocol buffer 編譯器將生成最小化的類,並依賴於共享的、基於反射的代碼來實現序列化、解析和各種其他操作。因此,生成的代碼將比 SPEED 模式小的多,但操作將變慢。類仍將實現與 SPEED 模式相同的公共 API。這種模式在處理包含大量 .proto 文件同時不需要所有操作都要求速度的應用程序中最有用。
- LITE_RUNTIME :protocol buffer 編譯器將生成僅依賴於 “lite” 運行庫的類(libprotobuf-lite 而不是libprotobuf)。lite 運行時比完整的庫小得多(大約小一個數量級),但會忽略某些特性,比如描述符和反射。這對於在受限平台(如移動電話)上運行的應用程序尤其有用。編譯器仍然會像在 SPEED 模式下那樣生成所有方法的快速實現。生成的類將僅用每種語言實現 MessageLite 接口,該接口只提供
Message接口的一個子集。
option optimize_for = CODE_SIZE;
cc_enable_arenas(文件選項):為生成的 C++ 代碼啟用 arena allocation 。objc_class_prefix(文件選項): 設置當前 .proto 文件生成的 Objective-C 類和枚舉的前綴。沒有默認值。你應該使用 recommended by Apple 的 3-5 個大寫字母作為前綴。注意所有 2 個字母前綴都由 Apple 保留。deprecated(字段選項):若設置為true, 指示該字段已被廢棄,新代碼不應使用該字段。在大多數語言中,這沒有實際效果。在 Java 中,這變成了一個@Deprecated注釋。將來,其他語言的代碼生成器可能會在字段的訪問器上生成棄用注釋,這將導致在編譯試圖使用該字段的代碼時發出警告。如果任何人都不使用該字段,並且您希望阻止新用戶使用它,那么可以考慮使用保留語句替換字段聲明。
int32 old_field = 6 [deprecated=true];
自定義選項
protocol buffer 還允許使用自定義選項。大多數人都不需要此高級功能。若確認要使用自定義選項,請參閱 Proto2 Language Guide 了解詳細信息。注意使用 extensions 來創建自定義選項,只允許用於 proto3 中。
生成自定義類
若要生成操作 .proto 文件中定義的消息類型的 Java、Python、C++、Go、Ruby、Objective-C 或 C# 代碼,需要對 .proto 文件運行 protocol buffer 編譯器 protoc。若還沒有安裝編譯器,請 download the package 並依據 README 完成安裝。對於 Go ,還需要為編譯器安裝特定的代碼生成器插件:可使用 GitHub 上的 golang/protobuf 庫。
Protocol buffer 編譯器的調用方式如下:
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 --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATH為import指令檢索 .proto 文件的目錄。若未指定,使用當前目錄。多個導入目錄可以通過多次傳遞--proto_path選項實現;這些目錄會依順序檢索。-I=*IMPORT_PATH*可作為--proto_path的簡易格式使用。- 可以提供一個或多個輸出指令:
--cpp_out在DST_DIR目錄 生成 C++ 代碼。參閱 C++ generated code reference 獲取更多信息。--java_out在DST_DIR目錄 生成 Java 代碼。參閱 Java generated code reference 獲取更多信息。--python_out在DST_DIR目錄 生成 Python代碼。參閱 Python generated code reference 獲取更多信息。--go_out在DST_DIR目錄 生成 Go 代碼。參閱 Go generated code reference 獲取更多信息。--ruby_out在DST_DIR目錄 生成 Ruby 代碼。 coming soon!--objc_out在DST_DIR目錄 生成 Objective-C 代碼。參閱 Objective-C generated code reference 獲取更多信息。--csharp_out在DST_DIR目錄 生成 C# 代碼。參閱 C# generated code reference 獲取更多信息。--php_out在DST_DIR目錄 生成 PHP代碼。參閱 PHP generated code reference 獲取更多信息。
作為額外的便利,若 DST_DIR 以 .zip 或 .jar 結尾,編譯器將會寫入給定名稱的 ZIP 格式壓縮文件,.jar 還將根據 Java JAR 的要求提供一個 manifest 文件。請注意,若輸出文件已經存在,它將被覆蓋;編譯器還不夠智能,無法將文件添加到現有的存檔中。
- 必須提供一個或多個 .proto 文件作為輸入。可以一次指定多個 .proto 文件。雖然這些文件是相對於當前目錄命名的,但是每個文件必須駐留在
IMPORT_PATHs中,以便編譯器可以確定它的規范名稱。
