Protocol Buffers - Google's data interchange format Protocol Buffers 是一種輕便高效的結構化數據存儲格式,可以用於資料序列化,適合做儲存資料或 RPC 資料交換的格式,可用在通訊協定、數據儲存等領域,是一種與程式語言無關、平台無關、可擴展的序列化結構資料格式。
安裝 protobuf compiler
在 mac 可透過 macport 安裝 protobuf 的 compiler for java, cpp
sudo port install protobuf-java
安裝時會同時安裝
protobuf-cpp
如果要用 protobuf 3,則要安裝 protobuf3-java,如果已經裝了 protobuf-java,必須要先移除後才能安裝 protobuf3
sudo port uninstall protobuf-java
sudo port uninstall protobuf-cpp
sudo port install protobuf3-java
安裝時會同時安裝
maven3
maven_select
protobuf3-cpp
使用 protoc 的 command line 指令
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
ex:
protoc -I=. --java_out=. ./file.proto
.proto 資料格式文件
安裝 protobuf compiler 後,需要編寫一個 .proto 的資料格式文件,用來描述要傳遞的資料內容。
目前官方有提供 proto3 的規格,但還沒有把 tutorial 的部分由 proto2 更新到 proto3。
以下為 proto3 language guide 的內容:
Defining A Message Type 定義 message
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
如果是 proto2 就不需要寫 syntax = "proto3";
Specifying Field Types 指定資料型別
資料型別可以是 scalar type 或是 enum 或是其他 message
Assigning Tags
等號後面的的數字 field number 代表 message 中的標記欄位,不能改變。最小的field number 設為 1, 編碼後的tag佔1 byte。最大的field number 設為 2^29 - 1, 編碼後的tag佔5 bytes。19000~19999 是 protobuf 保留的 field number,不能在 .proto 中使用
field number 需要的 bytes 數量 1~15 1 16~2047 2 2^11~2^18-1 3 2^18~2^25-1 4 2^25~2^32-1 5 Specifying Field Rules
singular : 0 or 1 個值,不能超過1個
repeated : 0 or 1 or more,序列化和反序列化過程中,會保留 repeated values 的順序。在proto3中,repeated fields of scalar numeric types默認使用packed方式。
Adding More Message Types
在一個.proto文件中可以定義多個messages。
message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; } message SearchResponse { ... }
Add Comments
使用 // 註釋
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. }
Reserved Fields
如果更新一個message時,需要完全移除某個欄位,但未來可能又重新使用這個欄位標籤,這可能會在導入舊版本的.proto文件時產生嚴重的bug。因此,需要將deleted fields標記為reserved。reserved field number 和 field name 應該分開寫。
message Foo { reserved 2, 15, 9 to 11; reserved "foo", "bar"; }
What's Generated From Your .proto?
使用protobuf的編譯器protoc編譯.proto文件時,編譯器會產生與message類型相關的所選語言的代碼。包括但不限於:
- getting and setting field values
- serializing your messages to an output stream
- parsing your messages from an input stream
以 C++ 及 Java 為例:
C++ :產生1個 .h 文件和1個 .cc 文件
Java:產生1個 .java 文件,其中每個message class都包括BuilderScalar Value Types
.proto Type | Java Type | C++ Type | 備註 |
---|---|---|---|
double | double | double | |
float | float | float | |
int32 | int | int32 | 使用variable-length encoding。如果你的資料可能含有負數,那麼請使用sint32。 |
int64 | long | int64 | 使用variable-length encoding。如果你的字段可能含有負數,那麼請使用sint64。 |
uint32 | int[1] | uint32 | 使用variable-length encoding。 |
uint64 | long[1] | uint64 | 使用variable-length encoding。 |
sint32 | int | int32 | 使用variable-length encoding。對於有負數的整數,比平常使用的int32效率還好。 |
sint64 | long | int64 | 使用variable-length encoding。 |
fixed32 | int[1] | uint32 | 總是4個 bytes。如果數值總是比 228 大的話,這個資料型別會比uint32效率還好。 |
fixed64 | long[1] | uint64 | 總是8個 bytes。如果數值總是比 256 大的話,這個資料型別會比uint64效率還好。 |
sfixed32 | int | int32 | 4個 bytes |
sfixed64 | long | int64 | 8個 bytes |
bool | boolean | bool | |
string | String | string | 必須是UTF-8編碼或者7-bit ASCII編碼 |
bytes | ByteString | string | 可能包含任意順序的資料 |
Default Values
當parse一個message時,如果編碼的message不包括某個singular field,解碼的message的相應的field則被設為默認值。
- strings : empty string
- bytes : empty bytes
- bools : false
- numeric types : 0
- enums : first defined enum value, which must be 0
- message fields : 依賴於不同的語言實現。
- repeated fields : empty list
一旦一個message被解析,我們並不知道某個field是明確地被設為默認值,還是未設定從而自動被設為默認值。因此,我們定義message時,並不能依賴默認值的行為。
例如,如果你有一個bool field isMine, true表示這個方塊(是Mine),false表示這個方塊(不是Mine)。當你收到一個message isMine = false,那這個方塊到底(不是雷)還是(未設定)呢? 因此,不要依賴任何默認值的行為。更應該注意的是,如果一個field的value是默認值,將不會被序列化。Enumerations
- 第一個 enum value 必須為0。
- 必須有1個0值作為枚舉的默認值。proto2使用第一個枚舉值作為默認值,為了和proto2兼容,必須將一個枚舉值設為0。
- 可以定義aliases來使得不同的枚舉量映射到同一枚舉值。
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; }
Using Other Message Types
可在一個 message 中使用另一個 message
message SearchResponse { repeated Result results = 1; } message Result { string url = 1; string title = 2; repeated string snippets = 3; }
Nested Types
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; } } }
Importing Definitions
使用另一個.proto文件中的定義,需要import
- import : 只能直接使用,不可傳遞
- import public : 可以傳遞
Using proto2 Message Types
proto2的enum不能用在proto3中,其他的都理論上是相容的。
Updateing Message Type
更新 Message Type 定義時,要注意以下事項
- 不要修改任一個 field 的 numeric 標籤
- 增加新的 field 時,舊訊息可以用新的 code parsing,同樣的,parsing 舊訊息的程式碼還是能用來 parsing 新訊息。
- 可以移除 fields,只要沒有重用 rag number,但也可以修改欄位名稱作為標記 ex: 加上 prefix: "OBSOLETE_",或是將該欄位標記為 reserved。
- int32, uint32, int64, uint64, and bool are all compatible,也就是說可以隨時改變為這幾種資料型別中的任一種。
- sint32 and sint64 可以互換,但跟其他整數型別不相容。
- string and bytes 如果使用 UTF-8 就是相容的。
- 內嵌的訊息是跟 bytes 相容的。
- fixed32 跟 sfixed32 相容。fixed64 跟 sfixed64 相容
- enum 跟 int32, uint32, int64, and uint64 相容。
Unknown Fields
proto3 可處理未定義的欄位資料,但不同語言的實作有可能會刪除這些未知的欄位,這跟 proto2 的做法不同。Any
可使用 Any 嵌入沒有 .proto 定義的 message,Any 就是以 bytes 的方式序列化,使用 Any 必須要
import google/protobuf/any.proto
import "google/protobuf/any.proto"; message ErrorStatus { string message = 1; repeated google.protobuf.Any details = 2; }
Any 用來取代 proto2 的 extensions
Oneof
訊息只會包含多個欄位其中的某一個欄位,Oneof 不能設定為 repeated
message SampleMessage { oneof test_oneof { string name = 4; SubMessage sub_message = 9; } }
Map
map<string, Project> projects = 3;
Map 不能為 repeated,maps are sorted by key
Packages
package foo.bar; message Open { ... }
Defining Services
可定義 RPC service interface,compiler 會自動產生 code and stubs
service SearchService { rpc Search (SearchRequest) returns (SearchResponse); }
protobuf 最直覺的 RPC 系統為 gRPC: a language- and platform-neutral open source RPC system developed at Google
proto3 與 proto2 的差異
proto3 比 proto2 支持更多語言,語法更簡單,去掉了一些複雜的語法和特性。
在第一行非空白非註釋行,必須寫:
syntax = "proto3";
移除了 "required"
"repeated" 默認採用 packed 編碼;
在 proto2 中,需要明確使用 [packed=true] 來指定 packed 編碼方式
語言增加 Go、Ruby、JavaLite
移除了 default
在 proto2 中,可以使用 default 選項為某一字段指定默認值。在 proto3 中,字段的默認值只能根據資料類型由系統決定。在資料被設置為默認值的時候,該字段不會被序列化。這樣可以節省空間,提高效率。
但這樣就無法區分某資料是根本沒有數值,還是被設定為 default value。這在 proto3 中問題不大,但在 proto2 中會有問題。更新協議的時候使用 default 選項為某個字段指定了一個與原來不同的默認值,舊的程式取到的該值會與新程式不一樣。
enum 類型的第一個數值必須為 0
移除了對分組的支援
分組的功能完全可以用消息嵌套的方式來實現,並且更清晰。在 proto2 中已經把分組語法標註為"expired"了。
舊程式在解析新增字段時,會把不認識的變數丟棄,再序列化後新增的變數就不見了
在 proto2 中,舊程式雖然會忽視不認識的新增變數,但並不會將其丟棄,再序列化的時候那些字段會被原樣保留。
但官方終於同意在 proto3 中恢復 proto2 的處理方式了。
移除了對擴展的支持,新增了 Any 類型
Any 類型是用來替代 proto2 中的擴展的。目前還在開發中。
proto2 中的擴展特性很像 Swift 語言中的擴展。理解起來有點困難,使用起來更是會帶來不少混亂。
相比之下,proto3 中新增的 Any 類型有點像 C/C++ 中的 void* 。
增加了 JSON 映射特性
ref:
Protobuf 的 proto3 與 proto2 的區別
References
比起 JSON 更方便、更快速、更簡短的 Protobuf 格式