【转】转自:序列化笔记之一:Google的Protocol Buffer格式分析

从公开介绍来看,ProtocolBuffer(PB)是google 的一种数据交换的格式,它独立于语言,独立于平台。作为一个学了多年通信的人,ProtocolBuffer在我看来是一种信源编码。所谓信源编码,就是将待传输的信源符号经过某种变换,转换成码流进行传输的这个变换过程。信源编码可分为两类:有损编码与无损编码,PB自然是属于无损编码,在无损编码中,又分为定长编码和变长编码,定长编码就是一个符号变换后的码字的比特长度是固定的,比如ASCII、Unicode都是定长编码,码字是8比特,16比特。变长编码则是将信源符号映射为不同的码字长度。典型的是Huffman编码。PB也属于这一类。

从另一个角度来看,也可以看做一种协议。无论如何,PB的信源就是整数、Float值、字符串等等程序设计中常见的变量,主要用于对象序列化。那么,如何记录一个对象的变量值呢?目前典型的格式有XML和JSON。这两种方式都有两个共同特点,即自描述特性以及文本描述。自描述是指变量名也包含在格式中。而PB则去除了这一条,同时采用二进制编码,通信底层的协议一般均为二进制,具有解析速度快、占用空间小的优点,缺点嘛,当然是缺乏可读性了。

我们来看看PB的格式是怎么设计的。

先考虑最简单的情况,要对一个整数值进行编码,怎么办?最简单的方式当然是直接把这个int值看作4字节,这4字节就作为整数的编码即可。不过这对于每个整数需要4个字节,PB于是考虑用变长编码,如果用变长编码的话,每个整数的编码长度可能不一样,如何区分边界呢?这是一个核心问题。

PB认为每个整数编码后还是整数个字节,但字节个数可能不同。整数个字节简化了一些设计,并将每个字节拿出1比特来作为边界的标记。一个字节有8比特,拿出最高位的那个比特MSB(Most Significant Bit)来,这个比特用于记录这个字节是否是编码结果的最后一个字节。如果等于1,则表示还没有到最后一个字节,否则表示到了最后一个字节,于是每个整数编码后的结果都是这样子:

0xxx xxxx表示某个整数编码后的结果是单个字节,因为MSB=0;

1xxx xxxx 0xxx xxxx表示某个整数编码后的结果是2个字节,因为前一个字节的MSB=1(编码结果未结束),后一个字节的MSB=0;

同理,三个字节、四个字节都用这种方法来表示边界。

边界弄好了,里面的内容就可以填了,xxxx这些内容填什么呢?就填整数的补码。至于什么是补码,到处都有资料。

举例:

0000 0001表示整数1;

1010 1100 0000 0010表示两个字节的结果,将两字节的MSB去掉为:0101100 0000010,PB对于多个字节的情况采用低字节优先,即后面的字节要放在高位,于是拼在一起的结果为:

00000100101100

表示300这个整数值。

整数的编码解决了,这只是一个很简单的例子,对于一个对象,里面包含多个多个变量,怎么编码呢?比如一个类的定义为:

class Test

{

int A;

float B;

string C;

double D;

}

在JSON等格式中,使用文本编码,看起来就很简单,比如:

{"A":"46","B":"13.45","C":"aaaa","D":"3.78"}

PB的设计者认为"A","B","C"等等这些变量名不应该包含在传输消息中,因为这个Test对象可能会被反复传输,每一次传输都要传输"A","B","C"这些标记,但实际上这些标记是不会变的,只有值会变,所以顶多传一次就行了,那么,PB的设计就换了一种思路,在通信双方都保持一份文档,记录了"A","B","C"的编号,比如:

"A","B","C",“D”的编号分别为1、2、3、4。于是在序列化的时候,只需要传输下面的信息:

1:"46",2:"13.45",3:"aaaa",4:"3.78"

这个例子虽然看起来并不起眼,但是程序里面很多时候变量比较长,其实还是能节省很多空间的,只要把这个信息传过去,对方本身保留了一份编号文档,于是可以反序列化了。

那么,按照这种逻辑,是不是1、2、3、4这些编号都没必要传了,直接按照某种约定顺序发过去就行了不是也可以?对方照着顺序解码即可。

但PB还是保留了1、2、3、4这些编号信息,因为某些值可能为空,没必要传递过去,甚至在程序中,一个对象中的很多变量值其实都是缺省值,或者无所谓的值,只有一部分需要传递过去,这时候,就只需要传递一部分即可。而1、2、3、4这些编号都不记录的话,就必须所有的都传递过去,反而并不节省空间。

