天天看點

protobuf3 自定義option_ProtoBuf3文法指南(Protocol Buffers)_下

0.說明

ProtoBuf3文法指南,

又稱為proto3,

是谷歌的Protocol Buffers第3個版本。

本文基于官方英文版本翻譯,

加上了自己的了解少量修改,

一共分為上下兩部分。

1.Any

Any類型消息允許在沒有.proto定義的情況下,

将消息作為嵌入類型使用。

Any以bytes的形式包含任意序列化的消息,

以及充當該消息類型的全局惟一辨別符的URL

進而解析為該消息類型。

為了使用Any類型,

需要使用聲明import 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 network_error;

detail.UnpackTo(&network_error);

... processing network_error ...

}

}

目前,用于Any類型的運作庫仍在開發之中,

如果你已經很熟悉proto2文法,

可以使用Any替換任意的proto3消息,

類似proto2消息的擴充方式。

2.Oneof

如果消息中有很多可選字段,

且同時最多隻會設定一個字段,

可以使用oneof特性加強這個行為,

并且可以節省記憶體.

Oneof字段類似于正常字段,

不僅共享記憶體中的所有字段之外,

而且最多可以同時設定一個字段。

設定其中任何一個字段會清除其它成員字段。

可以使用case()或者WhichOneof()方法檢查哪個oneof字段被設定了,

這取決于選擇使用的語言。

2.1.使用Oneof

為了在.proto中定義Oneof字段,

需要在字段名稱前面加上oneof關鍵字,

比如下面例子中的test_oneof:

message SampleMessage {

oneof test_oneof {

string name = 4;

SubMessage sub_message = 9;

}

}

然後将oneof成員字段到test_oneof字段的定義中,

可以增加任意類型的字段,

但是不能使用map和repeated字段。

在生成的代碼中,

oneof字段具有與正常字段相同的getter和setter,

有一個特殊的方法來檢查oneof的哪個值被設定了,

你可以在相應的語言API參考中,

找到更多關于所選語言的oneof API介紹.

2.2.Oneof 特性

設定oneof字段會自動清楚其它oneof成員字段的值,是以設定多個字段之後,隻有最後一次設定的字段會有值。

SampleMessage message;

message.set_name("name");

CHECK(message.has_name());

message.mutable_sub_message(); // Will clear name field.

CHECK(!message.has_name());

如果解析器遇到同一個oneof中有多個成員字段,隻有最後一個字段會被解析到消息中。

oneof不支援repeated。

反射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"); // Will delete sub_message

sub_message->set_... // Crashes here

同樣在C++中,如果使用Swap()交換兩個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());

2.3.向後相容性問題

當增加或者删除oneof字段時一定要小心,

如果檢查oneof的值傳回None/NOT_SET,

它意味着oneof字段沒有被指派,

或者在一個不同的版本中指派了。

但是沒有辦法區分,

因為沒有辦法判斷未知字段是否是一個oneof字段。

2.3.1.Tag重用問題:

将字段從oneof移入或移除,在消息被序列化或者解析後,也許會失去一些資訊,有些字段也許會被清除。但是可以安全地将單個字段移動到新的oneof中,并且如果已知隻設定了一個字段,則可以移動多個字段。

删除一個oneof字段然後再添加回來:在消息被序列化或者解析後,這可能會清除現在設定的oneof字段。

分割或合并oneof:這與移動正常字段有類似的問題。

3.Map

如果你希望建立一個關聯映射,

作為資料定義的一部分,

protocol buffer提供了一種友善快捷的文法:

map map_field = N;

其中key_type可以是任意Integer或者string類型,

即除了floating和bytes的任何标量類型,

value_type可以是出來map之外的任意類型。

如果想建立一個projects的映射,

每個Projecct使用一個string作為key,

可以這樣定義:

map projects = 3;

Map字段不能是repeated。

序列化後的順序和map疊代器的順序是不确定的,是以不要依賴Map的特定順序。

當為.proto檔案生成文本格式時,Map會按照key的順序排序,數值化的key會按照數值排序。

