码迷,mamicode.com
首页 > 其他好文 > 详细

Protocol Buffer编码方式

时间:2016-05-13 01:43:30      阅读:175      评论:0      收藏:0      [点我收藏+]

标签:

[toc]
本文翻译自: https://developers.google.com/protocol-buffers/docs/encoding

注:1、本文并非逐字逐句翻译,仅仅按照原文结构,以及知识点进行翻译,文章某些顺序以及描述方式将会被本人修改。
2、阅读本文之前需要对protocol buffer有一定认识,参见protocol buffer语法

主要介绍protocol buffer的二进制编码格式。在使用protocol buffer的时候这些可以不用理会,但是这些知识会让我们了解protocol buffer的编码格式是如何影响到我们编码之后的文件大小。

一个简单的Message

现在用一个最简单的Message举例:

message Test1{
    required int32 a = 1;
}

现在我们在一个应用程序中将a设置为150。然后利用输出流序列化这些信息。当我们打开这个序列化文件的时候可以看到一共利用3个bytes表示的这个Message:

08 96 01

这时如何编码的呢?我们将稍后展示。

基于128 Varints(整型变量)

想要了解protocol buffer的编码方式,我们首先要了解什么是varints,varints是一种将整数用1个或者多个bytes表示的一种序列化方法。越小的数使用越少的bytes。

除了最后一个byte之外,每一个varint中的byte的MSB(most significant bit)都是置位(set)的,这表示还有后续byte。低7位以二进制补码(the two’s complement)的形式表示这个数。

如:数字1,仅用1个byte表示即可。所以MSB没有置位。

0000 0001

如果1个byte不够,仅仅将MSB置位是不够的,还需要知道这几个bytes之间的关系,比如7bits仅仅可以表示0~127(这里考虑非负数)。则我们需要2个bytes来表示128。那么这2个byte到底哪一个表示高位哪一个表示低位呢?

protocol buffer将这个问题描述为一个LSB群(least significant group)的问题。protocol的做法是,least significant group first。这个如何理解呢?我看看一个例子。

现在我们表示数字300,这将比1更复杂:

1010 1100 0000 0010

我们主要关注两点:

  • 首先,由于有了多个bytes因此,除了最后一个以外其余的MSB均要置位。
  • 其次,least significant group first

    010 100 000 0010//我们去掉MSB得到
    000 0010 010 100//将左右调换
    10010100//拼接(为了方便观察,删除了前面的0),这个就是300
    

    least significant group first机制相比都能领悟到:其实就是将数字的二进制补码的每7位分为一组, 低7位先输出,编码在前面,在输出下一组,依次类推。

Message结构

本小节将描述protocol如何将Message类型等信息进行编码的。

protocol的Message实际上就是一系列的key-value pair。Message的二进制形式其实是使用域的数字作为key,这个域的名字以及类型需要在译码端通过引用该Message的定义来确定。

当一个Message进行编码时,key(注意,这里是tag number)和对应的值级联输出。在译码端译码时候,解析器需要跳过那些不能识别的域(嘿嘿,靠什么识别?)。这样,我们在添加新域的时候也不会影响旧代码的使用。事实上,key保存着两个值。其一,是.proto文件中定义的tag。其二,就是线型(wire type),它是用来提供下一个值长度的信息。

可用的线型:

类型 意义 用途
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 group开始 group(已经弃用)
4 group结束 group(已经弃用)
5 32-bit fixed32, sfixed32, float

我们可以看出其实每一个key都编码成另一种整型变量,要将最后的3位用来保存线型,因此要做如下操作:
(field_number << 3) | wire_type。

现在我们再来看一个简单的例子,我们现在都知道第一个数字是Key的一个整型变量,我们先表示一个数08(去掉MSB)

000 1000

从这个数字的低三位我们取出它的线型(0),然后向右边移动三位可以得到它的域tag(1)。所以现在我们知道tag为1,接下来的数是一个varint。接下来我们就可以利用上一节中介绍的varint译码方法进行译码。拿出我们最开始的例子:

96 01 = 1001 0110  0000 0001
   → 000 0001  ++  001 0110 (丢弃MSB,按照7bits进行反转)
   → 10010110
   → 2 + 4 + 16 + 128 = 150

可以看出存储的数据是150。
可见 08 96 01表示的是保存了一个1=150的键值对。既没有保存类型,也没有保存名字。

其他类型

有符号整数(signed integers)

从前几节我们了解到,线型为0的所有protocol buffer类型都会按照varints进行编码。然而,对于有符号类型(sint32和sint64)同“标准的”整型(int32 和int64)在编码负数时有着很大差异。如果用int32或者int64编码负数,那么就会按照长度为10bytes的varint方式进行编码,因为对于编码而言,把负数按照一个大的整数来处理更有效。如果我们使用有符号整数来对负数进行编码,那么它就会按照ZigZag方式进行,这会更加有效。

ZigZag将一个有符号数映射成为一个无符号数,这个无符号数是该有符号数的绝对值形式,因此,ZigZag的varint编码长度会更加短。这种编码方式是按照“锯齿状”往返于正数和负数之间,即

 0->0
-1->1
 1->2
-2->3
 2->4

很容易看出规律,编码之前的序列是在整数和负数之间对称跳转,编码之后的序列是在正半轴递增。

我们也就可以利用一个很简单的编码方式来进行编码:
对于sint32:

(n << 1) ^ (n >> 31)

对于sint64:

(n << 1) ^ (n >> 61)

