深入了解 Google Protocol Buffer

移動(dòng)應(yīng)用客戶端與服務(wù)器之間的通信協(xié)議,目前比較主流的有Facebook的Thrift,騰訊的JCE,以及Google的Protocol Buffer(以下簡(jiǎn)稱protobuf),本文主要介紹protobuf基本概念,協(xié)議解析,以及在Android中的應(yīng)用實(shí)踐。


一、Protocol Buffer 基本概念

Protobuf是一種靈活高效的,用于序列化結(jié)構(gòu)化數(shù)據(jù)的機(jī)制,類似于XML,但比XML更小,更快,更簡(jiǎn)單。Protobuf序列化為二進(jìn)制數(shù)據(jù),不依賴于平臺(tái)和語(yǔ)言,同時(shí)具備很好的兼容性。Protobuf基本使用方式如下:

1 . 編寫proto文件(applog.proto),定義數(shù)據(jù)結(jié)構(gòu)

syntax = "proto3";

package com.sohu.proto.rawlog;
option java_multiple_files = true;

message LogTime{
    optional int32 submit = 1;   
    optional int32 create = 2;  
    optional int32 test = 16; 
}

其中,optional代表該字段是否為可選的,int32代表數(shù)據(jù)類型,不同的類型對(duì)應(yīng)不同的編碼方式(wire_type),submit為字段名,1表示field_number。

2 . 使用代碼生成工具protoc,生成指定語(yǔ)言代碼

  --cpp_out=OUT_DIR           Generate C++ header and source.
  --csharp_out=OUT_DIR        Generate C# source file.
  --java_out=OUT_DIR          Generate Java source file.
  --javanano_out=OUT_DIR      Generate Java Nano source file.
  --js_out=OUT_DIR            Generate JavaScript source.
  --objc_out=OUT_DIR          Generate Objective C header and source.
  --python_out=OUT_DIR        Generate Python source file.
  --ruby_out=OUT_DIR          Generate Ruby source file.
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/applog.proto

3 . 調(diào)用Protobuf API,序列化及反序列化

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 
log.writeDelimitedTo(byteArrayOutputStream);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());  
Log log = Log.parseDelimitedFrom(byteArrayInputStream);

二、Protocol Buffer 編碼機(jī)制

1. Base 128 Varints

所謂序列化,即是將結(jié)構(gòu)化數(shù)據(jù)按一定的編碼規(guī)范轉(zhuǎn)換為指定格式的過(guò)程,protobuf使用的是Base 128 Varints的編碼方式。Varints是一種使用可變字節(jié)序列化整型的方法,越小的整數(shù)占用越小的字節(jié)數(shù),它的基本規(guī)則如下:

1 . 每個(gè)Byte的最高位(msb)是標(biāo)志位,如果該位為1,表示該Byte后面還有其它Byte,如果該位為0,表示該Byte是最后一個(gè)Byte
2 . 每個(gè)Byte的低7位是用來(lái)存數(shù)值的位
3 . Varints方法用Litte-Endian(小端)字節(jié)序

一個(gè)多位整數(shù)按照其存儲(chǔ)地址的最低或最高字節(jié)進(jìn)行排列,如果最低有效位在最高有效位的前面,則稱小端序;反之則稱大端序。

假設(shè)變量x類型為int,位于地址0x1001處,它的十六進(jìn)制為0x01234567,地址范圍為0x1001~0x1004字節(jié),其內(nèi)部排列順序依賴于機(jī)器的類型。

大端序:0x1001: 01,0x1002: 23,0x1003: 45,0x1004: 67

小端序:0x1001: 67,0x1002: 45,0x1003: 23,0x1004: 01

比如,300使用Varints序列化后的結(jié)果為1010 1100 0000 0010,其運(yùn)算過(guò)程如下:

  • 300轉(zhuǎn)化為二進(jìn)制 : 1 0010 1100
  • Byte低7位存儲(chǔ)數(shù)值 :000 0010 010 1100
  • Litte-Endian字節(jié)序 : 010 1100 000 0010
  • 添加MSB標(biāo)志位 :1010 1100 0000 0010
