Google的Protocol Buffer格式分析
【转】转自:序列化笔记之一: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格式分析的更多相关文章
- 序列化笔记之一:Google的Protocol Buffer格式分析
从公开介绍来看,ProtocolBuffer(PB)是google 的一种数据交换的格式,它独立于语言,独立于平台.作为一个学了多年通信的人,ProtocolBuffer在我看来是一种信源编码.所谓信 ...
- Protocol Buffer格式传输
1.简单明了介绍ProtocolBuffer 2. ProtocolBuffer(pb)所做事情其实类似于xml.json,也就是把某种数据结构的信息依照某种格式保存起来.主要用于数据存储.传输等. ...
- Google protocol buffer在windows下的编译
在caffe框架中,使用的数据格式是google的 protocol buffer.对这个不了解,所以,想简单学习一下.简单来说,Protocol Buffer 是一种轻便高效的结构化数据存储格式,可 ...
- Google protocol buffer的配置和使用(Linux&&Windows)
最近自己的服务器做到序列化这一步了,在网上看了下,序列化的工具有boost 和google的protocol buffer, protocol buffer的效率和使用程度更高效一些,就自己琢磨下把他 ...
- Protocol Buffer技术详解(Java实例)
Protocol Buffer技术详解(Java实例) 该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发 ...
- Protocol Buffer技术详解(C++实例)
Protocol Buffer技术详解(C++实例) 这篇Blog仍然是以Google的官方文档为主线,代码实例则完全取自于我们正在开发的一个Demo项目,通过前一段时间的尝试,感觉这种结合的方式比较 ...
- iOS 开发之 protocol Buffer 数据交换
前言: 从 14 年公司做项目时开始接触 Google 的 protocol Buffer,用了一段时间,后来到新公司就没有机会再使用了,趁着还没完全忘记,记录下. 简介:protocolbuffer ...
- [翻译]Protocol Buffer 基础: C++
目录 Protocol Buffer Basics: C++ 为什么使用 Protocol Buffers 在哪可以找到示例代码 定义你的协议格式 编译你的 Protocol Buffers Prot ...
- Protocol Buffer序列化Java框架-Protostuff
了解Protocol Buffer 首先要知道什么是Protocol Buffer,在编程过程中,当涉及数据交换时,我们往往需要将对象进行序列化然后再传输.常见的序列化的格式有JSON,XML等,这些 ...
随机推荐
- 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 ...
- 【hihoCoder第十四周】无间道之并查集
就是基础的并查集.0代表合并操作,1代表查询操作.一开始以为会卡路径压缩,忐忑的交了一版裸并查集,结果AC了.数据还是很水的. 以后坚持做hiho,当额外的练习啦~ #include <bits ...
- html5+css3中的background: -moz-linear-gradient 用法 (转载)
转载至-->http://www.cnblogs.com/smile-ls/archive/2013/06/03/3115599.html 在CSS中background: -moz-linea ...
- 同一台电脑启动两个或多个tomcat
今天要在机子的tomcat上部署新的项目,需要访问的端口为80,与之前不同. 但要求不能更改原tomcat部署项目的端口,因为该tomcat内的项目正在对外使用中,且不能断开服务器. 那么,我就需要再 ...
- [Cycle.js] Introducing run() and driver functions
Currently the code looks like : // Logic (functional) function main() { return { DOM: Rx.Observable. ...
- UITableView滑动按钮的操作
一.开题 首先先创建工程, 创建工程的步骤就不一一介绍了, 前面有提过, 接下来是要在VC上创建一个tableview当然你也可以选择一个类继承于UITableView两者都可以, 这要看个人喜欢了 ...
- 给UIImage添加蒙版
http://stackoverflow.com/questions/17448102/ios-masking-an-image-keeping-retina-scale-factor-in-acco ...
- 动态绑定GridView数据源遇到问题
1.GridView中的Button控件响应Command事件的时候出现System.ArgumentException: 回发或回调参数无效, 设置<pages enableEventVali ...
- MySQL的一些语法总结
初学MySQL,今天遇到了一个问题,然后汇总了一下MySQL的一些语法 1. date和datetime类型是不同的 date只记录日期(包括年月日),datetime记录日期和时间(包括年月日时分秒 ...
- OWIN启动项的检测
OWIN启动项的检测 通过以下方法设置启动项: 命名约定 Katana在命名空间内查找StartUp类 OwinStartup Attribute [assembly: OwinStartup(typ ...