【golang微服務(wù)】Protocol Buffers V3中文語(yǔ)法指南

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_numberresult_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::kFirstReservedNumberFieldDescriptor::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.hpbobjc.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、 WEBIMAGES、 LOCAL、 NEWS、 PRODUCTSVIDEO。你可以通過(guò)在消息定義中添加一個(gè)枚舉,為每個(gè)可能的值添加一個(gè)常量來(lái)非常簡(jiǎn)單地完成這項(xiàng)工作。

在下面的例子中,我們添加了一個(gè)名為 Corpusenum,包含所有可能的值,以及一個(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)?

  1. 必須有一個(gè)零值,這樣我們就可以使用0作為數(shù)值默認(rèn)值。
  2. 零值必須是第一個(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、 int64uint64bool都是兼容的——這意味著你可以在不破壞向前或向后兼容性的情況下將一個(gè)字段從這些類型中的一個(gè)更改為另一個(gè)。
  • 如果一個(gè)數(shù)字被解析到一個(gè)并不適當(dāng)?shù)念愋椭?,你?huì)得到與在 C++ 中將數(shù)字轉(zhuǎn)換為該類型相同的效果(例如,如果一個(gè)64位的數(shù)字被讀作 int32,它將被截?cái)酁?2位)
  • sint32sint64相互兼容,但與其他整數(shù)類型不兼容。
  • stringbytes是兼容的,只要字節(jié)是有效的 UTF-8。
  • 如果字節(jié)包含消息的編碼版本,則嵌入的消息與bytes兼容。
  • fixed32sfixed32兼容 fixed64sfixed64兼容。
  • 對(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ì)于JavaKotlin,包聲明符會(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)容本文略。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容