Protocol Buffer技术详解(数据编码)

之前已经发了三篇有关Protocol Buffer的技术博客,其中第一篇介绍了Protocol Buffer的语言规范,而后两篇则分别基于C++和Java给出了一些相对比较实用而又简单的示例。由于近期工作压力很大,因此对于是否继续写本篇博客也确实让我纠结了几天。但每每想到善终如始则无败事这句话时,最终的决定还是既然开始了,就要尽自己最大的努力去做,而不要留有丝毫的遗憾。
      该篇Blog的内容将完全取自于Google的官方文档,只是为一些相对难以理解的技术点加入了适当的注解。但因技术能力有限,如解释有误,欢迎指正。
      这是一篇让你对Protocol Buffer知其然亦知其所以然的文档,即便你在并不了解这其中的技术细节和处理机制的情况下,仍然能够在你的应用程序中正常的使用Protocol Buffer,然而我相信,通过对这些细节和机制的深入了解,不仅可以让你更好的使用和驾驭Protocol Buffer,而且还能深深地感受到Google工程师的智慧和高超的编程技艺,因此在我看来,深入的研习对我们编程能力的提高和思路的拓宽都是大有裨益的。不积跬步无以致千里。
    
      一、简单消息编码布局:
      让我们先看一下下面的消息定义示例:
      message Test1 {
          required int32 a = 1;
      }
      假设我们在应用程序中将字段a的值设置为150(十进制),此后再将该对象序列化到Binary文件中,你可以看到文件的数据为:
      08 96 01
      这3个字节的含义又是什么呢?它们又是按照什么样的编码规则生成的呢?让我们拭目以待。
    
      二、Base 128 Varints:
      在理解Protocol Buffer的编码规则之前,你首先需要了解varints。varints是一种使用一个或多个字节表示整型数据的方法。其中数值本身越小,其所占用的字节数越少。
      在varint中,除了最后一个字节之外的每个字节中都包含一个msb(most significant bit)设置(使用最高位),这意味着其后的字节是否和当前字节一起来表示同一个整型数值。而字节中的其余七位将用于存储数据本身。由此我们可以简单的解释一下Base 128,通常而言,整数数值都是由字节表示,其中每个字节为8位,即Base 256。然而在Protocol Buffer的编码中,最高位成为了msb,只有后面的7位存储实际的数据,因此我们称其为Base 128(2的7次方)。
      比如数字1,它本身只占用一个字节即可表示,所以它的msb没有被设置,如:
      0000 0001
      再比如十进制数字300,它的编码后表示形式为:
      1010 1100 0000 0010
      对于Protocol Buffer而言又是如何将上面的字节布局还原成300呢?这里我们需要做的第一步是drop掉每个字节的msb。从上例中可以看出第一个字节(1010 1100)的msb(最高位)被设置为1,这说明后面的字节将连同该字节表示同一个数值,而第二个字节(0000 0010)的msb为0,因此该字节将为表示该数值的最后一个字节了,后面如果还有其他的字节数据,将表示其他的数据。
      1010 1100 0000 0010
      -> 010 1100 000 0010
      上例中的第二行已经将第一行中每一个字节的msb去除。由于Protocol Buffer是按照Little Endian的方式进行数据布局的,因此我们这里需要将两个字节的位置进行翻转。
      010 1100 000 0010
      -> 000 0010 010 1100           //翻转第一行的两个字节
      -> 100101100                         //将翻转后的两个字节直接连接并去除高位0
      -> 256 + 32 + 8 + 4 = 300    //将上一行的二进制数据换算成十进制,其值为300
    
      三、消息结构:
      Protocol Buffer中的消息都是由一系列的键值对构成的。每个消息的二进制版本都是使用标签号作为key,而每一个字段的名字和类型均是在解码的过程中根据目标类型(反序列化后的对象类型)进行配对的。在进行消息编码时,key/value被连接成字节流。在解码时,解析器可以直接跳过不识别的字段,这样就可以保证新老版本消息定义在新老程序之间的兼容性,从而有效的避免了使用older消息格式的older程序在解析newer程序发来的newer消息时,一旦遇到未知(新添加的)字段时而引发的解析和对象初始化的错误。最后,我们介绍一下字段标号和字段类型是如何进行编码的。下面先列出Protocol Buffer可以支持的字段类型。

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

