ProtoBuf的使用以及原理分析

Protocal Buffers(简称protobuf)是Google的一项技术,用于结构化的数据序列化、反序列化。

Protobuf的使用比较广泛,常用于RPC 系统(Remote Procedure Call Protocol System)和持续数据存储系统。其主要优点是空间开销小和性能比较好,类似于XML生成和解析,但protobuf的效率高于XML,不过protobuf生成的是字节码,可读性比XML差。

相关官方文档:

Protocol Buffers官网:https://developers.google.com/protocol-buffers/

Protocol Buffers官网(中文):https://developers.google.com/protocol-buffers/?hl=zh-CN

Git地址:https://github.com/google/protobuf

Java API:https://developers.google.com/protocol-buffers/docs/reference/java/?hl=zh-CN

proro文件的编写指南:https://developers.google.com/protocol-buffers/docs/style?hl=zh-CN

Java使用指南:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN

一.protobuf的基本应用

使用protobuf开发的基本步骤为:

 1.配置开发环境 安装protocol 代码编译器

 2.编写对应的.proto文件,定义序列化对象结构

 3.使用编译器生成对应的序列化工具类

 4.编写自己的应用

使用github上的protoc-3.5.1-win32.zip https://github.com/google/protobuf/releases

每个Protobuf 的 字段 都有一定的格式:

限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值]

1.限定修饰符: required/optional/repeated

Required:表示是一个必须字段,缺失该字段会引发编解码异常,导致消息被丢弃。

Optional:表示是一个可选字段。

Repeated:表示该字段可重复,每次可以包含0~N个值,表示集合

2.数据类型

protobuf定义了一些基本的数据类型

string/bytes/bool/int32/int64/float/double

enum 枚举类     message 自定义类

3.字段名称

字段名称的命名方式与C、Java等语言的变量命名方式几乎是相同的。

protobuf建议字段的命名采用以下划线分割的驼峰式。例如 建议使用first_name 而不是firstName.

4.字段编码值

编码值的取值范围为 1~2^32(4294967296)。

相同的编码值,其限定修饰符和数据类型必须相同。

消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。不过通常习惯使用连续的字段编码值,比较易于理解

⑤.默认值。

在发送数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递。当接受数据时,对于optional字段,如果没有接收到对应值,则设置为默认值

以下是一个简单的User对象的proto文件

syntax="proto2";

option java_package = "com.chenpp.serializer.protobuf";
option java_outer_classname="UserProto";
message User {
    //1,2表示当前序列化后的字段顺序
	required int32 age  = 2; //年龄
    required string name = 1;//姓名
	
   
}

使用proto自己的编译器进行编译,生成实体类

protoc.exe --java_out=./java   ./user.proto 

–java_out 后面是生成java文件存放地址 
最后的参数是proto文件的名称,可以写绝对地址,也可以直接写proto文件名称

引入protobuf对应的dependency,打印对应的序列化结果:

 <dependency>
      <groupId>com.google.protobuf</groupId>
      <artifactId>protobuf-java</artifactId>
      <version>3.5.0</version>
 </dependency>
public class ProtoSerializer implements ISerializer {
    public <T> byte[] serialize(T obj) {
        UserProto.User user = UserProto.User.newBuilder().setAge(21).setName("chenpp").build();
        return user.toByteArray();
    }

    public <T> T deserialize(byte[] data, Class<T> clazz) {

        try {
            UserProto.User user = UserProto.User.parseFrom(data);
            return (T) user;
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }
        return null;
    }
}


==================protobuf========================
protobuf {age:21}序列化后的byte[]的length: 10
10  6  99  104  101  110  112  112  16  21  
name: "chenpp"
age: 21

二.protobuf序列化的原理

之前那篇文章,讲过Json里的序列化结果为: { "name":"chenpp","age":21}  -- 一共26个字节,而想要将其进行进一步压缩,就需要去掉一些冗余的字节

 思路:1)能不能去掉定义属性(约定1=name,2=age)  约定了字段,约定了类型  去除分隔符(引号,冒号,逗号之类的)

         2)压缩数字,因为日常经常使用到的都是一些比较小的数字,一共int占4个字节,但实际有效的字节数没有那么多

          英文转化成数字(ASCII)并对数字进行压缩

protobuf里使用到了两种压缩算法:varint和zigzag算法

varint算法

这是针对无符号整数,一种压缩方式

压缩方法:

1.对一个无符号整数,将其换算成二进制

2.从右往左取7,然后最高位1

3.一直继续直到取到最后一个有意义的字节(在最后一个有意义的字节上最高位补0)

4.先取到的字节排在后到的字节前面,得到一个新的字节,转换成十进制就是压缩的结果