最终,PB采用了“编号+对应变量值”的这种形式来序列化。因为编号肯定是唯一的,所以这种形式其实就是一系列Key-Value对,Key就是编号,Value就是编号对应变量的值。

编码结果就呈现为:

Key 1的编码--Value 1的编码--Key 2的编码--Value 2的编码--。。。

因为Key都是整数,所以就利用我们前面看到的整数编码。因为Key都是从1、2、3、。。开始的,所以对小整数编码结果如何短的话,就能节省空间,从前面可以看出,小整数的编码结果确实短,比如大多数小整数只占一个字节。

并且上面的编码结果也能对Key记录边界(最后一个字节的MSB=0,前面的字节MSB=1),也就是说能够知道Key的长度。Key后面跟的就是Value,那么,Value也面临和Key一样的问题,首先也需要知道Value的结果有多长,是不是也采用类似的方法呢,这样就会有些难办。比如Value如果是一个字符串,可能很长,每个字节都拿出一比特来这么弄,浪费且不说,而且字符串本身就是一个一个字节的,完全被打乱了,解码的时候速度会降低。所以Value值最好一整个的放在一起。

怎么办呢?最简单的一种思路是,关于Value长度的指示可以放在Key和Value之间。因为长度本身也是一个整数,就用前面那种方法进行编码即可,在解码时,先得到Key,然后后面跟着Value的长度,解析得到Value长度后,再解析Value值。

这种思路的编码结果就呈现为下面的形式:

Key 1的编码--Value 1长度指示的编码--Value 1的编码--Key 2的编码--Value 2长度指示的编码--Value 2的编码--。。。。

能不能更加节省呢?PB更加高明之处就在于此。通过观察可以知道,在程序设计时,很多变量都是一个整数(int,int64等等),因为前面的编码已经可以对整数进行自己定界了,如果Value是整数,就无需长度指示了,岂不是浪费了?但不指示的话,怎么知道后面是个整数呢?

PB于是把Key增加了3个比特(没错,就是3比特),记录后面的Value的类型。Value的类型在PB中称为wire_type,用3比特表示。Key的形式就成为:

(Key << 3) | wire_type

即将Key左移3位,最后3比特表示Value的类型。将这一整个东西用前面的方式编码。

因为wire_type只有3比特,所以表示的信息是粗略的,主要有以下几种:

wire type=0,表示这个Value是一个变长整数(用前面那种方式编码),比如int32, int64, uint32, uint64, sint32, sint64, bool, enum;
wire type=1,表示这个Value是一个64位的数,比如fixed64, sfixed64, double,Value为64位,8字节;(注意,int64的wire type=0,整数是变长编码的)
wire type=2,表示string, bytes, embedded messages, packed repeated fields;(这些Value的长度需要在Key后面记录下来)
wire type=3,表示groups中的Start Group,就是有一组,3表示接下来的Value是第一组;
wire type=4,表示groups中的End group;
wire type=5,表示32位固定长度的fixed32, sfixed32, float
比如,08 96 01这三个字节,因为第一个字节(08)的MSB=0,即:
0000 1000,去除MSB为:0001000。
最后三位(000)表示wire type=0,说明后面的Value是一个Varint;

而前面的0001表示整数1,表示是编号为第1个的变量;
后面的96 01,写成二进制:
1001 0110 0000 0001
可以看出,前一个字节的MSB=1,后一个字节的MSB=0,是完整的,去除掉两个MSB:
0010110 0000001
因为低字节优先,于是串起来:00000010010110=150。
这样,08 96 01这三个字节就表示第一个变量值为整数150。

另一个例子:12 07 74 65 73 74 69 6e 67

12的二进制为0001 0010,因为MSB=0,所以是最后一个字节,去除MSB:0010010,后三位010表示wire type=2,前四位0010表示第2个变量。

因为wire type=2,表示Value是string, bytes等变长流。接下来的数记录了Value的长度。

07的二进制:0000 0111,因为MSB=0,所以是最后一个字节,其值为0000111,即为7,表示Value的长度为7:,也就是后面的7个字节:74 65 73 74 69 6e 67

这7个字节假如是string,则为“testing”(ASCII码)

于是知道,传递的是第二个变量,且值为“testing”。

如果上面的例子串起来:08 96 01 12 07 74 65 73 74 69 6e 67

就表示对象的第一个整数值为150,第二个变量的字符串为“testing”。

假如用JSON的话,就类似于这样:

{"IntFlag":"150","StringFlag":"testing"}

其中,IntFlag和StringFlag假定是类的变量名,可以看出,JSON使用了40个左右的字节,而PB使用了12个字节,如果这个对象被反复传递(大多数程序一般都是这样),则总体开销很小。

至此,PB的格式基本已分析完毕。

