【转】转自:序列化笔记之一: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. VS2012/2013编辑器问题

    1. Visual Studio 2013 'Could not evaluate Expression' Debugger Abnormality 解决办法:http://weblog.west-w ...

  2. poj 2441 Arrange the Bulls(状态压缩dp)

    Description Farmer Johnson's Bulls love playing basketball very much. But none of them would like to ...

  3. NetAnalyzer笔记 之 九 使用C#对HTTP数据还原

    [创建时间:2016-05-12 00:19:00] NetAnalyzer下载地址 在NetAnalyzer2016中加入了一个HTTP分析功能,很过用户对此都很感兴趣,那么今天写一下具体的实现方式 ...

  4. [IOI1999]花店橱窗布置(DP路径记录)

    题目:[IOI1999]花店橱窗布置 问题编号:496 题目描述 某花店现有F束花,每一束花的品种都不一样,同时至少有同样数量的花瓶,被按顺序摆成一行,花瓶的位置是固定的,从左到右按1到V顺序编号,V ...

  5. Android服务Service总结

    转自 http://blog.csdn.net/liuhe688/article/details/6874378 富貴必從勤苦得,男兒須讀五車書.唐.杜甫<柏學士茅屋> 作为程序员的我们, ...

  6. 安装sql server提示挂起报错

    在安装sql server时出现“以前的某个程序安装已在安装计算机上创建挂起的文件操作.运行安装程序之前必须重新启动计算机”错误.无法进行下去. 参考有关资料后,以下步骤基本可以解决: 1)添加/删除 ...

  7. angularJS自定义过滤器、服务和指令

    自定义过滤器 mainApp.filter('mayfilter',function(){ return function(input){ (过滤逻辑代码) } });   自定义创建指令 mainA ...

  8. WebApi2官网学习记录---单元测试

    如果没有对应的web api模板,首先使用nuget进行安装 例子1: ProductController 是以硬编码的方式使用StoreAppContext类的实例,可以使用依赖注入模式,在外部指定 ...

  9. C#上传图片同时生成缩略图,控制图片上传大小。

    #region 上传图片生成缩略图 /// <summary> /// 上传图片 /// </summary> /// <param name="sender& ...

  10. CUICatalog: Invalid asset name supplied:

    [UIImage imageNamed:name];但是这个name却是空的,所以就报了这个错了. 解决方法,在项目中搜索UIImage imageNamed:,然后打印看看所谓的name是否为空.找 ...