今天我们就来聊一聊google protobuf
上一个项目我们的服务器和客户端采用的是google protobuf进行对象与字节码的序列化和反序列化的,至于protobuf的优点,我就不多说,大家可以看官网的介绍 【protobuf】,总之是一种跨语言,跨平台的,比xml更加,快速高效的序列化方式。
当时我们的客户端是用的unity3d游戏引擎,语言采用的是c#,虽然第三方有对c#的支持,但是每次生成都得花费大量的时间。于是我就手写了一个简单的protobuf的序列化解析器,大家可以点 这里
下面我将要说一下protobuf的核心内容。
学过编程的童鞋都知道,在日常的编程过程中一般会遇到下面的基本的数据类型:
- bool
- int
- long
- float
- double
- string
- 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占有10个字节
- 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
这样子会节约少量的字节。