Protobuf3語言指南(轉(zhuǎn))

英文原文:
Language Guide (proto3)
中文出處:
Protobuf語言指南
[譯]Protobuf 語法指南
中文出處是proto2的譯文,proto3的英文出現(xiàn)后在原來基礎(chǔ)上增改了,水平有限,還請(qǐng)指正

這個(gè)指南描述了如何使用Protocol buffer 語言去描述你的protocol buffer 數(shù)據(jù), 包括 .proto文件符號(hào)和如何從.proto文件生成類。包含了proto2版本的protocol buffer語言:對(duì)于老版本的proto3 符號(hào),請(qǐng)見Proto2 Language Guide(以及中文譯本,抄了很多這里的感謝下老版本的翻譯者)

本文是一個(gè)參考指南——如果要查看如何使用本文中描述的多個(gè)特性的循序漸進(jìn)的例子,請(qǐng)?jiān)?a target="_blank">教程中查找需要的語言的教程。

定義一個(gè)消息類型

先來看一個(gè)非常簡(jiǎn)單的例子。假設(shè)你想定義一個(gè)“搜索請(qǐng)求”的消息格式,每一個(gè)請(qǐng)求含有一個(gè)查詢字符串、你感興趣的查詢結(jié)果所在的頁數(shù),以及每一頁多少條查詢結(jié)果??梢圆捎萌缦碌姆绞絹矶x消息類型的.proto文件了:

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}1234567
  • 文件的第一行指定了你正在使用proto3語法:如果你沒有指定這個(gè),編譯器會(huì)使用proto2。這個(gè)指定語法行必須是文件的非空非注釋的第一個(gè)行。
  • SearchRequest消息格式有3個(gè)字段,在消息中承載的數(shù)據(jù)分別對(duì)應(yīng)于每一個(gè)字段。其中每個(gè)字段都有一個(gè)名字和一種類型。

指定字段類型

在上面的例子中,所有字段都是標(biāo)量類型:兩個(gè)整型(page_number和result_per_page),一個(gè)string類型(query)。當(dāng)然,你也可以為字段指定其他的合成類型,包括枚舉(enumerations)或其他消息類型。

分配標(biāo)識(shí)號(hào)

正如你所見,在消息定義中,每個(gè)字段都有唯一的一個(gè)數(shù)字標(biāo)識(shí)符。這些標(biāo)識(shí)符是用來在消息的二進(jìn)制格式中識(shí)別各個(gè)字段的,一旦開始使用就不能夠再改變。注:[1,15]之內(nèi)的標(biāo)識(shí)號(hào)在編碼的時(shí)候會(huì)占用一個(gè)字節(jié)。[16,2047]之內(nèi)的標(biāo)識(shí)號(hào)則占用2個(gè)字節(jié)。所以應(yīng)該為那些頻繁出現(xiàn)的消息元素保留 [1,15]之內(nèi)的標(biāo)識(shí)號(hào)。切記:要為將來有可能添加的、頻繁出現(xiàn)的標(biāo)識(shí)號(hào)預(yù)留一些標(biāo)識(shí)號(hào)。

最小的標(biāo)識(shí)號(hào)可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (從FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的標(biāo)識(shí)號(hào), Protobuf協(xié)議實(shí)現(xiàn)中對(duì)這些進(jìn)行了預(yù)留。如果非要在.proto文件中使用這些預(yù)留標(biāo)識(shí)號(hào),編譯時(shí)就會(huì)報(bào)警。同樣你也不能使用早期保留的標(biāo)識(shí)號(hào)。

指定字段規(guī)則

所指定的消息字段修飾符必須是如下之一:

  • singular:一個(gè)格式良好的消息應(yīng)該有0個(gè)或者1個(gè)這種字段(但是不能超過1個(gè))。

  • repeated:在一個(gè)格式良好的消息中,這種字段可以重復(fù)任意多次(包括0次)。重復(fù)的值的順序會(huì)被保留。

    在proto3中,repeated的標(biāo)量域默認(rèn)情況蝦使用packed。

    你可以了解更多的pakced屬性在Protocol Buffer 編碼

添加更多消息類型

在一個(gè).proto文件中可以定義多個(gè)消息類型。在定義多個(gè)相關(guān)的消息的時(shí)候,這一點(diǎn)特別有用——例如,如果想定義與SearchResponse消息類型對(duì)應(yīng)的回復(fù)消息格式的話,你可以將它添加到相同的.proto文件中,如:

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}123456789

添加注釋

向.proto文件添加注釋,可以使用C/C++/java風(fēng)格的雙斜杠(//) 語法格式,如:

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.
}12345

保留標(biāo)識(shí)符(Reserved)

如果你通過刪除或者注釋所有域,以后的用戶可以重用標(biāo)識(shí)號(hào)當(dāng)你重新更新類型的時(shí)候。如果你使用舊版本加載相同的.proto文件這會(huì)導(dǎo)致嚴(yán)重的問題,包括數(shù)據(jù)損壞、隱私錯(cuò)誤等等?,F(xiàn)在有一種確保不會(huì)發(fā)生這種情況的方法就是指定保留標(biāo)識(shí)符(and/or names, which can also cause issues for JSON serialization不明白什么意思),protocol buffer的編譯器會(huì)警告未來嘗試使用這些域標(biāo)識(shí)符的用戶。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}1234