從序列化中解析或者合并時,如果有重複的key,則隻使用最後一個key,當從文本格式中解析map時,如果存在重複的key,解析可能會失敗。

如果為Map字段提供了一個key但是沒有value,則字段序列化時的行為是依賴于語言的。在c++、Java和Python中,類型的預設值是序列化的,而在其他語言中,則不會被序列化。

生成的Map API目前可用于proto3支援的所有語言。

可以在相關的API參考中找到有關所選語言的Map API的更多資訊。

3.1.向後相容性

Map文法序列化後等同于如下内容,

是以即使是不支援Map文法的protocol buffer實作,

也是可以處理資料的:

message MapFieldEntry {

key_type key = 1;

value_type value = 2;

}

repeated MapFieldEntry map_field = N;

任何支援Map的protocol buffers實作,

都必須生成和接受上述定義的資料。

4.包

可以為.proto檔案添加一個可選的package聲明,

用來防止不同的消息類型的命名沖突:

package foo.bar;

message Open { ... }

在定義其他的消息類型時,

可以使用包名+消息名的方式來聲明字段類型:

message Foo {

...

foo.bar.Open open = 1;

...

}

包聲明生成代碼的方式取決于選擇的語言:

在C++中,生成的類包裝在C++命名空間中。例如Open将在命名空間foo::bar中。

在Java中,包聲明會變為java的一個包,除非在.proto檔案中顯式地使用java_package選項。

在Python中,包聲明會被忽略,因為Python子產品是根據它們在檔案系統中的位置來組織的。

在Go中,包聲明被用作Go包名,除非在.proto檔案中顯式地使用go_package選項。

在Ruby中,生成的類被封裝在嵌套的Ruby命名空間中,并轉換為所需的Ruby大寫樣式(首字母大寫;如果第一個字元不是一個字母,則使用PB_字首)。例如Open将在命名空間Foo::Bar中。

在C#中,包在轉換為PascalCase後被用作命名空間,除非在.proto檔案中顯式地使用csharp_namespace選項。例如Open将位于命名空間Foo.Bar中。

4.1.包和名稱解析

Protocol buffer語言中類型名稱的解析類似于c++:

首先搜尋最内層的作用域,

然後搜尋下一層的作用域,

以此類推,

每個包會被看作是其父類包的"内部"。

對于以"."開頭的意味着是從最外圍開始的。

比如".foo.bar.Baz"。

Protocol Buffer編譯器會解析.proto檔案中定義的所有類型名稱。

對于不同語言,

代碼生成器都知道如何引用具體的類型,

即使它有不同的作用域規則。

5.定義服務

如果要将消息類型用在RPC(遠端方法調用)系統中,

可以在.proto檔案中定義RPC服務接口,

protocol buffer編譯器将會根據所選的語言生成服務接口代碼及存根。

例如定義一個RPC服務并具有一個方法,

該方法能夠接收SearchRequest并傳回SearchResponse,

此時可以在.proto檔案中進行如下定義:

service SearchService {

rpc Search(SearchRequest) returns (SearchResponse);

}

與protocol buffer一起使用的最直接的RPC系統是gRPC,

它是谷歌開發的與語言和平台無關的開放源碼RPC系統。

gRPC在protocol buffer方面工作得特别好,

它允許使用一個特殊的protocol buffer編譯器插件,

從.proto檔案直接生成相關的RPC代碼。

如果不想使用gRPC,

也可以在自己的RPC實作中使用protocol buffer。

可以在Proto2語言指南中找到更多相關資訊。

還有許多正在進行的第三方項目,

使用protocol buffer開發RPC實作。

有關我們所知道的項目的連結清單,

請檢視第三方項目wiki頁面。

6.JSON映射

Proto3支援JSON的編碼規範,

使它更容易在不同系統之間共享資料,

在下表中按類型逐個描述對應編碼。

如果json編碼的資料中缺少一個值,

或者它的值為null,

這個資料在protocol buffer解析時,

會被表示成适當的預設值。

如果一個字段在protocol buffer中有預設值,

那麼預設情況下它将在Json編碼的資料中被省略,

