细说google protobuf序列化 与相关改进方案

今天我们就来聊一聊google protobuf

上一个项目我们的服务器和客户端采用的是google protobuf进行对象与字节码的序列化和反序列化的,至于protobuf的优点,我就不多说,大家可以看官网的介绍 【protobuf】,总之是一种跨语言,跨平台的,比xml更加,快速高效的序列化方式。
当时我们的客户端是用的unity3d游戏引擎,语言采用的是c#,虽然第三方有对c#的支持,但是每次生成都得花费大量的时间。于是我就手写了一个简单的protobuf的序列化解析器,大家可以点 这里

下面我将要说一下protobuf的核心内容。

学过编程的童鞋都知道,在日常的编程过程中一般会遇到下面的基本的数据类型:
  1. bool
  2. int
  3. long
  4. float
  5. double
  6. string
  7. byte[]
在protobuf中将bool,int,long划为一类叫做varint,字面上来将就是int变量,因为bool就是int是否为0,long就是超大的int。
而protobuf整个的序列化都依赖于varint的序列化。

google protobuf采用的是128为基础的序列化。

To understand your simple protocol buffer encoding, you first need to understand varints. Varints are a method of serializing integers using one or more bytes. Smaller numbers take a smaller number of bytes.

Each byte in a varint, except the last byte, has the most significant bit (msb) set – this indicates that there are further bytes to come. The lower 7 bits of each byte are used to store the two's complement representation of the number in groups of 7 bits, least significant group first.

这是官方的解释。说白了就是每7位占有一个字节,除了最低byte之外,前面的byte都以1开头作为标志位。如下:
0~127 ---> 00000000 ~01111111

128 ---> 1|0000000 --->10000000,00000001   (2个字节)
129 ---> 1|0000001 --->10000001,00000001   (2个字节)
130 ---> 1|0000010 --->10000010,00000001   (2个字节)

......
就拿官网的300的序列化和反序列来说

300 = 00000001,00101100 (2个字节)

序列化的时候

00000001,00101100 ---> 00|0000010|0101100

将数据以7位划分。
00,0000010,0101100
第一份为0忽略,第二部分是高位与第三部分交换位置:

0101100,0000010

将交换位置之后的前几项的第一位加1,最后一项加0,得到结果如下:
10101100,00000010

就是两个字节

反序列化的时候从第一个字节解析起,如果第一位是1,则说明后面还有有效字节,知道遇到第一位不是1的字节结束

然后将每个字节的第一位减去,然后重新逆序生成原来的数据
10101100,00000010 --->0101100,0000010--->0000010,0101100 --->00000001,00101100

最后得到300,就是这么简单。

说完varint我们就可以说怎样子对消息体进行序列化了。
//  a message
message Message {
     required int32 aInt         = 1; // a field ,field number = 1
     required float aFloat       = 2;
     required double aDouble = 3;
     required string  aString   = 4;
     required bytes  aBytes     = 5; 
}
在正式的介绍序列化之前,我们先来介绍wiretype 也就是数据类型

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
一般用到的是0,1,2,5,对于这4种类型,我们将其分为2类,
第一类是不需要标明实际数据长度的:0,1,5,这些类型序列化就成了fieldnumber + wiretype + data
第二类是需要标明实际数据长度的:2,这种数据序列化之后就成了 fieldNumer + wiretype + datalength + data

每个message包括很多的field,每个field都有一个标志位,称为field number

就拿Message{
aInt = 300;
aFloat =0.5f;
aDouble = 0.5d;
aString = "hello world";
aBytes  = byte[5];
}
来说这5个field是要分别进行序列化的
第一个是个int值300 序列化之后就是10101100,00000010 两个字节,fieldNumber =1,wiretype = 0

protobuf将fieldbumber 与wiretype一同进行varint序列化,

fieldNumber <<3 | wiretype的结果进行varint
1|000 为一个字节 00001000
所以最终序列化为 00001000,10101100,00000010 3个字节

aFloat=0.5按照规则:
2<<3|101 ---> 00010101(第一个字节)
后4个字节是0.5进行float --- byte[]的序列化
一共5个字节

aDouble = 0.5
3<<3|001 ---> 00011001(第一个字节)
后8个字节是0.5进行 double ---byte[] 的序列化
一共9个字节
aString = "hello";
4<<3 | 010 ---> 00100010(第一个字节)
将"hello"进行UTF8编码 得到 byte[] data
第二部分是将 data的长度进行varint序列化比如是5 ---> 101
第三部分是data所以一共是1 + 1 + 5共7字节
byte[] 的序列化与string相同。

最后将每个field序列化之后的结果累计就成了message进行序列化的结果

在进行一系列的序列化之后就会存在一些问题
  1. 负数会浪费字节,尤其是-1占有10个字节
  2. bool值会占有1个字节,这个字节是否可以省略
为了解决以上2个问题。我想出了一个解决方案,主要是对wiretype下手

因为wiretype只有3个字节所以最多可以标识8个变量
修改之后变成

0(000) --- 正数的varint
1(001) --- 负数的varint
2(010) ---- bool的true
3(011) ---- bool的false
4(100) ---- bit32
5(101) ---- bit64
6(110) ----  Length-delimited
这样子会节约少量的字节。








你可能感兴趣的:(细说google protobuf序列化 与相关改进方案)