注:不要在同一行reserved聲明中同時(shí)聲明域名字和標(biāo)識(shí)號(hào)

從.proto文件生成了什么?

當(dāng)用protocol buffer編譯器來運(yùn)行.proto文件時(shí),編譯器將生成所選擇語言的代碼,這些代碼可以操作在.proto文件中定義的消息類型,包括獲取、設(shè)置字段值,將消息序列化到一個(gè)輸出流中,以及從一個(gè)輸入流中解析消息。

  • 對(duì)C++來說,編譯器會(huì)為每個(gè).proto文件生成一個(gè).h文件和一個(gè).cc文件,.proto文件中的每一個(gè)消息有一個(gè)對(duì)應(yīng)的類。
  • 對(duì)Java來說,編譯器為每一個(gè)消息類型生成了一個(gè).java文件,以及一個(gè)特殊的Builder類(該類是用來創(chuàng)建消息類接口的)。
  • 對(duì)Python來說,有點(diǎn)不太一樣——Python編譯器為.proto文件中的每個(gè)消息類型生成一個(gè)含有靜態(tài)描述符的模塊,,該模塊與一個(gè)元類(metaclass)在運(yùn)行時(shí)(runtime)被用來創(chuàng)建所需的Python數(shù)據(jù)訪問類。
  • 對(duì)go來說,編譯器會(huì)位每個(gè)消息類型生成了一個(gè).pd.go文件。
  • 對(duì)于Ruby來說,編譯器會(huì)為每個(gè)消息類型生成了一個(gè).rb文件。
  • javaNano來說,編譯器輸出類似域java但是沒有Builder類
  • 對(duì)于Objective-C來說,編譯器會(huì)為每個(gè)消息類型生成了一個(gè)pbobjc.h文件和pbobjcm文件,.proto文件中的每一個(gè)消息有一個(gè)對(duì)應(yīng)的類。
  • 對(duì)于C#來說,編譯器會(huì)為每個(gè)消息類型生成了一個(gè).cs文件,.proto文件中的每一個(gè)消息有一個(gè)對(duì)應(yīng)的類。

你可以從如下的文檔鏈接中獲取每種語言更多API(proto3版本的內(nèi)容很快就公布)。API Reference

標(biāo)量數(shù)值類型

一個(gè)標(biāo)量消息字段可以含有一個(gè)如下的類型——該表格展示了定義于.proto文件中的類型,以及與之對(duì)應(yīng)的、在自動(dòng)生成的訪問類中定義的類型:

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type
double double double float float64 Float double float
float float float float float32 Float float float
int32 使用變長編碼,對(duì)于負(fù)值的效率很低,如果你的域有可能有負(fù)值,請(qǐng)使用sint64替代 int32 int int int32 Fixnum 或者 Bignum(根據(jù)需要) int integer
uint32 使用變長編碼 uint32 int int/long uint32 Fixnum 或者 Bignum(根據(jù)需要) uint integer
uint64 使用變長編碼 uint64 long int/long uint64 Bignum ulong integer/string
sint32 使用變長編碼,這些編碼在負(fù)值時(shí)比int32高效的多 int32 int int int32 Fixnum 或者 Bignum(根據(jù)需要) int integer
sint64 使用變長編碼,有符號(hào)的整型值。編碼時(shí)比通常的int64高效。 int64 long int/long int64 Bignum long integer/string
fixed32 總是4個(gè)字節(jié),如果數(shù)值總是比總是比228大的話,這個(gè)類型會(huì)比uint32高效。 uint32 int int uint32 Fixnum 或者 Bignum(根據(jù)需要) uint integer
fixed64 總是8個(gè)字節(jié),如果數(shù)值總是比總是比256大的話,這個(gè)類型會(huì)比uint64高效。 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 總是4個(gè)字節(jié) int32 int int int32 Fixnum 或者 Bignum(根據(jù)需要) int integer
sfixed64 總是8個(gè)字節(jié) int64 long int/long int64 Bignum long integer/string
bool bool boolean bool bool TrueClass/FalseClass bool boolean
string 一個(gè)字符串必須是UTF-8編碼或者7-bit ASCII編碼的文本。 string String str/unicode string String (UTF-8) string string
bytes 可能包含任意順序的字節(jié)數(shù)據(jù)。 string ByteString str []byte String (ASCII-8BIT) ByteString string

你可以在文章Protocol Buffer 編碼中,找到更多“序列化消息時(shí)各種類型如何編碼”的信息。

  1. 在java中,無符號(hào)32位和64位整型被表示成他們的整型對(duì)應(yīng)形似,最高位被儲(chǔ)存在標(biāo)志位中。
  2. 對(duì)于所有的情況,設(shè)定值會(huì)執(zhí)行類型檢查以確保此值是有效。
  3. 64位或者無符號(hào)32位整型在解碼時(shí)被表示成為ilong,但是在設(shè)置時(shí)可以使用int型值設(shè)定,在所有的情況下,值必須符合其設(shè)置其類型的要求。
  4. python中string被表示成在解碼時(shí)表示成unicode。但是一個(gè)ASCIIstring可以被表示成str類型。
  5. Integer在64位的機(jī)器上使用,string在32位機(jī)器上使用