以節省空間。

具體實作可以提供在JSON編碼中提供選項,

輸出具有預設值的字段。

proto3

JSON

JSON example

Notes

message

object

{"fooBar": v, "g": null, …}

生成JSON對象。消息字段名稱映射為小駝峰,并成為JSON對象的key。如果指定了'json_name字段選項,則指定的值将被用作key。解析器同時接受小駝峰名稱(或json_name選項指定的名稱)和原始的原型字段名稱。"null"是所有字段類型的可接受的值,并被作為對應字段類型的預設值。

enum

string

"FOO_BAR"

使用proto中指定的枚舉值的名稱。解析器同時接受枚舉名和整數值。

map

object

{"k": v, …}

所有的key都被轉換成string。

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值是一個十進制數。可以接受數字或字元串。

int64, fixed64, uint64

string

"1", "-10"

JSON值将是一個十進制字元串。可以接受數字或字元串。

float, double

number

1.1, -10.0, 0, "NaN", "Infinity"

JSON值是一個數字或特殊字元串值“NaN”、“Infinity”和“-Infinity”中的一個。可以接受數字或字元串。指數符号也被接受。-0被認為等于0。

Any

object

{"@type": "url", "f": v, … }

如果一個Any有一個特别的JSON映射,則它會轉換成如下形式:{"@type": xxx, "value": yyy}。否則,該值會被轉換成一個JSON對象,@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中的表示方式類似于原始類型,但是nulll在資料轉換和傳輸期間被允許和保留。

FieldMask

string

"f.fooBar,h"

參見field_mask.proto.

ListValue

array

[foo, bar, …]

Value

value

NullValue

null

JSON null

Empty

object

{}

空JSON對象

6.1.JSON選項

proto3的JSON實作可以提供以下選項:

帶有預設值的字段:帶有預設值的字段在proto3的JSON輸出中預設被省略。有一個選項,可以覆寫此行為,輸出字段可以攜帶預設值。

忽略未知字段:預設情況下,proto3的JSON解析器應該拒絕未知字段,但是可以在解析中提供忽略未知字段的選項。

使用原型字段名稱而不是小駝峰名稱:預設情況下,proto3的 JSON列印器應該将字段名稱轉換為小駝峰,并使用它作為JSON名稱。有一個使用原型字段名稱作為JSON名稱的選項。proto3的 JSON解析器需要同時接受轉換後的小駝峰名稱和原型字段名。

枚舉值以整數而不是字元串的形式輸出:在JSON輸出中預設使用枚舉值的名稱。有一個選項,可以使用枚舉值的數值來代替。

7.選項

proto檔案中的單個聲明可以用許多選項進行注釋。

選項不會改變聲明的總體含義,

但可能會影響在特定環境下處理聲明的方式。

可用選項的完整清單在google/protobuf/descriptor.proto中。

一些選項是檔案級别的,

這意味着它們應該在頂級作用域中編寫,

而不是在任何消息、枚舉或服務定義中。

有些選項是消息級選項,

這意味着它們應該寫入消息定義中。

有些選項是字段級選項,

這意味着它們應該寫入字段定義中。

選項也可以寫入枚舉類型、枚舉值、字段、服務類型和服務方法;

但是到目前為止,

并沒有一種選項能作用于所有的類型。

java_package(檔案選項):這個選項用于指定生成java類所在的包。如果在.proto檔案中沒有顯示的聲明java_package,就采用預設的包名。預設情況下将使用proto包(在.proto檔案中使用package關鍵字指定)。然而預設包通常不能成為好的Java包,因為它不是以反向域名開始。如果不生成Java代碼,則此選項無效。例如:

option java_package = "com.example.foo";

java_multiple_files(檔案選項):是否生成多個Java檔案,true定義的所有消息、枚舉和服務生成對應的多個類檔案,false隻有一個檔案,所有對象以内部類的形式出現。例如:

option java_multiple_files = true;

