#iOS#HeminWon文章Protocol Buffer

Protocol Buffer 序列化原理大揭秘 - 为什么Pr

2017-04-24  本文已影响2253人  Carson带你学安卓

前言

由于 Google出品,我相信Protocol Buffer已经具备足够的吸引力

阅读本文前请先阅读:

  1. 快来看看Google出品的Protocol Buffer,别只会用Json和XML了
  2. 手把手教你如何安装Protocol Buffer
  3. 这是一份很有诚意的 Protocol Buffer 语法详解

目录

目录

1. 定义

一种 结构化数据 的数据存储格式(类似于 XML、Json

  1. Google 出品 (开源)
  2. Protocol Buffer 目前有两个版本:proto2proto3
  3. 因为proto3 还是beta 版,所以本次讲解是 proto2

2. 作用

通过将 结构化的数据 进行 串行化(序列化),从而实现 数据存储 / RPC 数据交换的功能

  1. 序列化: 将 数据结构或对象 转换成 二进制串 的过程
  2. 反序列化:将在序列化过程中所生成的二进制串 转换成 数据结构或者对象 的过程

3. 特点

Protocol Buffer 特点

4. 应用场景

传输数据量大 & 网络环境不稳定 的数据存储、RPC 数据交换 的需求场景

如 即时IM (QQ、微信)的需求场景


总结

传输数据量较大的需求场景下,Protocol BufferXML、Json 更小、更快、使用 & 维护更简单!


5. 使用流程

关于 Protocol Buffer 的使用流程,具体请看我写的文章:快来看看Google出品的Protocol Buffer,别只会用Json和XML了


6. 知识基础

6.1 网络通信协议

通信协议结构

6.2 数据结构、对象与二进制串

不同的计算机语言中,数据结构,对象以及二进制串的表示方式并不相同。

a. 对于数据结构和对象

b. 二进制串

  1. byte 属于 Java 的八种基本数据类型
  2. 二进制串 容易和 String混淆:String 一种特殊对象(Object)。对于跨语言间的通讯,序列化后的数据当然不能是某种语言的特殊数据类型。

6.3 T - L - V 的数据存储方式

其中 Length可选存储,如 储存Varint编码数据就不需要存储Length

最终存储的字节流

相应字段在解码时才会被设置为默认值


7. 序列化原理解析

请记住Protocol Buffer三个关于数据存储 的重要结论:

序列化 = 对数据进行编码 + 存储

从上表可以看出:

  1. 对于存储Varint编码数据,就不需要存储字节长度 Length,所以实际上Protocol Buffer的存储方式是 T - V
  2. Protocol Buffer采用其他编码方式(如LENGTH_DELIMITED)则采用T - L - V

下面,我将对不同的编码方式 & 数据存储方式进行逐一讲解。


7.1 Wire Type = 0时的编码 & 数据存储方式

Wire Type = 0时的编码 & 数据存储方式

7.1.1 编码方式: Varint & Zigzag

A. Varint编码方式介绍

i. 简介

如:

  • 对于 int32 类型的数字,一般需要 4个字节 表示;
  1. 若采用 Varint编码,对于很小的 int32 类型 数字,则可以用 1个字节 来表示
  2. 虽然大的数字会需要 5 个 字节 来表示,但大多数情况下,消息都不会有很大的数字,所以采用 Varint方法总是可以用更少的字节数来表示数字

ii. 原理介绍

private void writeVarint32(int n) {                                                                                    
  int idx = 0;  
  while (true) {  
    if ((n & ~0x7F) == 0) {  
      i32buf[idx++] = (byte)n;  
      break;  
    } else {  
      i32buf[idx++] = (byte)((n & 0x7F) | 0x80);  
      // 步骤1:取出字节串末7位
      // 对于上述取出的7位:在最高位添加1构成一个字节
      // 如果是最后一次取出,则在最高位添加0构成1个字节

      n >>>= 7;  
      // 步骤2:通过将字节串整体往右移7位,继续从字节串的末尾选取7位,直到取完为止。
    }  
  }  
  trans_.write(i32buf, 0, idx); 
      // 步骤3: 将上述形成的每个字节 按序拼接 成一个字节串
      // 即该字节串就是经过Varint编码后的字节
}   

从上面可看出:Varint 中每个 字节 的最高位 都有特殊含义:

所以,当使用Varint解码时时,只要读取到最高位为0的字节时,就表示已经是Varint的最后一个字节

因此:

下面,我将用两个个例子来说明Varint编码方式的使用

Varint编码过程

从上面可以看出:

以此类推,比如300也只需要2个字节

下面继续看如何解析经过Varint 编码的字节

Varint 编码方式的不足

因为计算机定义负数的符号位为数字的最高位

  1. 对于int32 / int64 类型的字段值(正数),Protocol Buffer直接采用 Varint编码
  2. 对于sint32 / sint64 类型的字段值(负数),Protocol Buffer会先采用 Zigzag 编码,再采用 Varint编码

B. Zigzag编码方式详解

i. 简介

特别是对 表示负数的数据 能更好地进行数据压缩

b. 原理


public int int_to_zigzag(int n)
// 传入的参数n = 传入字段值的二进制表示(此处以负数为例)
// 负数的二进制 = 符号位为1,剩余的位数为 该数绝对值的原码按位取反;然后整个二进制数+1
{
        return (n <<1) ^ (n >>31);   
        // 对于sint 32 数据类型,使用Zigzag编码过程如下:
        // 1. 将二进制表示数 左移1位(左移 = 整个二进制左移,低位补0)
        // 2. 将二进制表示数 右移31位 
              // 对于右移:
              // 首位是1的二进制(有符号数),是算数右移,即右移后左边补1
              // 首位是0的二进制(无符号数),是逻辑左移,即右移后左边补0
        // 3. 将上述二者进行异或

        // 对于sint 64 数据类型 则为: return  (n << 1> ^ (n >> 63) ;
}


// 附:将Zigzag值解码为整型值
public int zigzag_to_int(int n) 
{
        return(n >>> 1) ^ -(n & 1);
// 右移时,需要用不带符号的移动,否则如果第一位数据位是1的话,就会补1
}
 

Zigzag编码

总结

Protocol Buffer 通过VarintZigzag编码后大大减少了字段值占用字节数。

7.1.2 存储方式:T - V

对于 Varint & Zigzag 编码,省略了T - L - V中的字节长度Length

Varint & Zigzag数据存储方式

下面将详细介绍T - V 存储方式中的存储细节:Tag & Value

1. Tag

  1. 存储了字段的标识号(field_number)和 数据类型(wire_type),即Tag = 字段数据类型(wire_type) + 标识号(field_number
  2. 占用 一个字节 的长度(如果标识号超过了16,则占用多一个字节的位置)
  3. 解包时,Protocol Buffer根据 TagValue 对应于消息中的 字段
// Tag 的具体表达式如下
 Tag  = (field_number << 3) | wire_type
// 参数说明:
// field_number:对应于 .proto文件中消息字段的标识号,表示这是消息里的第几个字段
// field_number << 3:表示 field_number = 将 Tag的二进制表示 右移三位 后的值 
// field_num左移3位不会导致数据丢失,因为表示范围还是足够大地去表示消息里的字段数目

//  wire_type:表示 字段 的数据类型
//  wire_type = Tag的二进制表示 的最低三位值
//   wire_type的取值
 enum WireType { 
      WIRETYPE_VARINT = 0, 
      WIRETYPE_FIXED64 = 1, 
      WIRETYPE_LENGTH_DELIMITED = 2, 
      WIRETYPE_START_GROUP = 3, 
      WIRETYPE_END_GROUP = 4, 
      WIRETYPE_FIXED32 = 5
   };

// 从上面可以看出,`wire_type`最多占用 3位 的内存空间(因为 3位 足以表示 0-5 的二进制)

//  以下是 wire_type 对应的 数据类型 表
wire_type对应数据类型
// 消息对象
 message person
 { 
    required int32     id = 1;  
    // wire type = 0,field_number =1 
    required string    name = 2;  
    // wire type = 2,field_number =2 
  }

//  如果一个Tag的二进制 = 0001 0010
标识号 = field_number = field_number  << 3 =右移3位 =  0000 0010 = 2
数据类型 = wire_type = 最低三位表示 = 010 = 2

2. Value

经过 Protocol Buffer采用Varint & Zigzag编码后 的消息字段的值

7.1.3 实例说明

下面通过一个实例进行整个编码过程的说明:

message Test
{

required int32 id1 = 1;

required int32 id2 = 2;
}

// 在代码中给id1 附上1个字段值:296
// 在代码中给id2 附上1个字段值:296
Test.setId1(300);
Test.setId2(296);

// 编码结果为:二进制字节流 = [8,-84,2,16, -88, 2]
编码过程

7.2 Wire Type = 1& 5时的编码&数据存储方式

Wire Type = 1& 5时的编码&数据存储方式

两种情况下,都是高位在后,低位在前

7.3 Wire Type = 2时的 编码 & 数据存储方式

Wire Type = 2时的编码&数据存储方式 编码方式 数据存储示意图

此处主要讲解三种数据类型:

1. String类型

字段值(即V) 采用UTF-8编码

编码 & 存储方式
message Test2
{
    required string str = 2;
}

// 将str设置为:testing
Test2.setStr(“testing”)

// 经过protobuf编码序列化后的数据以二进制的方式输出
// 输出为:18, 7, 116, 101, 115, 116, 105, 110, 103

实例

2. 嵌套消息类型(Message)

  1. 外部消息的 V即为 嵌套消息的字段
  2. T - L -V 里嵌套了一系列的 T - L -V
编码 & 存储方式
message Test2
{
    required string str = 1;
    required int32 id1 = 2;

    
}

message Test3 {
  required Test2 c = 1;
}

// 将Test2中的字段str设置为:testing
// 将Test2中的字段id1设置为:296
// 编码后的字节为:10 ,12 ,18,7,116, 101, 115, 116, 105, 110, 103,16,-88,2

编码 & 存储方式如下

编码 & 存储方式

3. 通过packed修饰的 repeat 字段

repeated 修饰的字段有两种表达方式:

message Test
{
    repeated int32 Car = 4 ;
    // 表达方式1:不带packed=true

    repeated int32 Car = 4 [packed=true];
    // 表达方式2:带packed=true
    // proto 2.1 开始可使用

// 区别在于:是否连续存储repeated类型数据
}


// 在代码中给`repeated int32 Car`附上3个字段值:3、270、86942

Test.setCar(3);
Test.setCar(270);
Test.setCar(86942);

repeated类型可以看成是数组

不带pack的存储方式 带pack的存储方式

通过采用带packed=truerepeated 字段存储方式,从而更好地压缩序列化后的数据长度。

特别注意


8. 特别注意

所以 required字段必须要被设置字段值

.proto文件内 字段定义的数据无关

若新版本 增加了 required 字段, 旧版本 在数据解码时会认为IsInitialized() 失败,所以慎用 required字段


9. 使用建议

根据上面的序列化原理分析,我总结出以下Protocol Buffer的使用建议

通过下面建议能有效降低序列化后数据量的大小

相应的字段在解码时才会被设置为默认值


10. 序列化 & 反序列化过程

10.1 Protocol Buffer的序列化 & 反序列化过程

序列化过程如下:

  1. 判断每个字段是否有设置值,有值才进行编码
  2. 根据 字段标识号&数据类型 将 字段值 通过不同的编码方式进行编码

由于:
a. 编码方式简单(只需要简单的数学运算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

所以Protocol Buffer的序列化速度非常快。

反序列化过程如下:

  1. 调用 消息类的 parseFrom(input) 解析从输入流读入的二进制字节数据流

从上面可知,Protocol Buffer解析过程只需要通过简单的解码方式即可完成,无需复杂的词法语法分析,因此 解析速度非常快。

  1. 将解析出来的数据 按照指定的格式读取到 JavaC++Phyton 对应的结构类型中

由于:
a. 解码方式简单(只需要简单的数学运算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

所以Protocol Buffer的反序列化速度非常快。

10.2 对比于XML 的序列化 & 反序列化过程

XML的反序列化过程如下:

  1. 从文件中读取出字符串
  2. 将字符串转换为 XML 文档对象结构模型
  3. XML 文档对象结构模型中读取指定节点的字符串
  4. 将该字符串转换成指定类型的变量

上述过程非常复杂,其中,将 XML 文件转换为文档对象结构模型的过程通常需要完成词法文法分析等大量消耗 CPU 的复杂计算。

因为序列化 & 反序列化过程简单,所以序列化 & 反序列化过程速度非常快,这也是 Protocol Buffer效率高的原因


11.总结

Protocol Buffer的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其原因如下:


接下来我会讲解Protocol Buffer的源码分析,有兴趣可以继续关注Carson_Ho的安卓开发笔记


请点赞!因为你的鼓励是我写作的最大动力!

相关文章阅读
Android开发:最全面、最易懂的Android屏幕适配解决方案
Android事件分发机制详解:史上最全面、最易懂
Android开发:史上最全的Android消息推送解决方案
Android开发:最全面、最易懂的Webview详解
Android开发:JSON简介及最全面解析方法!
Android四大组件:Service服务史上最全面解析
Android四大组件:BroadcastReceiver史上最全面解析


欢迎关注Carson_Ho的简书!

不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度

上一篇下一篇

猜你喜欢

热点阅读