默認(rèn)值

當(dāng)一個(gè)消息被解析的時(shí)候,如果被編碼的信息不包含一個(gè)特定的singular元素,被解析的對(duì)象鎖對(duì)應(yīng)的域被設(shè)置位一個(gè)默認(rèn)值,對(duì)于不同類型指定如下:

  • 對(duì)于strings,默認(rèn)是一個(gè)空string

  • 對(duì)于bytes,默認(rèn)是一個(gè)空的bytes

  • 對(duì)于bools,默認(rèn)是false

  • 對(duì)于數(shù)值類型,默認(rèn)是0

  • 對(duì)于枚舉,默認(rèn)是第一個(gè)定義的枚舉值,必須為0;

  • 對(duì)于消息類型(message),域沒有被設(shè)置,確切的消息是根據(jù)語言確定的,詳見generated code guide

    對(duì)于可重復(fù)域的默認(rèn)值是空(通常情況下是對(duì)應(yīng)語言中空列表)。

    注:對(duì)于標(biāo)量消息域,一旦消息被解析,就無法判斷域釋放被設(shè)置為默認(rèn)值(例如,例如boolean值是否被設(shè)置為false)還是根本沒有被設(shè)置。你應(yīng)該在定義你的消息類型時(shí)非常注意。例如,比如你不應(yīng)該定義boolean的默認(rèn)值false作為任何行為的觸發(fā)方式。也應(yīng)該注意如果一個(gè)標(biāo)量消息域被設(shè)置為標(biāo)志位,這個(gè)值不應(yīng)該被序列化傳輸。

    查看generated code guide選擇你的語言的默認(rèn)值的工作細(xì)節(jié)。

枚舉

當(dāng)需要定義一個(gè)消息類型的時(shí)候,可能想為一個(gè)字段指定某“預(yù)定義值序列”中的一個(gè)值。例如,假設(shè)要為每一個(gè)SearchRequest消息添加一個(gè) corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一個(gè)。 其實(shí)可以很容易地實(shí)現(xiàn)這一點(diǎn):通過向消息定義中添加一個(gè)枚舉(enum)并且為每個(gè)可能的值定義一個(gè)常量就可以了。

在下面的例子中,在消息格式中添加了一個(gè)叫做Corpus的枚舉類型——它含有所有可能的值 ——以及一個(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;
}123456789101112131415

如你所見,Corpus枚舉的第一個(gè)常量映射為0:每個(gè)枚舉類型必須將其第一個(gè)類型映射為0,這是因?yàn)椋?/p>

  • 必須有有一個(gè)0值,我們可以用這個(gè)0值作為默認(rèn)值。

  • 這個(gè)零值必須為第一個(gè)元素,為了兼容proto2語義,枚舉類的第一個(gè)值總是默認(rèn)值。

    你可以通過將不同的枚舉常量指定位相同的值。如果這樣做你需要將allow_alias設(shè)定位true,否則編譯器會(huì)在別名的地方產(chǎn)生一個(gè)錯(cuò)誤信息。

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.
}1234567891011

枚舉常量必須在32位整型值的范圍內(nèi)。因?yàn)閑num值是使用可變編碼方式的,對(duì)負(fù)數(shù)不夠高效,因此不推薦在enum中使用負(fù)數(shù)。如上例所示,可以在 一個(gè)消息定義的內(nèi)部或外部定義枚舉——這些枚舉可以在.proto文件中的任何消息定義里重用。當(dāng)然也可以在一個(gè)消息中聲明一個(gè)枚舉類型,而在另一個(gè)不同 的消息中使用它——采用MessageType.EnumType的語法格式。

當(dāng)對(duì)一個(gè)使用了枚舉的.proto文件運(yùn)行protocol buffer編譯器的時(shí)候,生成的代碼中將有一個(gè)對(duì)應(yīng)的enum(對(duì)Java或C++來說),或者一個(gè)特殊的EnumDescriptor類(對(duì) Python來說),它被用來在運(yùn)行時(shí)生成的類中創(chuàng)建一系列的整型值符號(hào)常量(symbolic constants)。

在反序列化的過程中,無法識(shí)別的枚舉值會(huì)被保存在消息中,雖然這種表示方式需要依據(jù)所使用語言而定。在那些支持開放枚舉類型超出指定范圍之外的語言中(例如C++和Go),為識(shí)別的值會(huì)被表示成所支持的整型。在使用封閉枚舉類型的語言中(Java),使用枚舉中的一個(gè)類型來表示未識(shí)別的值,并且可以使用所支持整型來訪問。在其他情況下,如果解析的消息被序列號(hào),未識(shí)別的值將保持原樣。