java_outer_classname(檔案選項):這個選項指定要生成Java類的名稱。如果在.proto檔案中沒有明确指定java_outer_classname,生成的class名稱将會根據.proto檔案的名稱采用駝峰命名。比如foo_bar.proto生成的java類名為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要小得多, 但操作将更慢。當然實作的類及其對外的API與SPEED模式都是一樣的。這種模式在包含大量.proto檔案的應用程式中非常有用,而且不需要把所有檔案都快速儲存下來。

LITE_RUNTIME:protocol buffer編譯器将生成隻依賴于lite運作時庫(libprotobuf-lite,而不是libprotobuf)的類。lite運作時比整個庫小得多(大約小一個數量級),但是省略了某些特性,比如描述符和反射。這對于運作在受限平台(如手機)上的應用程式尤其有用。編譯器仍然會像在SPEED模式下那樣生成所有方法的快速實作。生成的類将隻以每種語言實作MessageLite接口,它隻提供完整消息接口方法的一個子集。

option optimize_for = CODE_SIZE;

cc_enable_arenas(檔案選項):為C++生成的代碼啟用arena配置設定。

objc_class_prefix(檔案選項):設定Objective-C類的字首,該字首被前置到所有Objective-C生成的類和枚舉中。沒有預設值。按照蘋果公司的建議,應該使用3-5個大寫字母之間的字首。注意,所有兩個字母的字首都是蘋果保留的。

deprecated(字段選項):如果設定為true則表示該字段已經被廢棄,并且不應該在新的代碼中使用。在大多數語言中沒有實際的效果。在Java中,這會變成@Deprecated注解,将來其他語言的代碼生成器也許會在字辨別符中産生廢棄注解,廢棄注解會在編譯器嘗試使用該字段時發出警告。如果字段沒有被使用,也不希望新使用者使用它,可以考慮使用保留語句替換該字段聲明。

int32 old_field = 6 [deprecated = true];

7.1.自定義選項

Protocol Buffers允許定義和使用自己的選項。這是一個大多數人不需要的進階特性。如果您确實認為需要建立自己的選項,請參閱Proto2語言指南了解詳細資訊。注意建立自定義選項使用擴充,這隻允許在proto3中自定義選項。

8.生成代碼類

要生成Java、Python、C++、Go、Ruby、Objective-C或C#代碼,

可以通過在.proto檔案中定義消息、枚舉、服務等,

需要在.proto上運作protocol buffer編譯器protoc。

如果還沒有安裝編譯器,

請下載下傳安裝包并按照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 --javanano_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的簡化形式。

--ccpp_out在DST_DIR中生成C++代碼。更多資訊請參見C++生成的代碼參考。

--cjava_out在DST_DIR中生成Java代碼。更多資訊請參見Java生成的代碼參考。

--cpython_out在DST_DIR中生成Python代碼。有關更多資訊,請參閱Python生成的代碼參考。

--cgo_out在DST_DIR中生成Go代碼。更多資訊請參見Go生成的代碼參考。

--cruby_out在DST_DIR中生成Ruby代碼。Ruby生成的代碼參考即将釋出!

--cobjc_out在DST_DIR中生成Objective-C代碼。更多資訊請參見Objective-C生成的代碼參考。

--ccsharp_out在DST_DIR中生成C#代碼。更多資訊請參見C#生成的代碼參考。

--cphp_out在DST_DIR中生成PHP代碼。有關更多資訊,請參閱PHP生成的代碼參考。另外一個友善的地方是,如果DST_DIR以.zip或. JAR結尾,編譯器将輸出寫入具有給定名稱的單個zip格式存檔檔案,.JAR輸出還将根據Java JAR規範提供一個清單檔案。注意,如果輸出存檔已經存在,它将被覆寫;編譯器不夠聰明,無法向現有的歸檔檔案中添加檔案。

必須提供一個或多個.proto檔案作為輸入。可以同時指定多個.proto檔案。盡管檔案的命名是相對于目前目錄的,每個檔案必須位于其IMPORT_PATH下,這樣編譯器才能确定它的規範名稱。

9.參考文章

有疑問加站長微信聯系(非本文作者)

protobuf3 自定義option_ProtoBuf3文法指南(Protocol Buffers)_下