2. 消息結(jié)構(gòu)

protobuf每條消息都是由一系列的key-value鍵值對(duì)組成的,key和value分別采用不同的編碼方式。

key的具體值為(field_number << 3) | wire_type,也就是說(shuō),Byte第一位作為標(biāo)志位,最后三位用于存儲(chǔ)wire type(編碼數(shù)據(jù)類型),其他位用于存儲(chǔ)field_number值。

可用的wire types如下 :

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

這六種wire type,其中Start group 和 End group 已經(jīng)棄用,下面重點(diǎn)介紹Varint,64-bit,Length-delimited和32-bit。


Varint

varint是可變字節(jié)編碼類型,例如:logTime.submit = 4; logTime.create = 5; logTime.test = 6,則序列化后的數(shù)據(jù)有7個(gè)字節(jié),如下:

wire_type總共只有六種類型,因此用3bit足以表示,在一個(gè)Byte里,去掉mbs,以及3bit的wire_type,只剩下4bit來(lái)表示field_number,因此,一個(gè)Byte里,field_number只能表達(dá)0-15,如果超過(guò)15,則需要兩個(gè)或多個(gè)Byte來(lái)表示。


64-bit / 32-bit

varint適用于表達(dá)較小的整數(shù)(0-127),因?yàn)閙bs的存在,每個(gè)字節(jié)實(shí)際能用于表達(dá)數(shù)據(jù)的,只有7bit,當(dāng)數(shù)字很大的時(shí)候,protobuf定義了64-bit和32-bit兩種定長(zhǎng)編碼類型,比如2的28次方,兩種方式表達(dá)如下:

varint編碼還有個(gè)問(wèn)題在于不利于表達(dá)負(fù)數(shù),我們知道,計(jì)算機(jī)中的負(fù)數(shù)是以其補(bǔ)碼(原碼取反+1)形式存在的,比如-1,二進(jìn)制表示為:11111111 11111111 11111111 11111111,如果我們采用varint表示,則需要5個(gè)字節(jié),因?yàn)閜rotbuf在編碼格式上兼容int64,所以實(shí)際上表示-1需要用到10個(gè)字節(jié)。

為了解決這個(gè)問(wèn)題,Protobuf提供了sint32和sint64兩種數(shù)據(jù)類型。如果某個(gè)消息的某個(gè)字段出現(xiàn)負(fù)數(shù)值的可能性比較大,那么應(yīng)該使用sint32或sint64。這兩種數(shù)據(jù)類型在編碼時(shí),會(huì)先使用ZigZig編碼將負(fù)數(shù)映射成正數(shù),然后再使用Varint編碼。

Zigzig 算法如下:

(n << 1) ^ (n >> 31)    // sint32
(n << 1) ^ (n >> 63)    // sint64
Signed Original Encoded As
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

Zigzag 編碼用無(wú)符號(hào)數(shù)來(lái)表示有符號(hào)數(shù),正數(shù)和負(fù)數(shù)交錯(cuò),無(wú)論正負(fù)都可以采用較少的 Byte 來(lái)表示。


Length-delimited

Length-delimited編碼格式則會(huì)將數(shù)據(jù)的length也編碼進(jìn)最終數(shù)據(jù),使用Length-delimited編碼格式的數(shù)據(jù)類型包括string,bytes和自定義消息。

message Test2 {
  required string b = 2;
}

設(shè)置b的值為"testing",則編碼之后為:

12 07 74 65 73 74 69 6e 67

repeated / packed