關(guān)于如何在你的應(yīng)用程序的消息中使用枚舉的更多信息,請(qǐng)查看所選擇的語言generated code guide

使用其他消息類型

你可以將其他消息類型用作字段類型。例如,假設(shè)在每一個(gè)SearchResponse消息中包含Result消息,此時(shí)可以在相同的.proto文件中定義一個(gè)Result消息類型,然后在SearchResponse消息中指定一個(gè)Result類型的字段,如:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}123456789

導(dǎo)入定義

在上面的例子中,Result消息類型與SearchResponse是定義在同一文件中的。如果想要使用的消息類型已經(jīng)在其他.proto文件中已經(jīng)定義過了呢?
你可以通過導(dǎo)入(importing)其他.proto文件中的定義來使用它們。要導(dǎo)入其他.proto文件的定義,你需要在你的文件中添加一個(gè)導(dǎo)入聲明,如:

import "myproject/other_protos.proto";1

默認(rèn)情況下你只能使用直接導(dǎo)入的.proto文件中的定義. 然而, 有時(shí)候你需要移動(dòng)一個(gè).proto文件到一個(gè)新的位置, 可以不直接移動(dòng).proto文件, 只需放入一個(gè)偽 .proto 文件在老的位置, 然后使用import public轉(zhuǎn)向新的位置。import public 依賴性會(huì)通過任意導(dǎo)入包含import public聲明的proto文件傳遞。例如:

// 這是新的proto
// All definitions are moved here12
// 這是久的proto
// 這是所有客戶端正在導(dǎo)入的包
import public "new.proto";
import "other.proto";1234
// 客戶端proto
import "old.proto";
// 現(xiàn)在你可以使用新久兩種包的proto定義了。123

通過在編譯器命令行參數(shù)中使用-I/--proto_pathprotocal 編譯器會(huì)在指定目錄搜索要導(dǎo)入的文件。如果沒有給出標(biāo)志,編譯器會(huì)搜索編譯命令被調(diào)用的目錄。通常你只要指定proto_path標(biāo)志為你的工程根目錄就好。并且指定好導(dǎo)入的正確名稱就好。

使用proto2消息類型

在你的proto3消息中導(dǎo)入proto2的消息類型也是可以的,反之亦然,然后proto2枚舉不可以直接在proto3的標(biāo)識(shí)符中使用(如果僅僅在proto2消息中使用是可以的)。

嵌套類型

你可以在其他消息類型中定義、使用消息類型,在下面的例子中,Result消息就定義在SearchResponse消息內(nèi),如:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}12345678

如果你想在它的父消息類型的外部重用這個(gè)消息類型,你需要以Parent.Type的形式使用它,如:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}123

當(dāng)然,你也可以將消息嵌套任意多層,如:

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;
    }
  }
}1234567891011121314

更新一個(gè)消息類型

如果一個(gè)已有的消息格式已無法滿足新的需求——如,要在消息中添加一個(gè)額外的字段——但是同時(shí)舊版本寫的代碼仍然可用。不用擔(dān)心!更新消息而不破壞已有代碼是非常簡(jiǎn)單的。在更新時(shí)只要記住以下的規(guī)則即可。

  • 不要更改任何已有的字段的數(shù)值標(biāo)識(shí)。
  • 如果你增加新的字段,使用舊格式的字段仍然可以被你新產(chǎn)生的代碼所解析。你應(yīng)該記住這些元素的默認(rèn)值這樣你的新代碼就可以以適當(dāng)?shù)姆绞胶团f代碼產(chǎn)生的數(shù)據(jù)交互。相似的,通過新代碼產(chǎn)生的消息也可以被舊代碼解析:只不過新的字段會(huì)被忽視掉。注意,未被識(shí)別的字段會(huì)在反序列化的過程中丟棄掉,所以如果消息再被傳遞給新的代碼,新的字段依然是不可用的(這和proto2中的行為是不同的,在proto2中未定義的域依然會(huì)隨著消息被序列化)
  • 非required的字段可以移除——只要它們的標(biāo)識(shí)號(hào)在新的消息類型中不再使用(更好的做法可能是重命名那個(gè)字段,例如在字段前添加“OBSOLETE_”前綴,那樣的話,使用的.proto文件的用戶將來就不會(huì)無意中重新使用了那些不該使用的標(biāo)識(shí)號(hào))。
  • int32, uint32, int64, uint64,和bool是全部兼容的,這意味著可以將這些類型中的一個(gè)轉(zhuǎn)換為另外一個(gè),而不會(huì)破壞向前、 向后的兼容性。如果解析出來的數(shù)字與對(duì)應(yīng)的類型不相符,那么結(jié)果就像在C++中對(duì)它進(jìn)行了強(qiáng)制類型轉(zhuǎn)換一樣(例如,如果把一個(gè)64位數(shù)字當(dāng)作int32來 讀取,那么它就會(huì)被截?cái)酁?2位的數(shù)字)。
  • sint32和sint64是互相兼容的,但是它們與其他整數(shù)類型不兼容。
  • string和bytes是兼容的——只要bytes是有效的UTF-8編碼。
  • 嵌套消息與bytes是兼容的——只要bytes包含該消息的一個(gè)編碼過的版本。
  • fixed32與sfixed32是兼容的,fixed64與sfixed64是兼容的。
  • 枚舉類型與int32,uint32,int64和uint64相兼容(注意如果值不相兼容則會(huì)被截?cái)啵欢诳蛻舳朔葱蛄谢笏麄兛赡軙?huì)有不同的處理方式,例如,未識(shí)別的proto3枚舉類型會(huì)被保留在消息中,但是他的表示方式會(huì)依照語言而定。int類型的字段總會(huì)保留他們的

