序列化笔记之一: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项目,通过前一段时间的尝试,感觉这种结合的方式比较 ...
随机推荐
- react-router 组件式配置与对象式配置小区别
1. react-router 对象式配置 和 组件式配置 组件式配置(Redirect) ----对应---- 对象式配置(onEnter钩子) IndexRedirect -----对应-- ...
- 可爱的豆子——使用Beans思想让Python代码更易维护
title: 可爱的豆子--使用Beans思想让Python代码更易维护 toc: false comments: true date: 2016-06-19 21:43:33 tags: [Pyth ...
- Java中Comparable与Comparator的区别
相同 Comparable和Comparator都是用来实现对象的比较.排序 要想对象比较.排序,都需要实现Comparable或Comparator接口 Comparable和Comparator都 ...
- <译>通过PowerShell工具跨多台服务器执行SQL脚本
有时候,当我们并没有合适的第三方工具(大部分需要付费)去管理多台数据库服务器,那么如何做最省力.省心呢?!Powershell一个强大的工具,可以很方便帮到我们处理日常的数据库维护工作 .简单的几步搞 ...
- pdo的使用
PHP 数据对象 (PDO) 扩展为PHP访问数据库定义了一个轻量级的一致接口. PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据. PDO随 ...
- FullCalendar日历插件说明文档
FullCalendar提供了丰富的属性设置和方法调用,开发者可以根据FullCalendar提供的API快速完成一个日历日程的开发,本文将FullCalendar的常用属性和方法.回调函数等整理成中 ...
- Android—万能ListView适配器
ListView是开发中最常用的控件了,但是总是会写重复的代码,浪费时间又没有意义. 最近参考一些资料,发现一个万能ListView适配器,代码量少,节省时间,总结一下分享给大家. 首先有一个自定义的 ...
- Android手机相册的布局
实现类似下面的这种布局的方法
- 敏捷转型历程 - Sprint3 Grooming
我: Tech Leader 团队:团队成员分布在两个城市,我所在的城市包括我有4个成员,另外一个城市包括SM有7个成员.另外由于我们的BA离职了,我暂代IT 的PO 职位.PM和我在一个城市,但他不 ...
- postgresql无法安装pldbgapi的问题
要对函数进行调试需要安装插件pldbgapi,当初在windows上面的postgresql实例中执行了一下语句就安装上了: create extension pldbgapi; 但是在linux中执 ...