optional類型是序列化單個(gè)字段,而repeated類型是把每個(gè)字段依次進(jìn)行序列化,key相同,value不同,但如果repeated的字段較多,每次都帶上相同的key則會(huì)浪費(fèi)空間,因此,protobuf提供了packed選項(xiàng),當(dāng)repeated字段設(shè)置了packed選項(xiàng),則會(huì)使用Length-delimited格式來(lái)編碼字段值。


三、Protocol Buffer 應(yīng)用實(shí)踐

protobuf主要用于客戶端與服務(wù)器之間的數(shù)據(jù)傳遞,比如日志上報(bào),長(zhǎng)連接等,在實(shí)際使用過(guò)程中,主要遇到以下三個(gè)問(wèn)題:

1. 65535 method limit

protobuf的標(biāo)準(zhǔn)實(shí)現(xiàn)會(huì)生成大量的方法數(shù),其依賴庫(kù)以及自動(dòng)生成的Java代碼,所包含的方法數(shù)在一萬(wàn)以上,因此很容易導(dǎo)致Android客戶端達(dá)到65535的方法數(shù)限制。

為了解決這個(gè)問(wèn)題,我們?cè)谏蒵ava代碼的時(shí)候,可以選擇生成protobuf 3.0 精簡(jiǎn)版(Nano):

protoc -I=$SRC_DIR --javanano_out=$DST_DIR $SRC_DIR/applog.proto

精簡(jiǎn)版使用常量代替枚舉,使用public代替private,省略了get/set方法,以及builder模式,很大程度上減少了自動(dòng)生成的Java代碼的方法數(shù)。當(dāng)然,方法數(shù)的大頭還是集中在protobuf的依賴庫(kù)文件,當(dāng)我們生成nano版本的java代碼時(shí),還需要找到其對(duì)應(yīng)的庫(kù)文件,很遺憾的是,Google官方并沒(méi)有提供protobuf 3.0 nano 版本的依賴庫(kù),以下是jcenter查詢到的文件:

protobuf-java-3.0.0-javadoc.jar
protobuf-java-3.0.0-javadoc.jar.asc
protobuf-java-3.0.0-sources.jar
protobuf-java-3.0.0-sources.jar.asc
protobuf-java-3.0.0.jar
protobuf-java-3.0.0.jar.asc
protobuf-java-3.0.0.pom
protobuf-java-3.0.0.pom.asc

為了解決依賴庫(kù)的問(wèn)題,查看protobuf源碼,找到了javanano的源文件,然后編譯成jar包,其依賴文件如下:


至此,解決了protobuf方法數(shù)問(wèn)題,使其能夠很好的用于Android客戶端與服務(wù)器數(shù)據(jù)交換。


2. Message Delimited

protobuf轉(zhuǎn)換成的二進(jìn)制數(shù)據(jù)可以直接用于網(wǎng)絡(luò)傳輸,當(dāng)傳輸一條消息的時(shí)候,沒(méi)有任何問(wèn)題,但當(dāng)一次傳輸多條消息的時(shí)候,該如何界定數(shù)據(jù)的邊界呢?

為了解決多條數(shù)據(jù)傳輸?shù)膯?wèn)題,我們?cè)诿織l消息的頭部加上數(shù)據(jù)長(zhǎng)度,那么在解析的時(shí)候,就可以通過(guò)這個(gè)長(zhǎng)度來(lái)解析對(duì)應(yīng)的二進(jìn)制數(shù)據(jù)。具體實(shí)現(xiàn)如下:

public static final int DEFAULT_BUFFER_SIZE = 4096;

static int computePreferredBufferSize(int dataLength) {
    if (dataLength > DEFAULT_BUFFER_SIZE) {
        return DEFAULT_BUFFER_SIZE;
    }
    return dataLength;
}

public static int computeUInt32SizeNoTag(final int value) {
    if ((value & (~0 <<  7)) == 0) {
        return 1;
    }
    if ((value & (~0 << 14)) == 0) {
        return 2;
    }
    if ((value & (~0 << 21)) == 0) {
        return 3;
    }
    if ((value & (~0 << 28)) == 0) {
        return 4;
    }
    return 5;
}

