10.02 谷歌最流行的序列化格式:Protobuf 語言指南

什麼是 Protobuf

Protobuf是Protocol Buffers的簡稱,它是Google公司開發的一種數據描述語言,用於描述一種輕便高效的結構化數據存儲格式,並於2008年對外開源。Protobuf可以用於結構化數據串行化,或者說序列化。它的設計非常適用於在網絡通訊中的數據載體,很適合做數據存儲或 RPC 數據交換格式,它序列化出來的數據量少再加上以 K-V 的方式來存儲數據,對消息的版本兼容性非常強,可用於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。開發者可以通過Protobuf附帶的工具生成代碼並實現將結構化數據序列化的功能。

Protobuf中最基本的數據單元是message,是類似Go語言中結構體的存在。在message中可以嵌套message或其它的基礎數據類型的成員。

教程中將描述如何用protocol buffer語言構造你的protocol buffer數據,包括.proto文件的語法以及如何通過.proto文件生成數據訪問類。教程中使用的是proto3版本的protocol buffer語言。

定義Message

首先看一個簡單的例子,比如說你定義一個搜索請求的message,每一個搜索請求會包含一個搜索的字符串,返回第幾頁的結果,以及結果集的大小。在.proto文件中定義如下:

谷歌最流行的序列化格式:Protobuf 語言指南

  • .proto文件的第一行指定了使用proto3語法。如果省略protocol buffer編譯器默認使用proto2語法。他必須是文件中非空非註釋行的第一行。
  • SearchRequest定義中指定了三個字段(name/value鍵值對),每個字段都會有名稱和類型。

指定字段類型

上面的例子中,所有的字段都是標量類型的兩個整型(page_number和result_per_page)和一個字符串型(query)。不過你還可以給字段指定複合類型,包括枚舉類型和其他message類型

指定字段編號

在message定義中每個字段都有一個唯一的編號,這些編號被用來在二進制消息體中識別你定義的這些字段,一旦你的message類型被用到後就不應該在修改這些編號了。注意在將message編碼成二進制消息體時字段編號1-15將會佔用1個字節,16-2047將佔用兩個字節。所以在一些頻繁使用用的message中,你應該總是先使用前面1-15字段編號。

你可以指定的最小編號是1,最大是2E29 - 1(536,870,911)。其中19000到19999是給protocol buffers實現保留的字段標號,定義message時不能使用。同樣的你也不能重複使用任何當前message定義裡已經使用過和預留的字段編號。

定義字段的規則

message的字段必須符合以下規則:

  • singular:一個遵循singular規則的字段,在一個結構良好的message消息體(編碼後的message)可以有0或1個該字段(但是不可以有多個)。這是proto3語法的默認字段規則。(這個理解起來有些晦澀,舉例來說上面例子中三個字段都是singular類型的字段,在編碼後的消息體中可以有0或者1個query字段,但不會有多個。)
  • repeated:遵循repeated規則的字段在消息體重可以有任意多個該字段值,這些值的順序在消息體重可以保持(就是數組類型的字段)

添加更多消息類型

在單個.proto文件中可以定義多個message,這在定義多個相關message時非常有用。比如說,我們定義SearchRequest對應的響應message SearchResponse ,把它加到之前的.proto文件中。

谷歌最流行的序列化格式:Protobuf 語言指南

添加註釋

.proto文件中的註釋和C,C++的註釋風格相同,使用// 和 / ... /