由于在编码后每一个字段的key都是varint类型,key的值是由字段标号和字段类型合成编码所得,其公式如下:
      field_number << 3 | field_type
      由此看出,key的最后3个bits用于存储字段的类型信息。那么在使用该编码时,Protocol Buffer所支持的字段类型将不会超过8种。这里我们可以进一步计算出Protocol Buffer在一个消息中可以支持的字段数量为2的29次方减一。现在我们再来回顾一下之前给出的Test1消息被序列化后的第一个字节08的由来。
      0000 1000
      -> 000 1000                  //drop掉msb(最高位)
      最低的3位表示字段类型,即0为varint。我们再将结果右移3位( >> 3),此时得到的结果为1,即字段a在消息Test1中的标签号。通过这样的结果,Protocol Buffer的解码器可以获悉当前字段的标签号是1,其后所跟随数据的类型为varint。现在我们可以继续利用上面讲到的知识分析出后两个字节(96 01)的由来。
      96 01 = 1001 0110 0000 0001
          -> 001 0110 000 0001   //drop两个字节的msb
          -> 000 0001 001 0110   //翻转高低字节
          -> 10010110                   //去掉最高位中没用的0
          -> 128 + 16 + 4 + 2 = 150
    
      四、更多的值类型:
      1. 有符号整型
      如前所述,类型0表示varint,其中包含int32/int64/uint32/uint64/sint32/sint64/bool/enum。在实际使用中,如果当前字段可以表示为负数,那么对于int32/int64和sint32/sint64而言,它们在进行编码时将存在着较大的差别。如果使用int32/int64表示一个负数,该字段的值无论是-1还是-2147483648,其编码后长度将始终为10个字节,就如同对待一个很大的无符号整型一样。反之,如果使用的是sint32/sint64,Protocol Buffer将会采用ZigZag编码方式,其编码后的结果将会更加高效。
      这里简单讲述一下ZigZag编码,该编码会将有符号整型映射为无符号整型,以便绝对值较小的负数仍然可以有较小的varint编码值,如-1。下面是ZigZag对照表:

Signed Original Encoded As
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