public static byte[] writeDelimitedTo(MessageNano log) throws IOException {
    final int serialized = log.getSerializedSize();
    final int bufferSize = computePreferredBufferSize(
                computeUInt32SizeNoTag(serialized) + serialized);
    byte[] data = new byte[bufferSize];
    com.google.protobuf.nano.CodedOutputByteBufferNano codedOutputByteBufferNano = CodedOutputByteBufferNano.newInstance(data);
    codedOutputByteBufferNano.writeRawVarint32(serialized);
    log.writeTo(codedOutputByteBufferNano);
    return data;
}

其核心思想在于,通過(guò)消息內(nèi)容每個(gè)字段的tag,type以及value,計(jì)算出整個(gè)內(nèi)容所占的字節(jié)數(shù),然后再計(jì)算出為了表示內(nèi)容長(zhǎng)度所需要用到的字節(jié)數(shù),這樣就得到了整體需要的bufferSize,最后在寫入數(shù)據(jù)的時(shí)候,先寫入消息內(nèi)容的長(zhǎng)度,再寫入消息本身的內(nèi)容。

基于此,我們?cè)诿織l消息的頭部寫入該消息長(zhǎng)度,實(shí)現(xiàn)了一次傳輸多條消息。

3. 長(zhǎng)連接protobuf編碼解碼器

目前比較主流的長(zhǎng)連接框架有Netty和mina,為了解決數(shù)據(jù)傳遞過(guò)程中粘包和拆包的問(wèn)題,Netty提供了很多Encoder和Decoder,其中也包括了對(duì)protobuf的支持,比如一個(gè)典型的通信管道如下:

ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup); 
            b.channel(NioServerSocketChannel.class);
            b.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    // Decoder
                    ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
                    ch.pipeline().addLast(new ProtobufDecoder(Log.newBuilder().getDefaultInstanceForType()));
                    // Encoder
                    ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
                    ch.pipeline().addLast(new ProtobufEncoder());
                    // Register handler
                    ch.pipeline().addLast(new ServerHandler());
                }
            });

遺憾的是,Netty只提供了標(biāo)準(zhǔn)支持,對(duì)于Android客戶端所使用的Nano版本并沒(méi)有支持,因此在客戶端,得自己實(shí)現(xiàn)一套基于Nano的編碼解碼器。為了解決這個(gè)問(wèn)題,我寫了以下幾個(gè)類:

ProtobufNanoDecoder
ProtobufNanoEncoder
ProtobufNanoUtil
ProtobufNanoVarint32FrameDecoder
ProtobufNanoVarint32LengthFieldPrepender

所幸的是,深入理解了protobuf的編碼機(jī)制,自己寫編碼解碼器也并不困難,以ProtobufNanoVarint32LengthFieldPrepender為例:

@Sharable
public class ProtobufNanoVarint32LengthFieldPrepender extends MessageToByteEncoder<ByteBuf> {

    @Override
    protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {

        int bodyLen = msg.readableBytes();
        int bufferSize = ProtobufNanoUtil.computePreferredBufferSize(
                ProtobufNanoUtil.computeUInt32SizeNoTag(bodyLen) + bodyLen);
        out.ensureWritable(bufferSize);

        byte[] data = new byte[bufferSize];
        CodedOutputByteBufferNano codedOutputByteBufferNano = CodedOutputByteBufferNano.newInstance(data);
        codedOutputByteBufferNano.writeRawVarint32(bodyLen);
        codedOutputByteBufferNano.writeRawBytes(msg.array(), 0, bodyLen);

        out.writeBytes(data);
        out.writeBytes(msg, msg.readerIndex(), bodyLen);
    }
}

寫到這里,關(guān)于protobuf也就閑聊完了,用一個(gè)比較霸氣的詞結(jié)尾:以上。

最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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