Any

Any類型消息允許你在沒有指定他們的.proto定義的情況下使用消息作為一個(gè)嵌套類型。一個(gè)Any類型包括一個(gè)可以被序列化bytes類型的任意消息,以及一個(gè)URL作為一個(gè)全局標(biāo)識(shí)符和解析消息類型。為了使用Any類型,你需要導(dǎo)入import google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}123456

對(duì)于給定的消息類型的默認(rèn)類型URL是type.googleapis.com/packagename.messagename。

不同語言的實(shí)現(xiàn)會(huì)支持動(dòng)態(tài)庫以線程安全的方式去幫助封裝或者解封裝Any值。例如在java中,Any類型會(huì)有特殊的pack()unpack()訪問器,在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 ...
  }
}1234567891011121314

目前,用于Any類型的動(dòng)態(tài)庫仍在開發(fā)之中
如果你已經(jīng)很熟悉proto2語法,使用Any替換拓展

Oneof

如果你的消息中有很多可選字段, 并且同時(shí)至多一個(gè)字段會(huì)被設(shè)置, 你可以加強(qiáng)這個(gè)行為,使用oneof特性節(jié)省內(nèi)存.

Oneof字段就像可選字段, 除了它們會(huì)共享內(nèi)存, 至多一個(gè)字段會(huì)被設(shè)置。 設(shè)置其中一個(gè)字段會(huì)清除其它字段。 你可以使用case()或者WhichOneof() 方法檢查哪個(gè)oneof字段被設(shè)置, 看你使用什么語言了.

使用Oneof

為了在.proto定義Oneof字段, 你需要在名字前面加上oneof關(guān)鍵字, 比如下面例子的test_oneof:

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}123456

然后你可以增加oneof字段到 oneof 定義中. 你可以增加任意類型的字段, 但是不能使用repeated 關(guān)鍵字.

在產(chǎn)生的代碼中, oneof字段擁有同樣的 getters 和setters, 就像正常的可選字段一樣. 也有一個(gè)特殊的方法來檢查到底那個(gè)字段被設(shè)置. 你可以在相應(yīng)的語言API指南中找到oneof API介紹.

Oneof 特性

  • 設(shè)置oneof會(huì)自動(dòng)清楚其它oneof字段的值. 所以設(shè)置多次后,只有最后一次設(shè)置的字段有值.
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());12345
  • 如果解析器遇到同一個(gè)oneof中有多個(gè)成員,只有最會(huì)一個(gè)會(huì)被解析成消息。
  • oneof不支持repeated.
  • 反射API對(duì)oneof 字段有效.
  • 如果使用C++,需確保代碼不會(huì)導(dǎo)致內(nèi)存泄漏. 下面的代碼會(huì)崩潰, 因?yàn)?code>sub_message 已經(jīng)通過set_name()刪除了
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here1234
  • 在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());1234567

向后兼容性問題

當(dāng)增加或者刪除oneof字段時(shí)一定要小心. 如果檢查oneof的值返回None/NOT_SET, 它意味著oneof字段沒有被賦值或者在一個(gè)不同的版本中賦值了。 你不會(huì)知道是哪種情況,因?yàn)闆]有辦法判斷如果未識(shí)別的字段是一個(gè)oneof字段。

Tage 重用問題:

  • 將字段移入或移除oneof:在消息被序列號(hào)或者解析后,你也許會(huì)失去一些信息(有些字段也許會(huì)被清除)
  • 刪除一個(gè)字段或者加入一個(gè)字段:在消息被序列號(hào)或者解析后,這也許會(huì)清除你現(xiàn)在設(shè)置的oneof字段
  • 分離或者融合oneof:行為與移動(dòng)常規(guī)字段相似。

Map(映射)

如果你希望創(chuàng)建一個(gè)關(guān)聯(lián)映射,protocol buffer提供了一種快捷的語法:

map<key_type, value_type> map_field = N;1

其中key_type可以是任意Integer或者string類型(所以,除了floating和bytes的任意標(biāo)量類型都是可以的)value_type可以是任意類型。

例如,如果你希望創(chuàng)建一個(gè)project的映射,每個(gè)Projecct使用一個(gè)string作為key,你可以像下面這樣定義:

map<string, Project> projects = 3;1
  • Map的字段可以是repeated。
  • 序列化后的順序和map迭代器的順序是不確定的,所以你不要期望以固定順序處理Map
  • 當(dāng)為.proto文件產(chǎn)生生成文本格式的時(shí)候,map會(huì)按照key 的順序排序,數(shù)值化的key會(huì)按照數(shù)值排序。
  • 從序列化中解析或者融合時(shí),如果有重復(fù)的key則后一個(gè)key不會(huì)被使用,當(dāng)從文本格式中解析map時(shí),如果存在重復(fù)的key。

生成map的API現(xiàn)在對(duì)于所有proto3支持的語言都可用了,你可以從API指南找到更多信息。

向后兼容性問題

map語法序列化后等同于如下內(nèi)容,因此即使是不支持map語法的protocol buffer實(shí)現(xiàn)也是可以處理你的數(shù)據(jù)的:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;123456

當(dāng)然可以為.proto文件新增一個(gè)可選的package聲明符,用來防止不同的消息類型有命名沖突。如:

package foo.bar;
message Open { ... }12

在其他的消息格式定義中可以使用包名+消息名的方式來定義域的類型,如:

message Foo {
  ...
  required foo.bar.Open open = 1;
  ...
}12345

包的聲明符會(huì)根據(jù)使用語言的不同影響生成的代碼。

  • 對(duì)于C++,產(chǎn)生的類會(huì)被包裝在C++的命名空間中,如上例中的Open會(huì)被封裝在 foo::bar空間中; - 對(duì)于Java,包聲明符會(huì)變?yōu)閖ava的一個(gè)包,除非在.proto文件中提供了一個(gè)明確有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名稱空間中。
  • 對(duì)于javaNano包會(huì)使用Java包,除非你在你的文件中顯式的提供一個(gè)option java_package
  • 對(duì)于C#包可以轉(zhuǎn)換為PascalCase后作為名稱空間,除非你在你的文件中顯式的提供一個(gè)option csharp_namespace,例如,Open會(huì)在Foo.Bar名稱空間中

包及名稱的解析

Protocol buffer語言中類型名稱的解析與C++是一致的:首先從最內(nèi)部開始查找,依次向外進(jìn)行,每個(gè)包會(huì)被看作是其父類包的內(nèi)部類。當(dāng)然對(duì)于 (foo.bar.Baz)這樣以“.”分隔的意味著是從最外圍開始的。

ProtocolBuffer編譯器會(huì)解析.proto文件中定義的所有類型名。 對(duì)于不同語言的代碼生成器會(huì)知道如何來指向每個(gè)具體的類型,即使它們使用了不同的規(guī)則。

定義服務(wù)(Service)

如果想要將消息類型用在RPC(遠(yuǎn)程方法調(diào)用)系統(tǒng)中,可以在.proto文件中定義一個(gè)RPC服務(wù)接口,protocol buffer編譯器將會(huì)根據(jù)所選擇的不同語言生成服務(wù)接口代碼及存根。如,想要定義一個(gè)RPC服務(wù)并具有一個(gè)方法,該方法能夠接收 SearchRequest并返回一個(gè)SearchResponse,此時(shí)可以在.proto文件中進(jìn)行如下定義:

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}123

最直觀的使用protocol buffer的RPC系統(tǒng)是gRPC一個(gè)由谷歌開發(fā)的語言和平臺(tái)中的開源的PRC系統(tǒng),gRPC在使用protocl buffer時(shí)非常有效,如果使用特殊的protocol buffer插件可以直接為您從.proto文件中產(chǎn)生相關(guān)的RPC代碼。

如果你不想使用gRPC,也可以使用protocol buffer用于自己的RPC實(shí)現(xiàn),你可以從proto2語言指南中找到更多信息

還有一些第三方開發(fā)的PRC實(shí)現(xiàn)使用Protocol Buffer。參考第三方插件wiki查看這些實(shí)現(xiàn)的列表。

JSON 映射

Proto3 支持JSON的編碼規(guī)范,使他更容易在不同系統(tǒng)之間共享數(shù)據(jù),在下表中逐個(gè)描述類型。

如果JSON編碼的數(shù)據(jù)丟失或者其本身就是null,這個(gè)數(shù)據(jù)會(huì)在解析成protocol buffer的時(shí)候被表示成默認(rèn)值。如果一個(gè)字段在protocol buffer中表示為默認(rèn)值,體會(huì)在轉(zhuǎn)化成JSON的時(shí)候編碼的時(shí)候忽略掉以節(jié)省空間。具體實(shí)現(xiàn)可以提供在JSON編碼中可選的默認(rèn)值。