其公式为:
      (n << 1) ^ (n >> 31)    //sint32
      (n << 1> ^ (n >> 63)   //sint64
      需要补充说明的是,Protocol Buffer在实现上述位移操作时均采用的算术位移,因此对于(n >> 31)和(n >> 63)而言,如果n为负值位移后的结果就是-1,否则就是0。
      注:简单解释一下C语言中的算术位移和逻辑位移。他们的左移操作都是相同的,即低位补0,高位直接移除。不同的是右移操作,逻辑位移比较简单,高位全部补0。而算术位移则需要视当前值的符号位而定,补进的位和符号位相同,即正数全补0,负数全补1。换句话说,算术位移右移时要保证符号位的一致性。在C语言中,如果使用 int变量位移时就是算术位移,uint变量位移时是逻辑位移。
      2. Non-varint数值型
      double/fixed64始终都占用8个字节,float/fixed32始终占用4个字节。
      3. Strings
      其类型值为2,key信息之后是字节数组的长度信息,最后在紧随指定长度的实际数据值信息。如:
      message Test2 {
          required string b = 2;
      }
      现在我们设置b的值为"testing"。其编码后数据如下:
      12 07 74 65 73 74 69 6E 67
      第一个字节0x12表示key,通过解码可以得到字段类型2和字段标号2。第二个字节07表示testing的长度。后面7个红色高亮的字节则表示testing。
    
      五、嵌入消息:
      这里是一个包含嵌入消息的消息定义。
      message Test3 {
          required Test1 c = 3;
      }
      此时我们先将Test1的a字段值设置为150,其编码结果如下:
      1A 03 08 96 01
      从上面的结果可以看出08 96 01和之前直接编码Test1时是完全一致的,只是在前面增加了key(字段类型 + 标号)和长度信息。新增信息的解码方式和含义与前面的Strings完全相同,这里不再重复解释了。
    
      六、Packed Repeated Fields:
      Protocol Buffer从2.1.0版本开始引入了[pack = true]的字段级别选项。如果设置该选项,那么元素数量为0的repeated字段将不会被编码,否则数组中的所有元素会被编码成一个单一的key/value形式。毕竟数组中的每一个元素都具有相同的字段类型和标号。该编码形式,对包含较小值的整型元素而言,优化后的编码结果可以节省更多的空间。如:
      message Test4 {
          repeated int32 d = 4 [pack=true];
      }
      这里我们假设d字段包含3个元素,值分别为3,270,86942。编码结果如下:
      22             //key (字段标号4,类型为2)
      06             //数据中所有元素所占用的字节数量
      03             //第一个元素(varint 3)
      8E 02        //第二个元素(varint 270)
      9E A7 05  //第三个元素(varint 86942)
    
      七、字段顺序: 
      在.proto文件中定义消息的字段标号时,可以是不连续的,但是如果将其定义为连续递增的数值,将获得更好的编码和解码性能。
    
      结束语:
      本篇博客是Protocol Buffer技术详解系列的最后一篇博客,同时该系列博客又将是开源学习之旅系列主题中的第一个系列,希望今后能够借此平台与大家进行更多的技术交流,共同提高。如有意见或问题,欢迎留言。

Protocol Buffer技术详解(数据编码)的更多相关文章

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

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

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

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

  3. Protocol Buffer技术详解(语言规范)

    Protocol Buffer技术详解(语言规范) 该系列Blog的内容主体主要源自于Protocol Buffer的官方文档,而代码示例则抽取于当前正在开发的一个公司内部项目的Demo.这样做的目的 ...

  4. 《CDN技术详解》 - CDN知多少?

    开发时间久了,就会接触到性能和并发方面的问题,如果说,在自己还是菜鸟的时候完全不用理会这种问题或者说有其他的高手去处理这类问题,那么,随着经验的丰富起来,自己必须要独立去处理了.或者,知道思路也行,毕 ...

  5. IPv6技术详解:基本概念、应用现状、技术实践(下篇)

    本文来自微信技术架构部的原创技术分享. 1.前言 在上篇<IPv6技术详解:基本概念.应用现状.技术实践(上篇)>,我们讲解了IPV6的基本概念. 本篇将继续从以下方面展开对IPV6的讲解 ...

  6. IPv6技术详解:基本概念、应用现状、技术实践(上篇)

    本文来自微信技术架构部的原创技术分享. 1.前言 普及IPV6喊了多少年了,连苹果的APP上架App Store也早已强制IPV6的支持,然并卵,因为历史遗留问题,即使在IPV4地址如果饥荒的情况下, ...

  7. DNS技术和NAT技术详解

    DNS技术和NAT技术详解一.DNS(Domain Name System)1.什么是DNS2. 了解域名3.域名解析过程4.使用dig工具分析DNS过程5.浏览器输入URL后发生什么事?二.ICMP ...

  8. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  9. 「视频直播技术详解」系列之七:直播云 SDK 性能测试模型

    ​关于直播的技术文章不少,成体系的不多.我们将用七篇文章,更系统化地介绍当下大热的视频直播各环节的关键技术,帮助视频直播创业者们更全面.深入地了解视频直播技术,更好地技术选型. 本系列文章大纲如下: ...

随机推荐

  1. Java系列: 关于LinkedList的 ListIterator的add和remove

    static void testListIteratorAdd(){ LinkedList<String> strList = new LinkedList<String>() ...

  2. [CareerCup] 2.4 Partition List 划分链表

    2.4 Write code to partition a linked list around a value x, such that all nodes less than x come bef ...

  3. 浪潮之巅IT那点事之三——神奇的规律

    “道可道,非常道”是老子在<道德经>中的开篇第一句话,这句话的意思是:万事万物其真理是可以探索并道说得出来的,但这些真理并非是永恒的,天道轮转,没有永恒不变的真理(来自百度百科).在IT行 ...

  4. 三种实例化委托的方式(C# 编程指南)

    1.定义的委托和方法 delegate void TestDelegate(string s); static void M(string s) { Console.WriteLine(s); } 2 ...

  5. GDB代码调试与使用

    GDB代码调试与使用 Linux下GDB调试代码 源代码 编译生成执行文件 gcc -g test.c -o test 使用GDB调试 启动GDB:gdb test 从第一行列出源代码:list 直接 ...

  6. python实现简易数据库之三——join多表连接和group by分组

    上一篇里面我们实现了单表查询和top N查询,这一篇我们来讲述如何实现多表连接和group by分组. 一.多表连接 多表连接的时间是数据库一个非常耗时的操作,因为连接的时间复杂度是M*N(M,N是要 ...

  7. [网站公告]23:00-05:00阿里云SLB升级会造成4-8次每次10秒的闪断

    大家好,阿里云将于今天夜里(7月29日23:00-7月30日05:00)对负载均衡服务(SLB)进行升级操作,升级期间我们使用的SLB实例会有4-8次的闪断,每次闪断时间10秒左右.闪断期间会造成网站 ...

  8. ASP.NET MVC系列 框架搭建(一)之仓储层的搭建

    大神勿喷,小神默默学. 会了就是不值一提的东西,不会就是绝对的高大上. 最后上传源码.希望能给读者带来一些新的认识及知识. 还没上过头条..各位大神,请点支持一下小弟. 陆续更新.更新到你会为止!! ...

  9. Java学习笔记(十七)——java序列化

    [前面的话] 做项目总是要用到很多东西,遇到一个新的知识,并不是这个知识出来的时间短,而是对于自己来说是新的,所以就需要自己去学习,希望今后可以提高学习的效率. 这篇文章是关于Java 序列化的,选择 ...

  10. 编写高质量代码改善C#程序的157个建议[泛型集合、选择集合、集合的安全]

    前言   软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类.不管是数组还是集合类,它们都有各自的优缺点.如何使用好集合是我们在开发过程中必须掌握的技巧.不要小看这些技巧,一旦在开 ...