Protobuf是Google開源的一個項目,博主將會在幾篇博文中對其進行講解。
本文實例源碼github地址:https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2019Q4/20191225。
序列化和反序列化
有些時候,我們希望將對象保存到文件中,或者傳輸給其他的應用程序。比如:web網頁後端和前端的數據交互,應用程序產生的中間數據等等。
這種將數據結構或對象以某種格式轉化爲字節流的過程,稱之爲序列化
(Serialization),目的是把當前的狀態保存下來,在需要時復原數據結構或對象(序列化時不包含與對象相關聯的函數,所以後面只提數據結構)。反序列化
(Deserialization),是序列化的逆過程,讀取字節流,根據約定的格式協議,將數據結構復原。
在序列化和反序列化的過程中,需要注意的點:
- 代碼運行過程中,數據結構和對象位於內存,其中的各項數據成員可能彼此緊鄰,也可能分佈在並不連續的各個內存區域,比如指針指向的內存塊等。而文件中字節是順序存儲的,要想將數據結構保存成文件,就需要把所有的數據成員平
鋪開
,然後串接在一起; - 直接串接可能是不行的,因爲字節流中沒有天然的分界,所以在序列化時需要按照某種約定的格式(協議),以便在反序列化時知道從哪裏到哪裏是哪個數據成員,因此格式可能需要約定:指代數據成員的標識、起始位置、終止位置、長度、分隔符等。
由上可見,格式協議
是最重要的,它直接決定了序列化和反序列化的效率、字節流的大小和可讀性等。
最常見的序列化和反序列化的格式就是json格式、XML格式,他們有自己的一套完整的格式協議。而本文講解的格式是Protobuf
。
Protobuf
先貼上Protobuf的官方資料:
- Protobuf官方資料:Developer Guide
- Protobuf gitbub地址:protocolbuffers/protobuf
Protobuf簡介
Protobuf
是Google
開發的一種用於序列化結構化數據(比如Java中的Object,C中的Structure)的語言中立、平臺中立、可擴展的數據描述語言,可用於數據存儲、通信協議等方面。Protocol Buffers可以理解爲是更快、更簡單、更小的JSON或者XML,區別在於Protocol Buffers是二進制格式,而JSON和XML是文本格式。
目前protobuf支持的語言包括:C++、C#、Java、JS、OC、PHP、Ruby這七種。
相比較於XML、json,Protobuf的優點:
- 簡潔,體積小,消息大小隻需要json的10分之一,XML的20分之一;
- 速度快,解析速度比XML、json快20~100倍;
- 自動生成數據訪問類,方便應用程序的使用。Protobuf編譯器會將.proto文件編譯生成對應的數據訪問類;
- 向後兼容性好,不必破壞舊數據格式的程序就可以對數據結構進行升級。不必擔心因爲消息結構的改變而造成的大規模的代碼重構或者遷移的問題。
相對而言,Protobuf也有缺點:
- protobuf採用了二進制格式進行編碼,可讀性差;
- protobuf並非自描述的,必須要有格式定義文件(.proto 文件)。
既然Protobuf可以自動生成數據訪問類
,也就是說,只要規定了.proto
文件,可以直接生成C++的.cc文件和.h文件,可以直接生成python的.py文件,可以直接生成Java的.java文件……
那麼它是如何生成的呢?這就需要下載安裝Protobuf了。
Protobuf的下載安裝
Protobuf的release版本,下載可以移步:Protobuf release版本。
如果是Linux操作系統下,可以直接下載:protoc-3.8.0-linux-x86_64.zip。
這個版本包含了protoc二進制文件以及與protobuf一起分發的一組標準.proto文件。
進入bin文件夾,查看protoc的版本信息:
./protoc --version
如果打印出了protoc的版本信息,就表示沒有任何問題。
當然,你也可以選擇下載Protobuf的源代碼,然後通過解壓、編譯、安裝來使用它。這種方式的下載路徑爲:protobuf-3.8.0.tar.gz。
python2安裝步驟如下所示:
tar -xzf protobuf-3.8.0.tar.gz
cd protobuf-3.8.0
./configure --prefix=$INSTALL_DIR
make
make check
make install
python3安裝步驟如下所示:
tar -xzf protobuf-3.8.0.tar.gz
cd protobuf-3.8.0
./autogen.sh
./configure
make
make check
sudo make install
sudo ldconfig # refresh shared library cache.
很有可能,執行過程中會出現以下錯誤提示:
./autogen.sh: 4: ./autogen.sh: autoreconf: not found
解決辦法:執行以下命令即可。
sudo apt-get install autoconf
sudo apt-get install automake
sudo apt-get install libtool
其實推薦第一種安裝方式,在protobuf的使用過程中,一般只需要可執行文件即可。但是,如果你使用C++版本,但自己沒有Google對應的protobuf頭文件和靜態庫,還是需要第二種安裝方式。
通常情況Protobuf都安裝在/usr/local
目錄下,該目錄下包含了Protobuf的頭文件,靜態庫和動態庫文件,如果是需要使用C++版本,頭文件和靜態庫需要拷貝出來。
Protobuf的使用
在使用Protobuf之前,需要提前創建一個.proto文件。在.proto文件中,需要定義要生成的數據訪問類的成員信息等內容。然後,就可以指定該.proto文件來生成了。
如何直接生成呢?這就需要用到安裝的可執行文件了。
./protoc 指定.proto文件 --cpp_out=./
./protoc 指定.proto文件 --java_out=./
./protoc 指定.proto文件 --python_out=./
.proto語法結構
每個定義結構化數據結構體的.proto文件,也需要按照一定的結構和語法進行編寫,但這個語法是非常簡單的。
提前聲明一點,proto2和proto3的語法規則是有一定的差異和不兼容性的,需要注意。
版本聲明
在編寫.proto文件的最開始部分,需要指定.proto文件版本:
syntax = "proto2"; //聲明proto2版本(選其一)
syntax = "proto3"; //聲明proto3版本(選其一)
定義message結構
使用message定義一個消息類型,與C++、Java等高級語言對應起來就可以理解爲Class
。
每個message通常由字段修飾符、字段類型、字段名、標識號組成。
以Person爲例,在proto2
中:
message Person {
required int32 id =1;
required string name = 2;
optional int32 age = 3;
repeated string email = 4;
}
字段修飾符:只有三種字段修飾符(required、optional、repeated),且每個字段必須有字段修飾符。
- required:表示該字段的是必須設置的(該限制體現在:若在對應語言中該字段處於未被賦值/初始化的狀態,則會報錯);
- optional: 表示該字段的是可選設置的,可通過
[default=xxx]
指定一個默認值,若沒有顯示指定默認值並且該字段沒有被設置,則會使用該類型的默認值; - repeated: 表示該字段可以有多個值,一般會被編譯爲對應語言的集合類或數組。由於一些歷史原因,基本數值類型的repeated的字段並沒有被儘可能地高效編碼。在新的代碼中,用戶應該使用特殊選項
[packed=true]
來保證更高效的編碼。
字段類型:可以指定proto定義的數據類型,當然也可以指定自己定義的數據類型。proto定義的數據類型如下:
.proto類型 | C++ Type | Java Type | Python Type | Note |
---|---|---|---|---|
double | double | double | float | |
float | float | float | float | |
int32 | 使用可變長度編碼,負數編碼效率低下。如果可能具有負值,請改用sint32 | int32 | int | int |
int64 | 使用可變長度編碼,負數編碼效率低下。如果可能具有負值,請改用sint64 | int64 | long | int/long |
uint32 | 使用可變長度編碼 | uint32 | int | int/long |
uint64 | 使用可變長度編碼 | uint64 | long | int/long |
sint32 | 使用可變長度編碼 | int32 | int | int |
sint64 | 使用可變長度編碼 | int64 | long | int/long |
fixed32 | 始終爲四個字節 | uint32 | int | int/long |
fixed64 | 始終爲八個字節 | uint64 | long | int/long |
sfixed32 | 始終爲四個字節 | int32 | int | int |
sfixed64 | 始終爲八個字節 | int64 | long | int/long |
bool | bool | boolean | bool | |
string | 字符串必須始終包含UTF-8編碼或7位ASCII文本 | string | String | unicode(Python 2)/str(Python 3) |
bytes | 可以包含任意字節序列 | string | ByteString | bytes |
標識號:在消息定義中,每個字段都有唯一
的一個標識符。這些標識符是用來在消息的二進制格式中識別各個字段的,一旦開始使用就不能夠再改變。
注:[1, 15]之內的標識號在編碼的時候會佔用一個字節。[16, 2047]之內的標識號則佔用2個字節。所以應該爲那些頻繁出現的消息元素保留 [1, 15]之內的標識號。切記:要爲將來有可能添加的、頻繁出現的標識號預留一些標識號。
但是,在proto3
中,對這些規則做了一些的修改:
- 取消了required字段修飾符,optional字段修飾符可以省略;
- 移除了default選項;
- repeated字段默認採用packed編碼,即不需要明確使用[packed=true]來爲字段指定比較緊湊的packed編碼方式。
爲什麼移除default選項?
在proto3
中,字段的默認值只能根據字段類型由系統決定。也就是說,默認值全部是約定好的,而不再提供指定默認值的語法。
在proto2
中,若某字段被設置爲默認值的時候,該字段不會被序列化。這樣可以節省空間,提高效率。但這樣就無法區分某字段是根本沒賦值,還是賦值了默認值。也就是說,如果更新default默認值,會出現意想不到的問題。
爲什麼取消required字段修飾符?
因爲required是永久性
的:在將一個字段標識爲required的時候,應該特別小心。如果在某些情況下不想寫入或者發送一個required的字段,將原始該字段修飾符更改爲optional可能會遇到問題——舊版本的使用者會認爲不含該字段的消息是不完整的,從而可能會無目的的拒絕解析。
Google的一些工程師得出了一個結論:使用required弊多於利;他們更願意使用optional和repeated而不是required。當然,這個觀點並不具有普遍性。
也就是說,在proto3
中,定義同樣的message需要這樣:
message Person {
string name = 1;
string phone = 2;
string email = 3;
repeated string address = 4;
}
當然,除了proto定義的數據類型之外,還可以指定自己定義的數據類型,甚至是枚舉類型。
自己定義新的數據類型,只需要在.proto文件中定義新的message類型。枚舉類型利用enum
開頭,需要注意枚舉類型的第一個字段的標識號必須爲0。
例如,在proto2中:
syntax = "proto2";
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2;
}
message Address {
optional string country = 1;
optional string detail = 2;
}
message Person {
required int32 id =1;
required string name = 2;
optional int32 age = 3;
repeated string email = 4;
repeated PhoneNumber phone = 5;
optional Address address = 6;
}
在proto3中:
syntax = "proto3";
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
message Address {
string country = 1;
string detail = 2;
}
message Person {
int32 id =1;
string name = 2;
int32 age = 3;
repeated string email = 4;
repeated PhoneNumber phone = 5;
Address address = 6;
}
其他定義
proto可以通過導入import其他.proto文件中的定義來使用它們。即:
import proto路徑
proto可以新增一個可選的package聲明符,用來防止不同的消息類型有命名衝突。即:
package com.yngzmiao;
當然,proto除了這些定義規則之外,還有其他的規則。如message嵌套定義
、RPC服務接口
等等,一般情況下也不會使用到。需要了解的可以參考官方文檔。