proto3 JSON JSON示例 注意
message object {“fBar”: v, “g”: null, …} 產(chǎn)生JSON對(duì)象,消息字段名可以被映射成lowerCamelCase形式,并且成為JSON對(duì)象鍵,null被接受并成為對(duì)應(yīng)字段的默認(rèn)值
enum string “FOO_BAR” 枚舉值的名字在proto文件中被指定
map object {“k”: v, …} 所有的鍵都被轉(zhuǎn)換成string
repeated V array [v, …] null被視為空列表
bool true, false true, false
string string “Hello World!”
bytes base64 string “YWJjMTIzIT8kKiYoKSctPUB+”
int32, fixed32, uint32 number 1, -10, 0 JSON值會(huì)是一個(gè)十進(jìn)制數(shù),數(shù)值型或者string類型都會(huì)接受
int64, fixed64, uint64 string “1”, “-10” JSON值會(huì)是一個(gè)十進(jìn)制數(shù),數(shù)值型或者string類型都會(huì)接受
float, double number 1.1, -10.0, 0, “NaN”, “Infinity” JSON值會(huì)是一個(gè)數(shù)字或者一個(gè)指定的字符串如”NaN”,”infinity”或者”-Infinity”,數(shù)值型或者字符串都是可接受的,指數(shù)符號(hào)也可以接受
Any object {“@type”: “url”, “f”: v, … } 如果一個(gè)Any保留一個(gè)特上述的JSON映射,則它會(huì)轉(zhuǎn)換成一個(gè)如下形式:{"@type": xxx, "value": yyy}否則,該值會(huì)被轉(zhuǎn)換成一個(gè)JSON對(duì)象,@type字段會(huì)被插入所指定的確定的值
Timestamp string “1972-01-01T10:00:20.021Z” 使用RFC 339,其中生成的輸出將始終是Z-歸一化啊的,并且使用0,3,6或者9位小數(shù)
Duration string “1.000340012s”, “1s” 生成的輸出總是0,3,6或者9位小數(shù),具體依賴于所需要的精度,接受所有可以轉(zhuǎn)換為納秒級(jí)的精度
Struct object { … } 任意的JSON對(duì)象,見struct.proto
Wrapper types various types 2, “2”, “foo”, true, “true”, null, 0, … 包裝器在JSON中的表示方式類似于基本類型,但是允許nulll,并且在轉(zhuǎn)換的過程中保留null
FieldMask string “f.fooBar,h” 見fieldmask.proto
ListValue array [foo, bar, …]
Value value 任意JSON值
NullValue null JSON null

選項(xiàng)

在定義.proto文件時(shí)能夠標(biāo)注一系列的options。Options并不改變整個(gè)文件聲明的含義,但卻能夠影響特定環(huán)境下處理方式。完整的可用選項(xiàng)可以在google/protobuf/descriptor.proto找到。

一些選項(xiàng)是文件級(jí)別的,意味著它可以作用于最外范圍,不包含在任何消息內(nèi)部、enum或服務(wù)定義中。一些選項(xiàng)是消息級(jí)別的,意味著它可以用在消息定義的內(nèi)部。當(dāng)然有些選項(xiàng)可以作用在域、enum類型、enum值、服務(wù)類型及服務(wù)方法中。到目前為止,并沒有一種有效的選項(xiàng)能作用于所有的類型。

如下就是一些常用的選擇:

  • java_package (文件選項(xiàng)) :這個(gè)選項(xiàng)表明生成java類所在的包。如果在.proto文件中沒有明確的聲明java_package,就采用默認(rèn)的包名。當(dāng)然了,默認(rèn)方式產(chǎn)生的 java包名并不是最好的方式,按照應(yīng)用名稱倒序方式進(jìn)行排序的。如果不需要產(chǎn)生java代碼,則該選項(xiàng)將不起任何作用。如:
option java_package = "com.example.foo";1
  • java_outer_classname (文件選項(xiàng)): 該選項(xiàng)表明想要生成Java類的名稱。如果在.proto文件中沒有明確的java_outer_classname定義,生成的class名稱將會(huì)根據(jù).proto文件的名稱采用駝峰式的命名方式進(jìn)行生成。如(foo_bar.proto生成的java類名為FooBar.java),如果不生成java代碼,則該選項(xiàng)不起任何作用。如:
option java_outer_classname = "Ponycopter";1
  • optimize_for
    

    (文件選項(xiàng)): 可以被設(shè)置為 SPEED, CODE_SIZE,或者LITE_RUNTIME。這些值將通過如下的方式影響C++及java代碼的生成:

  • SPEED (default): protocol buffer編譯器將通過在消息類型上執(zhí)行序列化、語法分析及其他通用的操作。這種代碼是最優(yōu)的。
  • CODE_SIZE: protocol buffer編譯器將會(huì)產(chǎn)生最少量的類,通過共享或基于反射的代碼來實(shí)現(xiàn)序列化、語法分析及各種其它操作。采用該方式產(chǎn)生的代碼將比SPEED要少得多, 但是操作要相對(duì)慢些。當(dāng)然實(shí)現(xiàn)的類及其對(duì)外的API與SPEED模式都是一樣的。這種方式經(jīng)常用在一些包含大量的.proto文件而且并不盲目追求速度的 應(yīng)用中。
  • LITE_RUNTIME: protocol buffer編譯器依賴于運(yùn)行時(shí)核心類庫來生成代碼(即采用libprotobuf-lite 替代libprotobuf)。這種核心類庫由于忽略了一 些描述符及反射,要比全類庫小得多。這種模式經(jīng)常在移動(dòng)手機(jī)平臺(tái)應(yīng)用多一些。編譯器采用該模式產(chǎn)生的方法實(shí)現(xiàn)與SPEED模式不相上下,產(chǎn)生的類通過實(shí)現(xiàn) MessageLite接口,但它僅僅是Messager接口的一個(gè)子集。