Google的Protocol Buffer格式分析的更多相关文章

  1. 序列化笔记之一:Google的Protocol Buffer格式分析

    从公开介绍来看,ProtocolBuffer(PB)是google 的一种数据交换的格式,它独立于语言,独立于平台.作为一个学了多年通信的人,ProtocolBuffer在我看来是一种信源编码.所谓信 ...

  2. Protocol Buffer格式传输

    1.简单明了介绍ProtocolBuffer 2. ProtocolBuffer(pb)所做事情其实类似于xml.json,也就是把某种数据结构的信息依照某种格式保存起来.主要用于数据存储.传输等. ...

  3. Google protocol buffer在windows下的编译

    在caffe框架中,使用的数据格式是google的 protocol buffer.对这个不了解,所以,想简单学习一下.简单来说,Protocol Buffer 是一种轻便高效的结构化数据存储格式,可 ...

  4. Google protocol buffer的配置和使用(Linux&&Windows)

    最近自己的服务器做到序列化这一步了,在网上看了下,序列化的工具有boost 和google的protocol buffer, protocol buffer的效率和使用程度更高效一些,就自己琢磨下把他 ...

  5. Protocol Buffer技术详解(Java实例)

    Protocol Buffer技术详解(Java实例) 该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发 ...

  6. Protocol Buffer技术详解(C++实例)

    Protocol Buffer技术详解(C++实例) 这篇Blog仍然是以Google的官方文档为主线,代码实例则完全取自于我们正在开发的一个Demo项目,通过前一段时间的尝试,感觉这种结合的方式比较 ...

  7. iOS 开发之 protocol Buffer 数据交换

    前言: 从 14 年公司做项目时开始接触 Google 的 protocol Buffer,用了一段时间,后来到新公司就没有机会再使用了,趁着还没完全忘记,记录下. 简介:protocolbuffer ...

  8. [翻译]Protocol Buffer 基础: C++

    目录 Protocol Buffer Basics: C++ 为什么使用 Protocol Buffers 在哪可以找到示例代码 定义你的协议格式 编译你的 Protocol Buffers Prot ...

  9. Protocol Buffer序列化Java框架-Protostuff

    了解Protocol Buffer 首先要知道什么是Protocol Buffer,在编程过程中,当涉及数据交换时,我们往往需要将对象进行序列化然后再传输.常见的序列化的格式有JSON,XML等,这些 ...

随机推荐

  1. poj2752 Seek the Name, Seek the Fame

    Description The little cat is so famous, that many couples tramp over hill and dale to Byteland, and ...

  2. 【hihoCoder第十四周】无间道之并查集

    就是基础的并查集.0代表合并操作,1代表查询操作.一开始以为会卡路径压缩,忐忑的交了一版裸并查集,结果AC了.数据还是很水的. 以后坚持做hiho,当额外的练习啦~ #include <bits ...

  3. html5+css3中的background: -moz-linear-gradient 用法 (转载)

    转载至-->http://www.cnblogs.com/smile-ls/archive/2013/06/03/3115599.html 在CSS中background: -moz-linea ...

  4. 同一台电脑启动两个或多个tomcat

    今天要在机子的tomcat上部署新的项目,需要访问的端口为80,与之前不同. 但要求不能更改原tomcat部署项目的端口,因为该tomcat内的项目正在对外使用中,且不能断开服务器. 那么,我就需要再 ...

  5. [Cycle.js] Introducing run() and driver functions

    Currently the code looks like : // Logic (functional) function main() { return { DOM: Rx.Observable. ...

  6. UITableView滑动按钮的操作

    一.开题  首先先创建工程, 创建工程的步骤就不一一介绍了, 前面有提过, 接下来是要在VC上创建一个tableview当然你也可以选择一个类继承于UITableView两者都可以, 这要看个人喜欢了 ...

  7. 给UIImage添加蒙版

    http://stackoverflow.com/questions/17448102/ios-masking-an-image-keeping-retina-scale-factor-in-acco ...

  8. 动态绑定GridView数据源遇到问题

    1.GridView中的Button控件响应Command事件的时候出现System.ArgumentException: 回发或回调参数无效, 设置<pages enableEventVali ...

  9. MySQL的一些语法总结

    初学MySQL,今天遇到了一个问题,然后汇总了一下MySQL的一些语法 1. date和datetime类型是不同的 date只记录日期(包括年月日),datetime记录日期和时间(包括年月日时分秒 ...

  10. OWIN启动项的检测

    OWIN启动项的检测 通过以下方法设置启动项: 命名约定 Katana在命名空间内查找StartUp类 OwinStartup Attribute [assembly: OwinStartup(typ ...