如何理解这个编码代码呢?我们通过’^’将其分为左边部分和右边部分。从映射关系可以看出,我们将(0,-1)映射到[0,1],(1,-2)映射到[2,3],相当于把正半轴的数每一个变为原来两倍,这样整个空间看起来“扩大”了一倍,因此有足够的空间存放负数。将对于绝对值n,2n+1表示-n,而2n表示n。

左边部分就是绝对值扩大一倍,而右边部分是数学右移(最高位补原来的值),判断n的最高位是不是1。

若n是负数,由于异或的关系,左边部分和右边部分的前导1会全部清零。变为非负数,最低位根据n的最高位是否为1(即n是否是负数来确定),中间部分则是将补码变为原码(思考一下这时为什么)。

在解析sint32或者sint64时,就会按照上述过程的逆过程变为原来有符号数(通过译码端引用.proto文件知道是否使用ZigZag方式)。

非整型变量

非整型变量十分简单,double和fixed64的线型是1,在解析是这个会告诉解析器需要提取一块64bits的数据。类似的float和fixed32的线型是5,这告诉解析器需要提取一块32bits数据。这些数据都是按照小端方式存放的。

string

线型2(长度确定),这个表示会将string编码成为长度+特定字符串形式。先按照varint方式编码字符串长度(n bytes),后面紧接着n个bytes的数据。

message Test2{
    required string b = 2;
}

我们现在将b的值设定为”testing”
那么就会得到如下序列:

12 07 74 65 73 74 69 6e 67

红色的序列就是UTF8编码的”testing”,这里的Key是 0x12tag=2, type=2。后面紧跟的数表示序列长度,为7。

嵌套的Message

这里我们利用本文开始处定义的test1来定义test3:

message Test3{
    required Test1 c =3;
}

下面是编码之后的序列,同样的,我们也将Test1的域a设置为150。那么我们就得到这个序列:

1a 03 08 96 01

可以看出此时编码序列的最后三个bytes和我们第一个例编码出来的相同(08 96 01)。通过对1a和03可知,嵌套的Message编码方式是和string一致的(如何区分嵌套Message和string?,也要靠.proto)。

optional和repeated元素

proto2编码一个repeated域(没有[packed=true]选项)。编码之后的序列有0个或者多个key-value对,并且使用的是同一个tag。这些repeated域中的值不用连续出现,它们可以交错出现在其它域之间。解析时,这些元素相互之间的顺序是会保存下来,但是相对于其他域的顺序将会丢失(丢就丢咯~)。而proto3会利用打包编码(packed encoding)的方式对repeated域进行编码,之后会介绍。

在proto3的非repeated域以及proto2的optional域可能会出现对于一个tag没有key-value对的情况。

通常对于一个非repeated域不会出现多于一个实例。但是,解析器是被设计为可以处理多于一个实例的情况。

  • 对于数字以及string类型:如果一个域的实例出现多次,那么解析结果是解析器看到的最后一个值。
  • 对于嵌套Message域:如果一个域的实例出现多次,那么解析器将会“合并”这些域的实例。就像在执行Message::MergeFrom方法一样。所有的标准域都会被后来的覆盖,对于一个嵌套Message来说也会进行“合并”,对于repeated域来说,多出的实例会级联起来。所以解析两个级联的Message和独立解析两个Message然后将其“融合”得到的结果是一致的。如下:
Message message;
message.ParseFromString(str1 + str2);

上面的代码和下面是等价的:

MyMessage message,message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

这一个性质有的时候挺有用的,我们可以I在不知道他们具体类型的情况下合并两个Message

packed repeated域

protocol buffer在2.1.0版本引入了packed repeated域,这个域在proto2中的声明仅仅需要在repeated域后面添加[packed=true]。在proto3里面repeated域会默认声明为packed。经过这个声明之后的repeated域编码方式与proto2中的不同(更有效了)。如果packed repeated域中没有元素,那么它将不会被编码。否则,其中所有的元素将会统一打包编码为一个线型为2(长度确定)的key-value对。其中的每一个元素都将按照正常方式在这个包中编码(但是编码中不会出现tag)。

例如:

message Test4{
    repeated int32 d=4 [packed=true];
}

假设我们已经构造了一个Test4并且,设置repeated域d的值为3,270和86942。然后proto将会编码成为如下格式:

22          //tag(域d的tag序列号为4,线型为2),注意这个是16进制数
06          //表示接下来的长度(bytes)
03          //第一个数(varint 3)
8E 02       //第二个数(varint 270)
9E A7 05    //第三个数(varin 86942)

当且仅当repeated域中的元素是数字变量(varint,32-bit或者64-bit)的时候才可以声明为packed。

虽然packed repeated域通常不会编码出超过一个key-value对。但是编码器也要准备好接收多个key-value对。这种情况下有效的域应该被级联。每一个对都必须包含所有的元素。

当一个repeated域没有被声明为packed,但是是按照packed方式编译的,那么protocol解析器也要求可以解析这个域,反之亦然。这就保证了[packed=true]选项的前后向兼容性。

域的顺序

在protocol提供的C++,JAVA,PYTHON序列化代码中,当一个Message进行序列化时,可知的域是按照tag顺序序列化写入的,这些tag序号是我们在.proto文件中定义的。这允许解析器基于tag序列进行一些优化。但是protocol解析器是要满足,可以按照任意顺序解析的要求。因为不是所有的Message都是序列化一个对象,如,有时我们会通过一个简单的级联方式“合并”两个Message。

如果一个Message有一个“不可知”域,那么java和C++应用将会在顺序的域后边,按照不确定的顺序写入。

Protocol Buffer编码方式

标签:

原文地址:http://blog.csdn.net/sylar_d/article/details/51346290

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!