移動(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é)尾:以上。