ProtocolBuffer淺析
背景
ProtocolBuffer是google 定義的一種數(shù)據(jù)交換的格式,它獨(dú)立于語(yǔ)言,獨(dú)立于平臺(tái)。google 提供了多種語(yǔ)言的實(shí)現(xiàn):java、c#、c++、go 和 python,每一種實(shí)現(xiàn)都包含了相應(yīng)語(yǔ)言的編譯器以及庫(kù)文件。ProtocolBuffer類似于xml、json,不過(guò)它更小、更快、也更簡(jiǎn)單。
與Json對(duì)比
目前使用最廣泛的數(shù)據(jù)傳輸協(xié)議為JSON,JSON是一種輕量級(jí)的數(shù)據(jù)交換格式而且層次和結(jié)構(gòu)比較簡(jiǎn)單和清晰,這里主要對(duì)比一下Protocol Buffer和JSON的對(duì)比,給出優(yōu)勢(shì)和劣勢(shì):
優(yōu)勢(shì)
傳輸數(shù)據(jù)更小
序列化和反序列化更快
由于傳輸?shù)倪^(guò)程中使用的是二進(jìn)制,沒(méi)有結(jié)構(gòu)描述文件,無(wú)法解析內(nèi)容,安全性更高
劣勢(shì)
- 由于傳輸過(guò)程使用的是二進(jìn)制,自解釋性較差,需要原有的結(jié)構(gòu)描述文件才能解析
實(shí)際數(shù)據(jù)對(duì)比
序列化速度:比JSON快20-100倍
數(shù)據(jù)大小:序列化后體積小3倍
使用流程
Protocol Buffer的使用流程總體可以分為三步,如下圖所示:
根據(jù)業(yè)務(wù)創(chuàng)建并定義proto文件
使用Google Protocol Buffer 提供的工具生成對(duì)應(yīng)語(yǔ)言的源文件
將源文件拷貝到工程中,使用Protocol Buffer提供的庫(kù)序列化或反序列化數(shù)據(jù)
Android中使用
-
在項(xiàng)目根目錄的build.gradle中添加依賴:
dependencies { classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8' } -
在app的gradle中添加插件
apply plugin: 'com.google.protobuf'//添加插件 -
在app的build.gradle添加如下代碼
protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.7.1' // 也可以配置本地編譯器路徑 } generateProtoTasks { all().each { task -> task.builtins { java {}// 生產(chǎn)java源碼 } } } } -
添加proto文件的路徑
sourceSets { main { proto { srcDir 'src/main/proto' } java { srcDir 'src/main/java' } } } -
添加protobuf-java和protoc的依賴,其中protoc的依賴很重要,lite版中不需要添加
implementation 'com.google.protobuf:protobuf-java:3.7.1' implementation 'com.google.protobuf:protoc:3.7.1'
google推薦在Android項(xiàng)目中使用lite版,lite版本生成的java文件更加輕量,其配置如下:
protobuf {
//配置protoc編譯器
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'
}
//這里配置生成目錄,編譯后會(huì)在build的目錄下生成對(duì)應(yīng)的java文件
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
}
語(yǔ)法解析
簡(jiǎn)單示例
首先創(chuàng)建一個(gè).proto文件,并且在文件中聲明如下內(nèi)容:
syntax = "proto3"; //標(biāo)明當(dāng)前proto使用的版本為proto3
option java_package = "com.jon.protocol.protobuf"; //輸出的java文件包名
option java_outer_classname = "Test"; //輸出的java文件名稱
message TestRequest{
string name = 1; //名稱
int32 count = 2;
}
字段解析
在整個(gè)proto文件中數(shù)據(jù)類型分為基本類型和結(jié)構(gòu)類型,其中結(jié)構(gòu)類型主要為:
- message
- enum
- map
下面分別介紹一下不同結(jié)構(gòu)的作用及規(guī)定:
message
message表示一個(gè)結(jié)構(gòu),類似于java中類,一個(gè)proto文件中可以聲明多個(gè)message結(jié)構(gòu):
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
import
message可以引用不同proto文件中的message,只要在proto文件中的最上面聲明import即可,如下所示:
import "test.proto";
enum
enum使用很簡(jiǎn)單,直接在message中聲明enum結(jié)構(gòu)體并且將屬性聲明為對(duì)應(yīng)的enum即可:
message EnumRequest {
Corpus corpus = 1;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
}
在proto3中,enum第一個(gè)值必須為0,主要是為了和基礎(chǔ)類型的默認(rèn)值保持一致
map
map是proto3新加的,使用也很簡(jiǎn)單:
map<key_type, value_type> map_field = N;
//示例如下
message MapRequest {
map<string, TestRequest> map = 1;
}
基礎(chǔ)類型
如下
| .proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | Float | double | float | double | |
| float | float | float | float | float32 | Float | float | float | double | |
| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| uint32 | Uses variable-length encoding. | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
| uint64 | Uses variable-length encoding. | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
| sfixed32 | Always four bytes. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| sfixed64 | Always eight bytes. | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
| string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. | string | String | str/unicode[4] | string | String (UTF-8) | string | string | String |
| bytes | May contain any arbitrary sequence of bytes no longer than 232. | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | strin |
默認(rèn)值
- string:空串
- bytes:空字節(jié)
- bool:false
- 數(shù)字類型:0
- enum:默認(rèn)值是第一個(gè)元素,且值必須為0
repeated
repeated修飾的屬性類似于jsonArray,也類似于java中的List,該修飾符在格式正確的消息中可以重復(fù)任意次(包括0次)
message RepeatRequest {
repeated TestRequest requests = 1;
}
字段編號(hào)
- 字段編號(hào)從1開始,不可重復(fù)定義
- 字段編號(hào)1-15盡量保持經(jīng)常訪問(wèn)的字段使用,因?yàn)?-15編號(hào)在傳輸?shù)倪^(guò)程中只占用1個(gè)字節(jié)
字段擴(kuò)充
日常開發(fā)過(guò)程中,由于需求的變更,往往需要增加字段,這就涉及到字段的擴(kuò)充,字段擴(kuò)充需要達(dá)到一個(gè)目的:兼容
所以Protocol Buffer在字段擴(kuò)充中定義了如下規(guī)則:
不要修改已經(jīng)存在的字段標(biāo)號(hào)
-
不用的字段,可以刪除,但是編號(hào)一定不可以再次使用,建議將字段標(biāo)為廢棄,如加前綴:"OBSOLETE_", 或者標(biāo)記該字段為reserved
reserved 2;
只要記住上述規(guī)則,就能完成字段擴(kuò)充且老版本也能兼容
原理簡(jiǎn)介
Protocol Buffer 更快更小的主要原因如下:
- 數(shù)據(jù)在序列化的時(shí)候不會(huì)傳輸字段名,只會(huì)傳輸字段標(biāo)號(hào),并且沒(méi)有被設(shè)置值的字段是不會(huì)序列化和傳輸
- 采用可變長(zhǎng)度編碼,優(yōu)化數(shù)據(jù)占用
message TestRequest{
string name = 1; //名稱
int32 count = 2;
}
上面這個(gè)例子中,在序列化時(shí),"name" 、"count"的key值不會(huì)參與,由編號(hào)1、2代替,這樣在反序列化的時(shí)候直接通過(guò)編號(hào)找到對(duì)應(yīng)的key就可以。需要注意的是編號(hào)一旦確定就不可以更改,服務(wù)端和客戶端通過(guò)proto通信的時(shí)候需要提前定義號(hào)數(shù)據(jù)格式。
- 沒(méi)有賦值的key,不參與序列化
序列化時(shí)只會(huì)對(duì)賦值的key進(jìn)行序列化,沒(méi)有賦值的不參與,在反序列化的時(shí)候直接給默認(rèn)值即可 - 可變長(zhǎng)度編碼
可變長(zhǎng)度編碼,主要縮減整數(shù)占用字節(jié)實(shí)現(xiàn),例如java中int占用4個(gè)字節(jié),但是大多數(shù)情況下,我們使用的數(shù)字都比較小,使用1個(gè)字節(jié)就夠了,這就是可變長(zhǎng)度編碼完成的事 - TLV
TLV全稱為Tag-Length-Value,其中Tag表示后面數(shù)據(jù)的類型,Length不一定有,根據(jù)Tag的值確定,Value就是數(shù)據(jù)了,TLV表示數(shù)據(jù)時(shí),減少分隔符的使用,更加緊湊
T-L-V的數(shù)據(jù)存儲(chǔ)方式

其中Length不一定有,依據(jù)Tag確定,例如int類型的數(shù)據(jù)就只有Tag-Value,string類型的數(shù)據(jù)就必須是Tag-Length-Value。
數(shù)據(jù)類型
Protocol Buffer定義了如下的數(shù)據(jù)類型,其中部分?jǐn)?shù)據(jù)類型已經(jīng)不再使用:
| 類型 | 釋義 | 備注 |
|---|---|---|
| 0 | 可變長(zhǎng)度編碼 | int32 int64 uint32 uint64 sint32 sint64 bool enum |
| 1 | 64位長(zhǎng)度 | fixed64 sfixed64 double |
| 2 | value 的長(zhǎng)度 | string bytes message packed repeated fiels |
| 3 | Start Group | 廢棄 |
| 4 | End Group | 廢棄 |
| 5 | 32位長(zhǎng)度 | fixed32 sfixed32 float |
Tag
上面已經(jīng)介紹了Protocol Buffer的數(shù)據(jù)結(jié)構(gòu)及Tag的類型,但是Tag塊并不是只表示數(shù)據(jù)類型,其中數(shù)據(jù)編號(hào)也在Tag塊中,Tag的生成規(guī)則如下:
(field_number << 3) | wire_type
其中Tag塊的后3位表示數(shù)據(jù)類型,其他位表示數(shù)據(jù)編號(hào)
可變長(zhǎng)度編碼
Java中整數(shù)類型的長(zhǎng)度都是確定的,如int類型的長(zhǎng)度為4個(gè)字節(jié),可表示的整數(shù)范圍為-231——231-1,但是實(shí)際開發(fā)中用到的數(shù)字均比較小,會(huì)造成字節(jié)浪費(fèi),可變長(zhǎng)度編碼就能很好的解決這個(gè)問(wèn)題,可變長(zhǎng)度編碼規(guī)則如下:
- 字節(jié)最高位表示數(shù)據(jù)是否結(jié)束,如果最高位為1,則表示后面的字節(jié)也是該數(shù)據(jù)的一部分
舉個(gè)例子:

其中第一個(gè)字節(jié)由于最高位為1,則后面的字節(jié)也是前面的數(shù)據(jù)的一部分,第二個(gè)字節(jié)最高位為0,則表示數(shù)據(jù)計(jì)算終止,由于Protocol Buffer是低位在前,整體的轉(zhuǎn)換過(guò)程如下:

10000001 00000011 ——> 00000110000001 表示的10進(jìn)制數(shù)為:2^0 + 2^7 + 2^8 = 385 通過(guò)上面的例子可以知道一個(gè)字節(jié)表示的數(shù)的范圍0-128,上面介紹的Tag生成算法中由于后3位表示數(shù)據(jù)類型,所以Tag中1-15編號(hào)只占用1個(gè)字節(jié),所以確保編號(hào)中1-15為常用的,減少數(shù)據(jù)大小。
可變長(zhǎng)度編碼唯一的缺點(diǎn)就是當(dāng)數(shù)很大的時(shí)候int32需要占用5個(gè)字節(jié),但是從統(tǒng)計(jì)學(xué)角度來(lái)說(shuō),一般不會(huì)有這么大的數(shù).
案例分析
上面介紹了Protocol Buffer的原理,現(xiàn)在通過(guò)實(shí)例來(lái)展示分析過(guò)程,我們定義的proto文件如下:
message TestRequest{
string name = 1; //名稱
int32 count = 2;
}
其序列化后的字節(jié)數(shù)據(jù)如下:

前面介紹過(guò)Protocol Buffer的數(shù)據(jù)結(jié)構(gòu)為TLV,其中L不是必須的,根據(jù)T的類型來(lái)確定 先看下第一個(gè)字節(jié):

這里字節(jié)最高位為0,所以該Tag就用這一個(gè)字節(jié)表示,其中后3位表示類型,前面表示字段編號(hào),所以:
這里字節(jié)最高位為0,所以該Tag就用這一個(gè)字節(jié)表示,其中后3位表示類型,前面表示字段編號(hào),所以: file_num = 0001 = 1 type = 010 = 2 上面介紹過(guò)type=2,則后面有Length,按照可變長(zhǎng)度編碼規(guī)則,知道表示長(zhǎng)度的字節(jié)為:

所以Length=4,則value的長(zhǎng)度是4個(gè)字節(jié),直接取出后面4個(gè)字節(jié):

這4個(gè)字節(jié)對(duì)應(yīng)的就是test 再看下一組:

由上面的Tag知道: file_num=2 type=0 前面介紹過(guò)type=0,后面沒(méi)有Length,直接就是value,

value=1,通過(guò)上面的解析可以知道
file_num=1 value=test
file_num=2 value=1 這樣解析就結(jié)束了
上面介紹了Protocol Buffer的原理,解釋了為什么Protocol Buffer更快,更小,這里再總結(jié)一下:
序列化的時(shí)候,不序列化key的name,只序列化key的編號(hào)
序列化的時(shí)候,沒(méi)有賦值的key,不參與序列化,反序列化的時(shí)候直接使用默認(rèn)值填充
可變長(zhǎng)度編碼,減小字節(jié)占用
TLV編碼,去除沒(méi)有的符號(hào),使數(shù)據(jù)更加緊湊
參考資料:
proto3官網(wǎng)指南:https://developers.google.com/protocol-buffers/docs/proto3
protobuf-gradle-plugin:https://github.com/google/protobuf-gradle-plugin
博客:https://juejin.im/post/5dcbf630e51d451bfe5bb21b