序列化笔记之一: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格式分析
[转]转自:序列化笔记之一:Google的Protocol Buffer格式分析 从公开介绍来看,ProtocolBuffer(PB)是google 的一种数据交换的格式,它独立于语言,独立于平台.作 ...
- Protocol Buffer 序列化原理大揭秘 - 为什么Protocol Buffer性能这么好?
前言 习惯用 Json.XML 数据存储格式的你们,相信大多都没听过Protocol Buffer Protocol Buffer 其实 是 Google出品的一种轻量 & 高效的结构化数据存 ...
- Protocol Buffer格式传输
1.简单明了介绍ProtocolBuffer 2. ProtocolBuffer(pb)所做事情其实类似于xml.json,也就是把某种数据结构的信息依照某种格式保存起来.主要用于数据存储.传输等. ...
- Android:Google出品的序列化神器Protocol Buffer使用攻略
习惯用 Json.XML 数据存储格式的你们,相信大多都没听过Protocol Buffer Protocol Buffer 其实 是 Google出品的一种轻量 & 高效的结构化数据存储格式 ...
- Google protocol buffer在windows下的编译
在caffe框架中,使用的数据格式是google的 protocol buffer.对这个不了解,所以,想简单学习一下.简单来说,Protocol Buffer 是一种轻便高效的结构化数据存储格式,可 ...
- Google protocol buffer的配置和使用(Linux&&Windows)
最近自己的服务器做到序列化这一步了,在网上看了下,序列化的工具有boost 和google的protocol buffer, protocol buffer的效率和使用程度更高效一些,就自己琢磨下把他 ...
- Protocol Buffer序列化Java框架-Protostuff
了解Protocol Buffer 首先要知道什么是Protocol Buffer,在编程过程中,当涉及数据交换时,我们往往需要将对象进行序列化然后再传输.常见的序列化的格式有JSON,XML等,这些 ...
- Protocol Buffer技术详解(Java实例)
Protocol Buffer技术详解(Java实例) 该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发 ...
- Protocol Buffer技术详解(C++实例)
Protocol Buffer技术详解(C++实例) 这篇Blog仍然是以Google的官方文档为主线,代码实例则完全取自于我们正在开发的一个Demo项目,通过前一段时间的尝试,感觉这种结合的方式比较 ...
随机推荐
- Python高手之路【六】python基础之字符串格式化
Python的字符串格式化有两种方式: 百分号方式.format方式 百分号的方式相对来说比较老,而format方式则是比较先进的方式,企图替换古老的方式,目前两者并存.[PEP-3101] This ...
- 【翻译】Awesome R资源大全中文版来了,全球最火的R工具包一网打尽,超过300+工具,还在等什么?
0.前言 虽然很早就知道R被微软收购,也很早知道R在统计分析处理方面很强大,开始一直没有行动过...直到 直到12月初在微软技术大会,看到我软的工程师演示R的使用,我就震惊了,然后最近在网上到处了解和 ...
- ShenNiu.MVC管理系统
本篇将要和大家分享的是一个简单的后台管理系统,这里先发个地址http://www.lovexins.com:8081/(登陆账号:youke,密码:123123:高级用户账号:gaoji,密码:123 ...
- 高频交易算法研发心得--MACD指标算法及应用
凤鸾宝帐景非常,尽是泥金巧样妆. 曲曲远山飞翠色:翩翩舞袖映霞裳. 梨花带雨争娇艳:芍药笼烟骋媚妆. 但得妖娆能举动,取回长乐侍君王. [摘自<封神演义>纣王在女娲宫上香时题的诗] 一首定 ...
- php批量删除
php批量删除可以实现多条或者全部数据一起删除 新建php文件 显示数据库中内容: <table width="100%" border="1" cell ...
- Windows API 设置窗口下控件Enable属性
参考页面: http://www.yuanjiaocheng.net/webapi/create-crud-api-1-put.html http://www.yuanjiaocheng.net/we ...
- EF上下文对象线程内唯一性与优化
在一次请求中,即一个线程内,若是用到EF数据上下文对象,就创建一个,这也加是很多人的代码中习惯在使用上下文对象时,习惯将对象建立在using中,也是为了尽早释放上下文对象, 但是如果有一个业务逻辑调用 ...
- winform 窗体圆角设计
网上看到的很多winform窗体圆角设计代码都比较累赘,这里分享一个少量代码就可以实现的圆角.主要运用了System.Drawing.Drawing2D. 效果图 代码如下. private void ...
- the Zen of Python---转载版
摘自译文学习区 http://article.yeeyan.org/view/legendsland/154430 The Zen of Python Python 之禅 Beautiful is b ...
- Ubuntu搭建lnmp环境
1.安装nginx 安装 sudo apt-get install nginx 服务启动.停止.重启 /etc/init.d/nginx start /usr/sbin/nginx -c /etc/n ...