option optimize_for = CODE_SIZE;1
  • cc_enable_arenas(文件選項(xiàng)):對(duì)于C++產(chǎn)生的代碼啟用arena allocation
  • objc_class_prefix(文件選項(xiàng)):設(shè)置Objective-C類的前綴,添加到所有Objective-C從此.proto文件產(chǎn)生的類和枚舉類型。沒有默認(rèn)值,所使用的前綴應(yīng)該是蘋果推薦的3-5個(gè)大寫字符,注意2個(gè)字節(jié)的前綴是蘋果所保留的。
  • deprecated(字段選項(xiàng)):如果設(shè)置為true則表示該字段已經(jīng)被廢棄,并且不應(yīng)該在新的代碼中使用。在大多數(shù)語言中沒有實(shí)際的意義。在java中,這回變成@Deprecated注釋,在未來,其他語言的代碼生成器也許會(huì)在字標(biāo)識(shí)符中產(chǎn)生廢棄注釋,廢棄注釋會(huì)在編譯器嘗試使用該字段時(shí)發(fā)出警告。如果字段沒有被使用你也不希望有新用戶使用它,嘗試使用保留語句替換字段聲明。
int32 old_field = 6 [deprecated=true];1

自定義選項(xiàng)

ProtocolBuffers允許自定義并使用選項(xiàng)。該功能應(yīng)該屬于一個(gè)高級(jí)特性,對(duì)于大部分人是用不到的。如果你的確希望創(chuàng)建自己的選項(xiàng),請(qǐng)參看 Proto2 Language Guide。注意創(chuàng)建自定義選項(xiàng)使用了拓展,拓展只在proto3中可用。

生成訪問類

可以通過定義好的.proto文件來生成Java,Python,C++, Ruby, JavaNano, Objective-C,或者C# 代碼,需要基于.proto文件運(yùn)行protocol buffer編譯器protoc。如果你沒有安裝編譯器,下載安裝包并遵照README安裝。對(duì)于Go,你還需要安裝一個(gè)特殊的代碼生成器插件。你可以通過GitHub上的protobuf庫找到安裝過程

通過如下方式調(diào)用protocol編譯器:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto1
  • IMPORT_PATH聲明了一個(gè).proto文件所在的解析import具體目錄。如果忽略該值,則使用當(dāng)前目錄。如果有多個(gè)目錄則可以多次調(diào)用--proto_path,它們將會(huì)順序的被訪問并執(zhí)行導(dǎo)入。-I=IMPORT_PATH--proto_path的簡(jiǎn)化形式。

  • 當(dāng)然也可以提供一個(gè)或多個(gè)輸出路徑:

  • --cpp_out 在目標(biāo)目錄DST_DIR中產(chǎn)生C++代碼,可以在C++代碼生成參考中查看更多。
  • --java_out 在目標(biāo)目錄DST_DIR中產(chǎn)生Java代碼,可以在 Java代碼生成參考中查看更多。
  • --python_out 在目標(biāo)目錄 DST_DIR 中產(chǎn)生Python代碼,可以在Python代碼生成參考中查看更多。
  • --go_out 在目標(biāo)目錄 DST_DIR 中產(chǎn)生Go代碼,可以在GO代碼生成參考中查看更多。
  • --ruby_out在目標(biāo)目錄 DST_DIR 中產(chǎn)生Go代碼,參考正在制作中。
  • --javanano_out在目標(biāo)目錄DST_DIR中生成JavaNano,JavaNano代碼生成器有一系列的選項(xiàng)用于定制自定義生成器的輸出:你可以通過生成器的README查找更多信息,JavaNano參考正在制作中。
  • --objc_out在目標(biāo)目錄DST_DIR中產(chǎn)生Object代碼,可以在Objective-C代碼生成參考中查看更多。
  • --csharp_out在目標(biāo)目錄DST_DIR中產(chǎn)生Object代碼,可以在C#代碼生成參考中查看更多。
  • --php_out在目標(biāo)目錄DST_DIR中產(chǎn)生Object代碼,可以在PHP代碼生成參考中查看更多。

作為一個(gè)方便的拓展,如果DST_DIR以.zip或者.jar結(jié)尾,編譯器會(huì)將輸出寫到一個(gè)ZIP格式文件或者符合JAR標(biāo)準(zhǔn)的.jar文件中。注意如果輸出已經(jīng)存在則會(huì)被覆蓋,編譯器還沒有智能到可以追加文件。
- 你必須提議一個(gè)或多個(gè).proto文件作為輸入,多個(gè).proto文件可以只指定一次。雖然文件路徑是相對(duì)于當(dāng)前目錄的,每個(gè)文件必須位于其IMPORT_PATH下,以便每個(gè)文件可以確定其規(guī)范的名稱。

本文轉(zhuǎn)自:https://blog.csdn.net/u011518120/article/details/54604615

?著作權(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)容