以:500为例:其实有意义的就是2个字节

 0000 0001 1111  0100= 2^2+2^4+2^5+2^6+2^7+2^8 = 4+16+32+64+128+256 = 500

按照其压缩方式得到的新的二进制字节为:

1111 0100  | 0000 0011

1111 0100 代表的是负数,使用补码(正数取反+1),并且最高位符号位为1

转化后: 先 -11111 0011   取反  0000 1100 = 12

计算出来就是 -12 3

字符如何转化成数字编码

对于英文字母,这里的name字段,使用ASCII码对照表查找对应的数字

chenpp: 对应的ASCII

c-99  h-104  e-101  n-110  p-112

按照varint的算法 取7位补最高位为1(最后一个字节最高位补0)

对于小于127(2^7-1=127)的数字,其有效字节只有1位,压缩的时候最高位补0,故压缩之前和压缩之后的数字没有变化

protobuf的存储格式

protobuf采用T-L-V的格式进行存储

[Tag | length | value ]

l其中length为可选, 但是string必须有length(这样在反序列化的时候程序才知道该字符串从哪里开始到哪里结束), 而int是不需要length的

Tag:字段标识符,用于标识字段 其值等于

    field_number(当前字段的编号,第几个字段)<<3|wire_type(int64/int32/可变长度string)

Length:Value的字节长度 (string需要有,int不需要)

Value:消息字段经过编码后的值

Age:int32  2<<3|0 = 16

Name: string 1<<3|2 = 10 

在反序列化的时候根据tag mod 8 的余数判断对应的wireType,从而知道该字段对应的存储方式和编码方式

对应User(name="chenpp",age=21)按照varint进行压缩后其序列化结果为:

c-99  h-104  e-101  n-110  p-112

String:tag-length-value 

Int32:tag-value

10        6          99      104      101    110   112   112      16         21

Tag – length  -  c  -     h     –   e    -   n   -   p   -  p   -  Tag -  Value

根据上述压缩算法可知:对于int32/int64,value有且只有最后一个字节为正数,故当遇到第一个为正数的字节时就知道其value值已经获取完毕,所以对于int32类型的字段,不需要length,只需要tag和value就足够了

ZigZag算法

在计算机中,负数会被表示为很大的整数,因为负数的符号位在最高位,如果使用varint算法进行压缩的话会需要 32/7 ~ 5字节,

加大了空间的开销.故在protobuf中对于有符号整数会使用sint32/sint64来表示。protobuf中负数的压缩方式是先使用ZigZag算符号数(无论数值是正数还是负数,都会进行一次压缩计算)转化为无符号数,再使用varint进行压缩

ZigZag算法的思路:

负数之所以不好压缩:一个原因是因为其最高位为1,另一个原因是对于绝对值比较小的负数,其正数会有很多的前导零,那么在使用补码表示负数的时候(取反+1),会导致负数会有很多的前导1,使得无法压缩

所以ZigZag采用的办法就是:先将符号位从最高位移动到最低位,其余数字均往前移动1位;然后再对所有的数字(符号位除外)进行取反,这样得到的计算结果就是一个可以压缩的数字(符号位不占据最高位,而小绝对值的数值由于取反操作其前导1都变为了前导0)

比方说-300:

其对应的正数的原码为: 0000 0000 0000 0000 0000 0001 0010 1100

取反: 1111 1111 1111 1111 1111 1110 1101 0011

+1:    1111 1111 1111 1111  1111 1110 1101 0100 (-300)

移动符号位之后: 1111 1111 1111 1111 1111 1101 1010 1001

取反:0000 0000 0000 0000 0000 0010 0101 0111

计算后为0010 0101 0111 = 599

在ZigZag算法里也是使用这种思路对有符号整数进行压缩的,将其转化成表达式就是

Sint32: (n<<1)^ (n>>31)

Sint64: (n<<1)^(n>>63)

当n为正数时,n>>31为0000 0000 0000 0000 0000 0000 0000 0000;当n为负数时,n>>31为1111 1111 1111 1111 1111 1111 1111 1111

(n>>31)与(n<<1)进行异或之后,如果n为正数,(n>>31)^(n<<1) =n<<1;如果n为负数,其计算结果和上述所说的最高位移动到最后,然后取反效果是一样的(n<<1补的最低位为0和n>>31异或运算之后一定为1,而其他位上与1做异或运算相当于取反),这样一来就可以使用varint进行压缩计算了

对-300的ZigZag计算结果:599 使用varint算法进行压缩

得到1101 0111   0000  0100

其结果为:-41   4

到此,protobuf的压缩原理就介绍完了

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值