/* 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.
}

保留字段

當你刪掉或者註釋掉message中的一個字段時,未來其他開發者在更新message定義時就可以重用之前的字段編號。如果他們意外載入了老版本的.proto文件將會導致嚴重的問題,比如數據損壞、隱私洩露等。一種避免問題發生的方式是指定保留的字段編號和字段名稱。如果未來有人用了這些字段標識那麼在編譯時protocol buffer的編譯器會報錯。

谷歌最流行的序列化格式:Protobuf 語言指南

proto會生成什麼代碼

當使用protocol buffer編譯器編譯.proto文件時,編譯器會根據你在.proto文件中定義的message類型生成指定編程語言的代碼。生成的代碼包括訪問和設置字段值、格式化message類型到輸出流,從輸入流解析出message等。

  • For C++, the compiler generates a .h and .cc file from each .proto, with a class for each message type described in your file.
  • For Java, the compiler generates a .java file with a class for each message type, as well as a special Builderclasses for creating message class instances.
  • Python is a little different – the Python compiler generates a module with a static descriptor of each message type in your .proto, which is then used with a metaclass to create the necessary Python data access class at runtime.
  • For Go, the compiler generates a .pb.go file with a type for each message type in your file.
  • For Ruby, the compiler generates a .rb file with a Ruby module containing your message types.
  • For Objective-C, the compiler generates a pbobjc.h and pbobjc.m file from each .proto, with a class for each message type described in your file.
  • For C#, the compiler generates a .cs file from each .proto, with a class for each message type described in your file.
  • For Dart, the compiler generates a .pb.dart file with a class for each message type in your file.

標量類型

谷歌最流行的序列化格式:Protobuf 語言指南

默認值

當時一個被編碼的message體中不存在某個message定義中的singular字段時,在message體解析成的對象中,相應字段會被設置為message定義中該字段的默認值。默認值依類型而定:

  • 對於字符串,默認值為空字符串。
  • 對於字節,默認值為空字節。
  • 對於bools,默認值為false。
  • 對於數字類型,默認值為零。
  • 對於枚舉,默認值是第一個定義的枚舉值,該值必須為0。
  • 對於消息字段,未設置該字段。它的確切值取決於語言。有關詳細信息,請參閱代碼生成指南。

枚舉類型

在定義消息類型時,您可能希望其中一個字段只有一個預定義的值列表中的值。例如,假設您要為每個SearchRequest添加corpus字段,其中corpus可以是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO。您可以非常簡單地通過向消息定義添加枚舉,併為每個可能的枚舉值值添加常量來實現。

在下面的例子中,我們添加了一個名為Corpus的枚舉類型,和一個Corpus類型的字段:

谷歌最流行的序列化格式:Protobuf 語言指南

如你所見,Corpus枚舉的第一個常量映射到了0:所有枚舉定義都需要包含一個常量映射到0並且作為定義的首行,這是因為:

  • 必須有0值,這樣我們就可以將0作為枚舉的默認值。
  • proto2語法中首行的枚舉值總是默認值,為了兼容0值必須作為定義的首行。

使用其他Message類型

可以使用其他message類型作為字段的類型,假設你想在每個SearchResponse消息中攜帶類型為Result的消息,

你可以在同一個.proto文件中定義一個Result消息類型,然後在SearchResponse中指定一個Result類型的字段。

谷歌最流行的序列化格式:Protobuf 語言指南

導入消息定義

在上面的示例中,Result消息類型在與SearchResponse相同的文件中定義 - 如果要用作字段類型的消息類型已在另一個.proto文件中定義,該怎麼辦?

您可以通過導入來使用其他.proto文件中的定義。要導入另一個.proto的定義,請在文件頂部添加一個import語句:

import "myproject/other_protos.proto";

默認情況下,您只能使用直接導入的.proto文件中的定義。但是,有時你可能需要將.proto文件移動到新位置。現在,你可以在舊位置放置一個虛擬.proto文件,在文件中使用import public語法將所有導入轉發到新位置,而不是直接移動.proto文件並在一次更改中更新所有調用點。任何導入包含import public語句的proto文件的人都可以傳遞依賴導入公共依賴項。例如

谷歌最流行的序列化格式:Protobuf 語言指南

編譯器會在通過命令行參數-I或者--proto-path中指定的文件夾中搜索.proto文件,如果沒有提供編譯器會在喚其編譯器的目錄中進行搜索。通常來說你應該將--proto-path的值設置為你項目的根目錄,並對所有導入使用完全限定名稱。

使用proto2的消息類型

可以導入proto2版本的消息類型到proto3的消息類型中使用,當然也可以在proto2消息類型中導入proto3的消息類型。但是proto2的枚舉類型不能直接應用到proto3的語法中。

嵌套消息類型

消息類型可以被定義和使用在其他消息類型中,下面的例子裡Result消息被定義在SearchResponse消息中

谷歌最流行的序列化格式:Protobuf 語言指南

如果你想在外部使用定義在父消息中的子消息,使用Parent.Type引用他們

谷歌最流行的序列化格式:Protobuf 語言指南

你可以嵌套任意多層消息

谷歌最流行的序列化格式:Protobuf 語言指南

更新Message

如果一個現存的消息類型不再滿足你當前的需求--比如說你希望在消息中增加一個額外的字段--但是仍想使用由舊版的消息格式生成的代碼,不用擔心!只要記住下面的規則,在更新消息定義的同時又不破壞現有的代碼就非常簡單。

  • 不要更改任何已存字段的字段編號。
  • 如果添加了新字段,任何由舊版消息格式生成的代碼所序列化的消息,仍能被依據新消息格式生成的代碼所解析。你應該記住這些元素的默認值這些新生成的代碼就能夠正確地與由舊代碼序列化創建的消息交互了。類似的,新代碼創建的消息也能由舊版代碼解析:舊版消息(二進制)在解析時簡單地忽略了新增的字段,查看下面的未知字段章節瞭解更多。
  • 只要在更新後的消息類型中不再重用字段編號,就可以刪除該字段。你也可以重命名字段,比如說添加OBSOLETE_前綴或者將字段編號設置為reserved,這些未來其他用戶就不會意外地重用該字段編號了。

未知字段

未知字段是格式良好的協議緩衝區序列化數據,表示解析器無法識別的字段。例如,當舊二進制文件解析具有新字段的新二進制文件發送的數據時,這些新字段將成為舊二進制文件中的未知字段。

最初,proto3消息在解析期間總是丟棄未知字段,但在3.5版本中,我們重新引入了未知字段的保留以匹配proto2行為。在版本3.5及更高版本中,未知字段在解析期間保留,幷包含在序列化輸出中。

映射類型

如果你想創建一個映射作為message定義的一部分,protocol buffers提供了一個簡易便利的語法

map map_field = N;

key_type可以是任意整數或者字符串(除了浮點數和bytes以外的所有標量類型)。注意enum不是一個有效的key_type。value_type可以是除了映射以外的任意類型(意思是protocol buffers的消息體中不允許有嵌套map)。

舉例來說,假如你想創建一個名為projects的映射,每一個Project消息關聯一個字符串鍵,你可以像如下來定義:

map<string> projects = 3;
/<string>
  • 映射裡的字段不能是follow repeated規則的(意思是映射裡字段的值不能是數組)。
  • 映射裡的值是無序的,所以不能依賴映射裡元素的順序。
  • 生成.proto的文本格式時,映射按鍵排序。數字鍵按數字排序。
  • 從線路解析或合併時,如果有重複的映射鍵,則使用最後看到的鍵。從文本格式解析映射時,如果存在重複鍵,則解析可能會失敗。
  • 如果未給映射的字段指定值,字段被序列化時的行為依語言而定。在C++, Java和Python中字段類型的默認值會被序列化作為字段值,而其他語言則不會。

給Message加包名

你可以在.proto文件中添加一個可選的package符來防止消息類型之前的名稱衝突。

谷歌最流行的序列化格式:Protobuf 語言指南

在定義message的字段時像如下這樣使用package名稱

谷歌最流行的序列化格式:Protobuf 語言指南

package符對生成代碼的影響視編程語言而定

定義服務

如果想消息類型與RPC(遠程過程調用)系統一起使用,你可以在.proto文件中定義一個RPC服務接口,然後protocol buffer編譯器將會根據你選擇的編程語言生成服務接口代碼和stub,加入你要定義一個服務,它的一個方法接受SearchRequest消息返回SearchResponse消息,你可以在.proto文件中像如下示例這樣定義它:

谷歌最流行的序列化格式:Protobuf 語言指南

與protocol buffer 一起使用的最簡單的RPC系統是gRPC:一種由Google開發的語言和平臺中立的開源RPC系統。 gRPC特別適用於protocol buffer,並允許您使用特殊的protocol buffer編譯器插件直接從.proto文件生成相關的RPC代碼。

如果你不想使用gRPC,可以使用自己實現的RPC系統,更多關於實現RPC系統的細節可以在Proto2 Language Guide中找到。

JSON編解碼

Proto3支持JSON中的規範編碼,使得在系統之間共享數據變得更加容易。在下表中逐個類型地列出了編碼規則。

如果JSON編碼數據中缺少某個值,或者其值為null,則在解析為protocol buffer時,它將被解釋為相應的默認值。如果字段在protocol buffer中具有默認值,則默認情況下將在JSON編碼的數據中省略該字段以節省空間。編寫編解碼實現可以覆蓋這個默認行為在JSON編碼的輸出中保留具有默認值的字段的選項。

谷歌最流行的序列化格式:Protobuf 語言指南

生成代碼

要生成Java,Python,C ++,Go,Ruby,Objective-C或C#代碼,你需要使用.proto文件中定義的消息類型,你需要在.proto上運行protocol buffer編譯器protoc。如果尚未安裝編譯器,請下載該軟件包並按照README文件中的說明進行操作。對於Go,還需要為編譯器安裝一個特殊的代碼生成器插件:你可以在GitHub上的golang/protobuf項目中找到這個插件和安裝說明。

編譯器像下面這樣喚起:

谷歌最流行的序列化格式:Protobuf 語言指南

  • IMPORT_PATH指定了在解析import命令時去哪裡搜索.proto文件,如果忽略將在當前工作目錄進行查找,可以通過傳遞多次--proto-path參數來指定多個import目錄,他們將會按順序被編譯器搜索。-I=IMPORT_PATH是--proto_path的簡短形式。
  • 你可以提供一個或多個輸出命令:
  • --cpp_out generates C++ code in DST_DIR. See the C++ generated code referencefor more.
  • --java_out generates Java code in DST_DIR. See the Java generated code referencefor more.
  • --python_out generates Python code in DST_DIR. See the Python generated code reference for more.
  • --go_out generates Go code in DST_DIR. See the Go generated code reference for more.
  • --ruby_out generates Ruby code in DST_DIR. Ruby generated code reference is coming soon!
  • --objc_out generates Objective-C code in DST_DIR. See the Objective-C generated code reference for more.
  • --csharp_out generates C# code in DST_DIR. See the C# generated code referencefor more.
  • --php_out generates PHP code in DST_DIR. See the PHP generated code referencefor more.
  • 必須提供一個或多個.proto文件作為輸入。可以一次指定多個.proto文件。雖然文件是相對於當前目錄命名的,但每個文件必須存在於其中一個IMPORT_PATH中,以便編譯器可以確定其規範名稱。

原文鏈接:https://segmentfault.com/a/1190000020386857


分享到:


相關文章: