ProtocolBuffer淺析

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的使用流程總體可以分為三步,如下圖所示:

image
  1. 根據(jù)業(yè)務(wù)創(chuàng)建并定義proto文件

  2. 使用Google Protocol Buffer 提供的工具生成對(duì)應(yīng)語(yǔ)言的源文件

  3. 將源文件拷貝到工程中,使用Protocol Buffer提供的庫(kù)序列化或反序列化數(shù)據(jù)

Android中使用

  1. 在項(xiàng)目根目錄的build.gradle中添加依賴:

        dependencies {
            classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
        }
    
  2. 在app的gradle中添加插件

    apply plugin: 'com.google.protobuf'//添加插件
    
  3. 在app的build.gradle添加如下代碼

    protobuf {
        protoc {
            artifact = 'com.google.protobuf:protoc:3.7.1' // 也可以配置本地編譯器路徑
        }
    
        generateProtoTasks {
            all().each { task ->
                task.builtins {
                    java {}// 生產(chǎn)java源碼
                }
            }
        }
    }
    
  4. 添加proto文件的路徑

    sourceSets {
        main {
            proto {
                srcDir 'src/main/proto'
            }
            java {
                srcDir 'src/main/java'
            }
        }
    }
    
  5. 添加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)

  1. 字段編號(hào)從1開始,不可重復(fù)定義
  2. 字段編號(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ī)則:

  1. 不要修改已經(jīng)存在的字段標(biāo)號(hào)

  2. 不用的字段,可以刪除,但是編號(hào)一定不可以再次使用,建議將字段標(biāo)為廢棄,如加前綴:"OBSOLETE_", 或者標(biāo)記該字段為reserved

    reserved 2;
    

只要記住上述規(guī)則,就能完成字段擴(kuò)充且老版本也能兼容

原理簡(jiǎn)介

Protocol Buffer 更快更小的主要原因如下:

  1. 數(shù)據(jù)在序列化的時(shí)候不會(huì)傳輸字段名,只會(huì)傳輸字段標(biāo)號(hào),并且沒(méi)有被設(shè)置值的字段是不會(huì)序列化和傳輸
  2. 采用可變長(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ǔ)方式

image

其中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è)例子:


image

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

image

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ù)如下:

testproto.png

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

image1

這里字節(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é)為:

image2

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

image3

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

image4

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

image5

value=1,通過(guò)上面的解析可以知道

  1. file_num=1 value=test

  2. file_num=2 value=1 這樣解析就結(jié)束了

上面介紹了Protocol Buffer的原理,解釋了為什么Protocol Buffer更快,更小,這里再總結(jié)一下:

  1. 序列化的時(shí)候,不序列化key的name,只序列化key的編號(hào)

  2. 序列化的時(shí)候,沒(méi)有賦值的key,不參與序列化,反序列化的時(shí)候直接使用默認(rèn)值填充

  3. 可變長(zhǎng)度編碼,減小字節(jié)占用

  4. 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容