Protocol Buffers V3中文語(yǔ)法指南[翻譯]
本文是官方protocol buffers v3指南的翻譯。
本文翻譯自https://developers.google.com/protocol-buffers/docs/proto3。
定義一個(gè)消息類型
首先讓我們看一個(gè)非常簡(jiǎn)單的例子。假設(shè)你想要定義一個(gè)搜索請(qǐng)求消息格式,其中每個(gè)搜索請(qǐng)求都包含一個(gè)查詢?cè)~字符串、你感興趣的查詢結(jié)果所在的特定頁(yè)碼數(shù)和每一頁(yè)應(yīng)展示的結(jié)果數(shù)。
下面是用于定義這個(gè)消息類型的 .proto 文件。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 文件的第一行指定使用
proto3語(yǔ)法: 如果不這樣寫,protocol buffer編譯器將假定你使用proto2。這個(gè)聲明必須是文件的第一個(gè)非空非注釋行。 -
SearchRequest消息定義指定了三個(gè)字段(名稱/值對(duì)) ,每個(gè)字段表示希望包含在此類消息中的每一段數(shù)據(jù)。每個(gè)字段都有一個(gè)名稱和一個(gè)類型。
指定字段類型
在上面的示例中,所有字段都是標(biāo)量類型(scalar types): 兩個(gè)整數(shù)(page_number和 result_per_page)和一個(gè)字符串(query)。但是也可以為字段指定組合類型,包括枚舉和其他消息類型。
分配字段編號(hào)
如你所見(jiàn),消息定義中的每個(gè)字段都有一個(gè)唯一的編號(hào)。這些字段編號(hào)用來(lái)在消息二進(jìn)制格式中標(biāo)識(shí)字段,在消息類型使用后就不能再更改。注意,范圍1到15中的字段編號(hào)需要一個(gè)字節(jié)進(jìn)行編碼,包括字段編號(hào)和字段類型。范圍16到2047的字段編號(hào)采用兩個(gè)字節(jié)。因此,應(yīng)該為經(jīng)常使用的消息元素保留數(shù)字1到15的編號(hào)。切記為將來(lái)可能添加的經(jīng)常使用的元素留出一些編號(hào)。
你可以指定的最小字段數(shù)是1,最大的字段數(shù)是 229?1229?1 ,即536,870,911。你也不能使用19000到19999 (FieldDescriptor::kFirstReservedNumber 到FieldDescriptor::kLastReservedNumber)的編號(hào),它們是預(yù)留給Protocol Buffers協(xié)議實(shí)現(xiàn)的。如果你在你的.proto文件中使用了預(yù)留的編號(hào)Protocol Buffers編譯器就會(huì)報(bào)錯(cuò)。同樣,你也不能使用任何之前保留的字段編號(hào)。
指定字段規(guī)則
消息字段可以是下列字段之一:
-
singular: 格式正確的消息可以有這個(gè)字段的零個(gè)或一個(gè)(但不能多于一個(gè))。這是 proto3語(yǔ)法的默認(rèn)字段規(guī)則。 -
repeated: 該字段可以在格式正確的消息中重復(fù)任意次數(shù)(包括零次)。重復(fù)值的順序?qū)⒈槐A簟?/li>
在 proto3中,標(biāo)量數(shù)值類型的repeated字段默認(rèn)使用packed編碼。
你可以在 Protocol Buffer Encoding 中找到關(guān)于packed編碼的更多信息。
添加更多消息類型
可以在一個(gè).proto 文件中定義多個(gè)消息類型。如果你正在定義多個(gè)相關(guān)的消息,這是非常有用的——例如,如果想定義與 SearchRequest 消息類型對(duì)應(yīng)的應(yīng)答消息格式SearchResponse,你就可以將其添加到同一個(gè).proto文件中。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
添加注釋
要給你的.proto文件添加注釋,需要使用C/C++風(fēng)格的//和/* ... */語(yǔ)法。
/* SearchRequest 表示一個(gè)分頁(yè)查詢
* 其中有一些字段指示響應(yīng)中包含哪些結(jié)果 */
message SearchRequest {
string query = 1;
int32 page_number = 2; // 頁(yè)碼數(shù)
int32 result_per_page = 3; // 每頁(yè)返回的結(jié)果數(shù)
}
保留字段
如果你通過(guò)完全刪除字段或?qū)⑵渥⑨尩魜?lái)更新消息類型,那么未來(lái)的用戶在對(duì)該類型進(jìn)行自己的更新時(shí)可以重用字段號(hào)。如果其他人以后加載舊版本的相同.proto文件,這可能會(huì)導(dǎo)致嚴(yán)重的問(wèn)題,包括數(shù)據(jù)損壞,隱私漏洞等等。確保這種情況不會(huì)發(fā)生的一種方法是指定已刪除字段的字段編號(hào)(和/或名稱,這也可能導(dǎo)致 JSON 序列化問(wèn)題)是保留的(reserved)。如果將來(lái)有任何用戶嘗試使用這些字段標(biāo)識(shí)符,protocol buffer編譯器將發(fā)出提示。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意,不能在同一個(gè)reserved語(yǔ)句中混合字段名和字段編號(hào)。
從你的.proto文件生成了什么?
當(dāng)你使用 protocol buffer 編譯器來(lái)運(yùn)行.proto文件時(shí),編譯器用你選擇的語(yǔ)言生成你需要使用文件中描述的消息類型,包括獲取和設(shè)置字段值,將消息序列化為輸出流,以及從輸入流解析消息的代碼。
- 對(duì)C++來(lái)說(shuō),編譯器會(huì)為每個(gè)
.proto文件生成一個(gè).h文件和一個(gè).cc文件,.proto文件中的每一個(gè)消息有一個(gè)對(duì)應(yīng)的類。 - 對(duì)于 Java,編譯器生成一個(gè)
.java文件,每種消息類型都有一個(gè)類,還有一個(gè)特殊的Builder類用于創(chuàng)建消息類實(shí)例。 - 對(duì)于 Kotlin,除了 Java 生成的代碼之外,編譯器還生成一個(gè)每種消息類型的
.kt文件,包含一個(gè) DSL,可用于簡(jiǎn)化消息實(shí)例的創(chuàng)建。 -
Python 稍有不同ー Python 編譯器為
.proto文件中的每個(gè)消息類型生成一個(gè)帶靜態(tài)描述符的模塊,然后與 metaclass 一起使用,在運(yùn)行時(shí)創(chuàng)建必要的 Python 數(shù)據(jù)訪問(wèn)類。 - 對(duì)于 Go,編譯器為文件中的每種消息類型生成一個(gè)類型(type)到一個(gè)
.pb.go文件。 - 對(duì)于 Ruby,編譯器生成一個(gè)
.rb文件,其中包含一個(gè)包含消息類型的 Ruby 模塊。 - 對(duì)于 Objective-C,編譯器從每個(gè)
.proto文件生成一個(gè)pbobjc.h和pbobjc.m文件,.proto文件中描述的每種消息類型都有一個(gè)類。 - 對(duì)于 C# ,編譯器生從每個(gè)
.proto文件生成一個(gè).cs文件。.proto文件中描述的每種消息類型都有一個(gè)類。 - 對(duì)于 Dart,編譯器為文件中的每種消息類型生成一個(gè)
.pb.dart文件。
你可以通過(guò)學(xué)習(xí)所選語(yǔ)言的教程(proto3版本即將推出)了解更多關(guān)于使用每種語(yǔ)言的 API 的信息。有關(guān) API 的更多細(xì)節(jié),請(qǐng)參閱相關(guān)的 API reference(proto3版本也即將推出)。
標(biāo)量值類型
標(biāo)量消息字段可以具有以下類型之一——該表顯示了.proto文件,以及自動(dòng)生成類中的對(duì)應(yīng)類型(省略了Ruby、C#和Dart):
| .proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | PHP Type |
|---|---|---|---|---|---|---|
| double | double | double | float | float64 | float | |
| float | float | float | float | float32 | float | |
| int32 | 使用可變長(zhǎng)度編碼。編碼負(fù)數(shù)效率低下——如果你的字段可能有負(fù)值,則使用 sint32代替。 | int32 | int | int | int32 | integer |
| int64 | 使用可變長(zhǎng)度編碼。編碼負(fù)數(shù)效率低下——如果你的字段可能有負(fù)值,則使用 sint64代替。 | int64 | long | int/long[4] | int64 | integer/string[6] |
| uint32 | 使用變長(zhǎng)編碼。 | uint32 | int[2] | int/long[4] | uint32 | integer |
| uint64 | 使用變長(zhǎng)編碼。 | uint64 | long[2] | int/long[4] | uint64 | integer/string[6] |
| sint32 | 使用可變長(zhǎng)度編碼。帶符號(hào)的 int 值。這些編碼比普通的 int32更有效地編碼負(fù)數(shù)。 | int32 | int | int | int32 | integer |
| sint64 | 使用可變長(zhǎng)度編碼。帶符號(hào)的 int 值。這些編碼比普通的 int64更有效地編碼負(fù)數(shù)。 | int64 | long | int/long[4] | int64 | integer/string[6] |
| fixed32 | 總是四個(gè)字節(jié)。如果值經(jīng)常大于228,則比 uint32更有效率。 | uint32 | int[2] | int/long[4] | uint32 | integer |
| fixed64 | 總是8字節(jié)。如果值經(jīng)常大于256,則比 uint64更有效率。 | uint64 | integer/string[6] | |||
| sfixed32 | 總是四個(gè)字節(jié)。 | int32 | int | int | int32 | integer |
| sfixed64 | 總是八個(gè)字節(jié)。 | int64 | integer/string[6] | |||
| bool | bool | boolean | bool | bool | boolean | |
| string | 字符串必須始終包含 UTF-8編碼的或7位 ASCII 文本,且不能長(zhǎng)于232。 | string | String | str/unicode[5] | string | string |
| bytes | 可以包含任何不超過(guò)232字節(jié)的任意字節(jié)序列。 | string | ByteString | str (Python 2) bytes (Python 3) | []byte | string |
在使用 Protocol Buffer Encoding 對(duì)消息進(jìn)行序列化時(shí),可以了解有關(guān)這些類型如何編碼的更多信息。
[1] Kotlin 使用來(lái)自 Java 的相應(yīng)類型,甚至是無(wú)符號(hào)類型,以確?;旌?Java/Kotlin 代碼庫(kù)的兼容性。
[2] 在 Java 中,無(wú)符號(hào)的32位和64位整數(shù)使用它們的有符號(hào)對(duì)應(yīng)項(xiàng)來(lái)表示,最高位存儲(chǔ)在有符號(hào)位中。
[3] 在任何情況下,為字段設(shè)置值都將執(zhí)行類型檢查,以確保其有效。
[4] 64位或無(wú)符號(hào)的32位整數(shù)在解碼時(shí)總是表示為 long ,但如果在設(shè)置字段時(shí)給出 int,則可以表示為 int。在任何情況下,值必須與設(shè)置時(shí)表示的類型相匹配。見(jiàn)[2]。
[5] Python 字符串在解碼時(shí)表示為 unicode,但如果給出了 ASCII 字符串,則可以表示為 str (這可能會(huì)更改)。
[6] 整數(shù)用于64位機(jī)器,字符串用于32位機(jī)器。
默認(rèn)值
當(dāng)解析消息時(shí),如果編碼消息不包含特定的 singular 元素,則解析對(duì)象中的相應(yīng)字段將設(shè)置為該字段的默認(rèn)值。
- 對(duì)于字符串,默認(rèn)值為空字符串。
- 對(duì)于字節(jié),默認(rèn)值為空字節(jié)。
- 對(duì)于布爾值,默認(rèn)值為 false。
- 對(duì)于數(shù)值類型,默認(rèn)值為零。
- 對(duì)于枚舉,默認(rèn)值是第一個(gè)定義的枚舉值,該值必須為0。
- 對(duì)于消息字段,未設(shè)置該字段。其確切值與語(yǔ)言有關(guān)。詳細(xì)信息請(qǐng)參閱生成的代碼指南。
repeated 字段的默認(rèn)值為空(通常是適當(dāng)語(yǔ)言中的空列表)。
請(qǐng)注意,對(duì)于標(biāo)量消息字段,一旦消息被解析,就無(wú)法判斷字段是顯式設(shè)置為默認(rèn)值(例如,是否一個(gè)布爾值是被設(shè)置為 false)還是根本沒(méi)有設(shè)置: 在定義消息類型時(shí)應(yīng)該牢記這一點(diǎn)。例如,如果你不希望某個(gè)行為在默認(rèn)情況下也發(fā)生,那么就不要設(shè)置一個(gè)布爾值,該布爾值在設(shè)置為 false 時(shí)會(huì)開(kāi)啟某些行為。還要注意,如果將標(biāo)量消息字段設(shè)置為默認(rèn)值,則該值將不會(huì)在傳輸過(guò)程中序列化。
有關(guān)生成的代碼的默認(rèn)工作方式的更多詳細(xì)信息,請(qǐng)參閱所選語(yǔ)言的生成代碼指南。
枚舉
在定義消息類型時(shí),你可能希望其中一個(gè)字段只能是預(yù)定義的值列表中的一個(gè)值。例如,假設(shè)你想為每個(gè) SearchRequest 添加一個(gè)語(yǔ)料庫(kù)字段,其中語(yǔ)料庫(kù)可以是 UNIVERSAL、 WEB、 IMAGES、 LOCAL、 NEWS、 PRODUCTS 或 VIDEO。你可以通過(guò)在消息定義中添加一個(gè)枚舉,為每個(gè)可能的值添加一個(gè)常量來(lái)非常簡(jiǎn)單地完成這項(xiàng)工作。
在下面的例子中,我們添加了一個(gè)名為 Corpus 的enum,包含所有可能的值,以及一個(gè)類型為 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;
}
如你所見(jiàn),Corpus enum 的第一個(gè)常量映射為零: 每個(gè) enum 定義必須包含一個(gè)常量,該常量映射為零作為它的第一個(gè)元素。這是因?yàn)?
- 必須有一個(gè)零值,這樣我們就可以使用0作為數(shù)值默認(rèn)值。
- 零值必須是第一個(gè)元素,以便與 proto2語(yǔ)義兼容,其中第一個(gè)枚舉值總是默認(rèn)值。
你可以通過(guò)將相同的值分配給不同的枚舉常量來(lái)定義別名。為此,你需要將 allow _ alias 選項(xiàng)設(shè)置為 true,否則,當(dāng)發(fā)現(xiàn)別名時(shí),protocol 編譯器將生成錯(cuò)誤消息。
message MyMessage1 {
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
message MyMessage2 {
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
}
枚舉的常數(shù)必須在32位整數(shù)的范圍內(nèi)。由于枚舉值在傳輸時(shí)使用變長(zhǎng)編碼,因此負(fù)值效率低,因此不推薦使用??梢栽谙⒍x中定義枚舉,如上面的例子所示,也可以在外面定義——這樣就可以在.proto文件中的消息定義中重用這些枚舉。你還可以使用_MessageType_._EnumType_ 語(yǔ)法,使用在一個(gè)消息中聲明的enum類型作為不同消息中的字段類型。
當(dāng)對(duì)一個(gè)使用了枚舉的.proto文件運(yùn)行 protocol buffer 編譯器的時(shí)候,對(duì)于 Java, Kotlin,或 C++ 生成的代碼中將有一個(gè)對(duì)應(yīng)的enum,或者對(duì)于 Python 會(huì)生成一個(gè)特殊的EnumDescriptor類,它被用于在運(yùn)行時(shí)生成的類中創(chuàng)建一組帶有整數(shù)值的符號(hào)常量。
注意:生成的代碼可能會(huì)受到特定于語(yǔ)言的枚舉數(shù)限制(單種語(yǔ)言的數(shù)量低于千)。請(qǐng)檢查你計(jì)劃使用的語(yǔ)言的限制。
在反序列化過(guò)程中,不可識(shí)別的枚舉值將保留在消息中,盡管當(dāng)消息被反序列化時(shí),這種值的表示方式依賴于語(yǔ)言。在支持值超出指定符號(hào)范圍(如 C++ 和 Go)的開(kāi)放枚舉類型的語(yǔ)言中,未知枚舉值僅存儲(chǔ)為其底層的整數(shù)表示形式。在具有閉合枚舉類型(如 Java)的語(yǔ)言中,枚舉中的一個(gè)類型將用于表示一個(gè)無(wú)法識(shí)別的值,并且可以使用特殊的訪問(wèn)器訪問(wèn)底層的整數(shù)。在這兩種情況下,如果消息被序列化,那么不可識(shí)別的值仍然會(huì)與消息一起被序列化。
有關(guān)如何在應(yīng)用程序中使用消息enum的詳細(xì)信息,請(qǐng)參閱為所選語(yǔ)言生成的代碼指南。
預(yù)留值
如果通過(guò)完全刪除枚舉條目或注釋掉枚舉類型來(lái)更新枚舉類型,那么未來(lái)的用戶在自己更新該類型時(shí)可以重用該數(shù)值。這可能會(huì)導(dǎo)致嚴(yán)重的問(wèn)題,如果以后有人加載舊版本的相同.proto文件,包括數(shù)據(jù)損壞,隱私漏洞等等。確保不發(fā)生這種情況的一種方法是指定已刪除條目的數(shù)值(和/或名稱,這也可能導(dǎo)致 JSON 序列化問(wèn)題)為 reserved。如果任何未來(lái)的用戶試圖使用這些標(biāo)識(shí)符,protocol buffer 編譯器將報(bào)錯(cuò)。你可以使用 max關(guān)鍵字指定保留的數(shù)值范圍最大為可能的值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
注意,不能在同一個(gè)保留語(yǔ)句中混合字段名和數(shù)值。
使用其他消息類型
你可以使用其他消息類型作為字段類型。例如,假設(shè)你希望在每個(gè) SearchResponse消息中包含UI個(gè) Result消息——為了做到這一點(diǎn),你可以在同一個(gè).proto文件中定義 Result消息類型。然后在 SearchResponse中指定 Result 類型的字段。
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
導(dǎo)入定義
在上面的示例中,Result消息類型定義在與 SearchResponse相同的文件中——如果你希望用作字段類型的消息類型已經(jīng)在另一個(gè).proto文件中定義了,該怎么辦?
你可以通過(guò) import 來(lái)使用來(lái)自其他 .proto 文件的定義。要導(dǎo)入另一個(gè).proto 的定義,你需要在文件頂部添加一個(gè) import 語(yǔ)句:
import "myproject/other_protos.proto";
默認(rèn)情況下,只能從直接導(dǎo)入的 .proto 文件中使用定義。但是,有時(shí)你可能需要將 .proto 文件移動(dòng)到新的位置。你可以在舊目錄放一個(gè)占位的.proto文件使用import public 概念將所有導(dǎo)入轉(zhuǎn)發(fā)到新位置,而不必直接移動(dòng).proto文件并修改所有的地方。
注意,Java 中沒(méi)有 import public 功能。
import public依賴項(xiàng)可以被任何導(dǎo)入包含import public語(yǔ)句的 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";
// You use definitions from old.proto and new.proto, but not other.proto
protocol 編譯器使用命令行-I/--proto_path參數(shù)指定的一組目錄中搜索導(dǎo)入的文件。如果沒(méi)有給該命令行參數(shù),則查看調(diào)用編譯器的目錄。一般來(lái)說(shuō),你應(yīng)該將 --proto_path 參數(shù)設(shè)置為項(xiàng)目的根目錄并為所有導(dǎo)入使用正確的名稱。
使用proto2消息類型
導(dǎo)入 proto2消息類型并在 proto3消息中使用它們是可能的,反之亦然。然而,proto2 enum 不能直接在 proto3語(yǔ)法中使用(如果一個(gè)導(dǎo)入的 proto2消息使用了它們,那沒(méi)問(wèn)題)。
嵌套類型
你可以在其他消息類型中定義和使用消息類型,如下面的例子——這里的Result消息是在 SearchResponse消息中定義的:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果要在其父消息類型之外重用此消息類型,請(qǐng)通過(guò)_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;
}
}
}
更新消息類型
如果現(xiàn)有的消息類型不再滿足你的所有需要——例如,你希望消息格式有一個(gè)額外的字段——但是你仍然希望使用用舊格式創(chuàng)建的代碼,不要擔(dān)心!在不破壞任何現(xiàn)有代碼的情況下更新消息類型非常簡(jiǎn)單,只需記住以下規(guī)則:
- 不要更改任何現(xiàn)有字段的字段編號(hào)
- 如果添加新字段,那么任何使用“舊”消息格式通過(guò)代碼序列化的消息仍然可以通過(guò)新生成的代碼進(jìn)行解析。你應(yīng)該記住這些元素的默認(rèn)值,以便新代碼能夠正確地與舊代碼生成的消息交互。類似地,新代碼創(chuàng)建的消息可以通過(guò)舊代碼解析: 舊的二進(jìn)制文件在解析時(shí)直接忽略新字段。有關(guān)詳細(xì)信息,請(qǐng)參閱 未知字段 部分。
- 字段可以被刪除,只要字段編號(hào)不再用于你更新的消息類型。你可能希望改為重命名字段,或者為其添加”O(jiān)BSOLETE_“前綴,或者聲明字段編號(hào)為
reserved,以便.proto的未來(lái)用戶不可能不小心重復(fù)使用這個(gè)編號(hào)。 -
int32、uint32、int64、uint64和bool都是兼容的——這意味著你可以在不破壞向前或向后兼容性的情況下將一個(gè)字段從這些類型中的一個(gè)更改為另一個(gè)。 - 如果一個(gè)數(shù)字被解析到一個(gè)并不適當(dāng)?shù)念愋椭?,你?huì)得到與在 C++ 中將數(shù)字轉(zhuǎn)換為該類型相同的效果(例如,如果一個(gè)64位的數(shù)字被讀作 int32,它將被截?cái)酁?2位)
-
sint32和sint64相互兼容,但與其他整數(shù)類型不兼容。 -
string和bytes是兼容的,只要字節(jié)是有效的 UTF-8。 - 如果字節(jié)包含消息的編碼版本,則嵌入的消息與
bytes兼容。 -
fixed32與sfixed32兼容fixed64與sfixed64兼容。 - 對(duì)于
string、bytes和消息字段,optional字段與repeated字段兼容。給定重復(fù)字段的序列化數(shù)據(jù)作為輸入,如果該字段是基本類型字段,期望該字段為可選字段的客戶端將接受最后一個(gè)輸入值; 如果該字段是消息類型字段,則合并所有輸入元素。注意,這對(duì)于數(shù)字類型(包括 bools 和 enums)通常是不安全的。重復(fù)的數(shù)值類型字段可以按packed的格式序列化,如果是optional字段,則無(wú)法正確解析這些字段。 - Enum 在格式方面與 int32、 uint32、 int64和 uint64兼容(請(qǐng)注意,如果不適合,值將被截?cái)?。但是要注意,當(dāng)消息被反序列化時(shí),客戶端代碼可能會(huì)區(qū)別對(duì)待它們: 例如,未被識(shí)別的 proto3
enum類型將保留在消息中,但是當(dāng)消息被反序列化時(shí),這種類型的表示方式依賴于語(yǔ)言。Int 字段總是保留它們的值。 - 將單個(gè)值更改為新的
oneof成員是安全的,并且二進(jìn)制兼容。如果確保沒(méi)有代碼一次設(shè)置多個(gè)字段,那么將多個(gè)字段移動(dòng)到新的oneof字段中可能是安全的。將任何字段移動(dòng)到現(xiàn)有的字段中都是不安全的。
未知字段
未知字段是格式良好的協(xié)議緩沖區(qū)序列化數(shù)據(jù),表示解析器不識(shí)別的字段。例如,當(dāng)舊二進(jìn)制解析由新二進(jìn)制發(fā)送的帶有新字段的數(shù)據(jù)時(shí),這些新字段將成為舊二進(jìn)制中的未知字段。
最初,proto3消息在解析過(guò)程中總是丟棄未知字段,但在3.5版本中,我們重新引入了未知字段的保存來(lái)匹配 proto2行為。在3.5及以后的版本中,解析期間保留未知字段,并將其包含在序列化輸出中。
Any
Any 消息類型允許你將消息作為嵌入類型使用,而不需要其 .proto 定義。Any包含一個(gè)任意序列化的字節(jié)消息,以及一個(gè)解析為該消息的類型作為消息的全局唯一標(biāo)識(shí)符的URL。要使用 Any類型,需要導(dǎo)入google/protobuf/any.proto。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
給定消息類型的默認(rèn)類型 URL 是type.googleapis.com/_packagename_._messagename_。
不同的語(yǔ)言實(shí)現(xiàn)將支持運(yùn)行庫(kù)助手以類型安全的方式打包和解包 Any值。例如在java中,Any類型會(huì)有特殊的pack()和unpack()訪問(wèn)器,在C++中會(huì)有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 ...
}
}
目前正在開(kāi)發(fā)用于處理任何類型的運(yùn)行時(shí)庫(kù)。
如果你已經(jīng)熟悉 proto2語(yǔ)法,Any 可以保存任意的 proto3消息,類似于 proto2消息,可以允許擴(kuò)展。
oneof
如果你有一條包含多個(gè)字段的消息,并且最多同時(shí)設(shè)置其中一個(gè)字段,那么你可以通過(guò)使用oneof來(lái)實(shí)現(xiàn)并節(jié)省內(nèi)存。
oneof字段類似于常規(guī)字段,只不過(guò)oneof中的所有字段共享內(nèi)存,而且最多可以同時(shí)設(shè)置一個(gè)字段。設(shè)置其中的任何成員都會(huì)自動(dòng)清除所有其他成員。根據(jù)所選擇的語(yǔ)言,可以使用特殊 case()或 WhichOneof() 方法檢查 one of 中的哪個(gè)值被設(shè)置(如果有的話)。
使用oneof
要定義 oneof 字段需要在你的.proto文件中使用oneof關(guān)鍵字并在后面跟上名稱,在下面的例子中字段名稱為test_oneof。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后將其中一個(gè)字段添加到該字段的定義中。你可以添加任何類型的字段,除了map字段和repeated字段。
在生成的代碼中,其中一個(gè)字段具有與常規(guī)字段相同的 getter 和 setter。你還可以獲得一個(gè)特殊的方法來(lái)檢查其中一個(gè)設(shè)置了哪個(gè)值(如果有的話)。你可以在相關(guān)的 API 參考文獻(xiàn)中找到更多關(guān)于所選語(yǔ)言的 API。
oneof 特性
- 設(shè)置一個(gè)字段將自動(dòng)清除該字段的所有其他成員。因此,如果你設(shè)置了多個(gè) oneof字段,那么只有最后設(shè)置的字段仍然具有值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
- 如果解析器在連接中遇到同一個(gè)成員的多個(gè)成員,則只有最后看到的成員用于解析消息。
- oneof 不支持
repeated。 - 反射 api 適用于 oneof 字段。
- 如果將 oneof 字段設(shè)置為默認(rèn)值(例如將 int32 oneof 字段設(shè)置為0) ,則將設(shè)置該字段的“ case”,并在連接上序列化該值。
- 如果你使用 C++ ,確保你的代碼不會(huì)導(dǎo)致內(nèi)存崩潰。下面的示例代碼將崩潰,因?yàn)橥ㄟ^(guò)調(diào)用
set_name()方法已經(jīng)刪除了sub_message。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
- 在C++中,如果你使用
Swap()兩個(gè) oneof 消息,每個(gè)消息,兩個(gè)消息將擁有對(duì)方的值,例如在下面的例子中,msg1會(huì)擁有sub_message并且msg2會(huì)有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());
向后兼容性問(wèn)題
添加或刪除一個(gè)字段時(shí)要小心。如果檢查 one of 的值返回None/NOT_SET,這可能意味著 one of 沒(méi)有被設(shè)置,或者它已經(jīng)被設(shè)置為 one of 的不同版本中的一個(gè)字段。這沒(méi)有辦法區(qū)分,因?yàn)闆](méi)有辦法知道未知字段是否是 oneof 的成員。
標(biāo)簽重用問(wèn)題
- 將字段移入或移出 oneof:在序列化和解析消息之后,你可能會(huì)丟失一些信息(某些字段將被清除)。但是,你可以安全地將單個(gè)字段移動(dòng)到新的 oneof 字段中,并且如果已知只設(shè)置了一個(gè)字段,則可以移動(dòng)多個(gè)字段。
- 刪除一個(gè)oneof 字段再添加回來(lái):這可能會(huì)在消息被序列化和解析后清除當(dāng)前設(shè)置的 oneof 字段。
- 拆分或合并oneof:這與移動(dòng)常規(guī)字段有類似的問(wèn)題。
Maps
如果你想創(chuàng)建一個(gè)關(guān)聯(lián)映射作為你數(shù)據(jù)定義的一部分,protocol buffers提供了一個(gè)方便的快捷語(yǔ)法:
map<key_type, value_type> map_field = N;
…其中key_type可以是任何整型或字符串類型(因此,除了浮點(diǎn)類型和字節(jié)以外的任何標(biāo)量類型) 。注意,枚舉不是有效的key_type。value_type可以是除另一個(gè)映射以外的任何類型。
例如,如果你想創(chuàng)建一個(gè)項(xiàng)目映射,其中每個(gè)Project消息都與一個(gè)字符串鍵相關(guān)聯(lián),你可以這樣定義:
map<string, Project> projects = 3;
- 映射字段不能重復(fù)。
- 映射值的有線格式排序和映射迭代排序是未定義的,因此不能依賴于映射項(xiàng)的特定排序。
- 當(dāng)為
.proto生成文本格式時(shí),映射按鍵排序。數(shù)字鍵按數(shù)字排序。 - 當(dāng)從連接解析或合并時(shí),如果有重復(fù)的映射鍵,則使用最后看到的鍵。當(dāng)從文本格式解析映射時(shí),如果有重復(fù)的鍵,解析可能會(huì)失敗。
- 如果為映射字段提供了鍵但沒(méi)有值,則該字段序列化時(shí)的行為與語(yǔ)言相關(guān)。在 C++ 、 Java、 Kotlin 和 Python 中,類型的默認(rèn)值是序列化的,而在其他語(yǔ)言中,沒(méi)有任何值是序列化的。
生成的映射 API 目前可用于所有支持 proto3的語(yǔ)言。你可以在相關(guān)的 API 參考中找到更多關(guān)于所選語(yǔ)言的映射 API 的信息。
向后兼容性
map語(yǔ)法序列化后等同于如下內(nèi)容,因此即使是不支持map語(yǔ)法的protocol buffer實(shí)現(xiàn)也是可以處理你的數(shù)據(jù)的:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射的protocol buffers實(shí)現(xiàn)都必須生成并接受上述定義可以接受的數(shù)據(jù)。
Packages
可以向 .proto 文件添加一個(gè)可選package說(shuō)明符,以防止協(xié)議消息類型之間的名稱沖突。
package foo.bar;
message Open { ... }
然后,你可以在定義消息類型的字段時(shí)使用package說(shuō)明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
package 說(shuō)明符影響生成代碼的方式取決于你選擇的語(yǔ)言:
- 對(duì)于C++,產(chǎn)生的類會(huì)被包裝在C++的命名空間中,如上例中的
Open會(huì)被封裝在foo::bar空間中; - 對(duì)于Java和Kotlin,包聲明符會(huì)變?yōu)閖ava的一個(gè)包,除非在
.proto文件中提供了一個(gè)明確有option java_package; - 對(duì)于 Python,這個(gè)包聲明符是被忽略的,因?yàn)镻ython模塊是按照其在文件系統(tǒng)中的位置進(jìn)行組織的
- 對(duì)于Go,包可以被用做Go包名稱,除非你顯式的提供一個(gè)
option go_package在你的.proto文件中。 - 對(duì)于Ruby,生成的類可以被包裝在內(nèi)置的Ruby名稱空間中,轉(zhuǎn)換成Ruby所需的大小寫樣式 (首字母大寫;如果第一個(gè)符號(hào)不是一個(gè)字母,則使用PB_前綴),例如Open會(huì)在
Foo::Bar名稱空間中。 - 在 C# 中,包在轉(zhuǎn)換到 PascalCase 后被用作名稱空間,除非你在
.proto文件中提供option csharp_namespace。例如,Open將位于Foo.Bar名稱空間中。
package和名稱解析
在 protocol buffer 語(yǔ)言中,類型名稱解析的工作原理類似于 C++ : 首先搜索最內(nèi)層的作用域,然后搜索下一個(gè)最內(nèi)層的作用域,依此類推,每個(gè)包都被認(rèn)為是其父包的“ inner”。前導(dǎo)的“ .”(例如,.foo.bar.Baz)表示從最外側(cè)的范圍開(kāi)始。
protocol buffer 通過(guò)解析導(dǎo)入的 .proto 文件來(lái)解析所有類型名稱。每種語(yǔ)言的代碼生成器都知道如何引用該語(yǔ)言中的每種類型,即使它有不同的作用域規(guī)則。
定義服務(wù)
如果希望將消息類型與 RPC (遠(yuǎn)程過(guò)程調(diào)用)系統(tǒng)一起使用,可以在.proto 文件和 protocol buffer 編譯器將用你選擇的語(yǔ)言生成服務(wù)接口代碼和存根。因此,例如你希望定義一個(gè) RPC 服務(wù),其方法接受你的 SearchRequest并返回一個(gè) SearchResponse,則可以在.proto文件如下定義。
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
使用 protocol buffers 最直接的 RPC 系統(tǒng)是 gRPC,這是 Google 開(kāi)發(fā)的一個(gè)語(yǔ)言和平臺(tái)中立的開(kāi)源 RPC 系統(tǒng),可以與 protocol buffers 一起使用。gRPC 特別適用于protocol buffers ,它可以讓你直接從你的.proto文件使用特殊的 protocol buffers 編譯器插件。
如果你不想使用 gRPC,你也可以在你自己的 RPC 實(shí)現(xiàn)中使用協(xié)議緩沖。你可以在《proto2語(yǔ)言指南》中找到更多相關(guān)信息。
還有一些正在進(jìn)行的第三方項(xiàng)目正在開(kāi)發(fā) RPC 的實(shí)施協(xié)議緩沖。有關(guān)我們所知道的項(xiàng)目的鏈接列表,請(qǐng)參閱第三方添加項(xiàng) wiki 頁(yè)面。
JSON 映射
proto3支持 JSON 的規(guī)范編碼,使得系統(tǒng)之間更容易共享數(shù)據(jù)。下表按類型逐一描述了編碼。
如果 json 編碼的數(shù)據(jù)中缺少某個(gè)值,或者該值為 null,那么在解析為 protocol buffer 時(shí),該值將被解釋為適當(dāng)?shù)哪J(rèn)值。如果一個(gè)字段在 protocol buffer 中具有默認(rèn)值,為了節(jié)省空間,默認(rèn)情況下 json 編碼的數(shù)據(jù)中將省略該字段。具體實(shí)現(xiàn)可以提供在JSON編碼中可選的默認(rèn)值。
| proto3 | JSON | JSON example | Notes |
|---|---|---|---|
| message | object | {"fooBar": v, "g": null, …} |
生成 JSON 對(duì)象。消息字段名映射到 lowerCamelCase 并成為 JSON 對(duì)象鍵。如果指定了 json_name 字段選項(xiàng),則將使用指定的值作為鍵。解析器接受 lowerCamelCase 名稱(或 json_name 選項(xiàng)指定的名稱)和原始 proto 字段名稱。 null 是所有字段類型的接受值,并被視為相應(yīng)字段類型的默認(rèn)值。 |
| enum | string | "FOO_BAR" |
使用 proto 中指定的枚舉值的名稱。解析器接受枚舉名稱和整數(shù)值。 |
| map | object | {"k": v, …} |
所有鍵都轉(zhuǎn)換為字符串。 |
| repeated V | array | [v, …] |
null 被接受為空列表 []。 |
| bool | true, false | true, false |
|
| string | string | "Hello World!" |
|
| bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON 值將是使用帶填充的標(biāo)準(zhǔn) base64編碼方式編碼為字符串的數(shù)據(jù)。接受帶/不帶填充的標(biāo)準(zhǔn)或 URL 安全的 base64編碼。 |
| int32, fixed32, uint32 | number | 1, -10, 0 |
JSON 值將是一個(gè)十進(jìn)制數(shù)字。接受數(shù)字或字符串。 |
| int64, fixed64, uint64 | string | "1", "-10" |
JSON 值將是一個(gè)十進(jìn)制字符串。接受數(shù)字或字符串。 |
| float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" |
JSON 值將是一個(gè)數(shù)字或一個(gè)特殊的字符串值“NaN”、“ Infinity”和“-Infinity”。接受數(shù)字或字符串。也接受指數(shù)表示法。-0被認(rèn)為等效于0。 |
| Any | object |
{"@type": "url", "f": v, … } |
如果Any包含一個(gè)具有特殊 JSON 映射的值,它將被轉(zhuǎn)換如下: {"@type": xxx, "value": yyy}. 否則,該值將轉(zhuǎn)換為 JSON 對(duì)象,并插入"@type"字段以指示實(shí)際的數(shù)據(jù)類型。 |
| Timestamp | string | "1972-01-01T10:00:20.021Z" |
使用 RFC3339,其中生成的輸出將始終是 Z 標(biāo)準(zhǔn)化的,并使用0、3、6或9個(gè)小數(shù)位。也接受“ Z”以外的偏移量。 |
| Duration | string | "1.000340012s", "1s" |
生成的輸出總是包含0、3、6或9個(gè)小數(shù)位,具體取決于所需的精度,后綴“ s”。接受任何小數(shù)位(也可以沒(méi)有) ,只要他們符合納秒精度和后綴“ s”是必需的。 |
| Struct | object |
{ … } |
任何JSON對(duì)象。請(qǐng)參見(jiàn) struct.proto。 |
| Wrapper types | various types | 2, "2", "foo", true, "true", null, 0, … |
Wrappers 使用與包裝原語(yǔ)類型相同的 JSON 表示,只是在數(shù)據(jù)轉(zhuǎn)換和傳輸期間允許并保留 null。 |
| FieldMask | string | "f.fooBar,h" |
請(qǐng)參見(jiàn) field_mask.proto. |
| ListValue | array | [foo, bar, …] |
|
| Value | value | 任何 JSON 值。請(qǐng)檢查 google.protobuf.Value 以獲取詳細(xì)信息。 | |
| NullValue | null | JSON null | |
| Empty | object | {} |
一個(gè)空的JSON對(duì)象 |
JSON選項(xiàng)
一個(gè)proto3協(xié)議 JSON 實(shí)現(xiàn)可能提供以下選項(xiàng):
- 提供默認(rèn)值的字段:在proto3 JSON 輸出中,值為默認(rèn)值的字段被省略??梢蕴峁┮粋€(gè)選項(xiàng),用默認(rèn)值覆蓋此行為和輸出字段。
- 忽略位置字段:在缺省情況下,Proto3 JSON 解析器應(yīng)該拒絕未知字段,但在解析過(guò)程中可能會(huì)提供一個(gè)忽略未知字段的選項(xiàng)。
- 使用 proto 字段名而不是小駝峰名稱:默認(rèn)情況下,proto3 JSON 打印機(jī)應(yīng)該將字段名轉(zhuǎn)換為 lowerCamelCase,并使用它作為 JSON 名稱??梢蕴峁┮粋€(gè)選項(xiàng),用原型字段名作為 JSON 名。需要協(xié)議3 JSON 解析器同時(shí)接受轉(zhuǎn)換后的 lowerCamelCase 名稱和原始字段名稱。
- 以整數(shù)而不是字符串形式展示枚舉值:在 JSON 輸出中,默認(rèn)情況下使用枚舉值的名稱??梢蕴峁┮粋€(gè)選項(xiàng)來(lái)代替使用枚舉值的數(shù)值。
剩下options等內(nèi)容本文略。