浅析 Protocol Buffer 编码格式

本文深入探讨protobuf的编码格式,包括键值对结构、Base 128 Varints编码、字段标识号与数据类型的编码方式,以及zigzag编码和IEEE754标准在处理负数和浮点数时的应用,揭示protobuf在结构化数据压缩上的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

proto buffer 是google的一种结构化数据格式标准,已经有不少人做过protobuf与thrift等其他结构化数据格式标准的性能比较。由于正好之前也用过protobuf,对其压缩协议的方式比较感兴趣,所以做了一番探索,本篇的主要内容是protobuf的数据压缩格式,而不关注具体使用。


概括

protobuf的主要是将数据进行序列化和反序列化,目的在提供一种轻便高效的结构化数据格式
主要的应用场合:

  • 1.数据存储格式。
  • 2.RPC数据通信。

使用

  • 首先编写协议文件 .proto文件,例如一个协议定义文件:test.proto,文件内容如下
message test{
     required int32 a = 1;
     optional int32 b = 2;
}
  • 解析.proto协议文件,针对每个协议生成对应的编解码代码
    这样做的好处在哪里?
    与xml和json这样的数据格式标准对比,xml和json里都需要写字段名称。而protobuf对协议做了预先处理,每个协议都有各自的解析代码,将字段名称这些都数据都节省掉,有效提高压缩率。

编码

  • 键值对

    这里写图片描述

    所有的protobuf处理过的数据都会转换成如上Key->Value的扁平化数据格式,一个Field就是一个字段。
    注意事项:

    1. 这里可以看出protobuf将一个协议压缩到这个地步,但是并没有指明是哪一个协议,所以使用者一般都需要加上一个协议标识。
    2. 可以从上图看出数据是没有一个起点终点标识符的,所以,使用者需要给数据加上长度标识,否则协议解析时无法知道是否已经接收完一整个数据包。
  • Base 128 Varints

    这里写图片描述

    特点:

    1. 每个字节的第一位有特殊的作用:表示是否需要继续读取下一位作为完整的数据内容
    2. 小字节序。低位存在内存的低地址端,高位存在内存的高地址端
    3. 每个字节使用7bit的空间存储有效数据,即数据描述范围是0-127
    4. 由于每个字节第一位已经被使用,所以我们正常一个整数,大多数小于4个字节的描述,在protobuf极端情况下需要5个字节描述。
  • Key编码方式

    官方定义是field_number<<3|wire_type
    就是上文说的字段标识号左移3位,然后拼上字段的数据类型(wire_type)
    数据类型如下:
    这里写图片描述
    Type=3或者4表示的是“组”的概念,组主要是为了描述一些组合数据,但是其他数据结构已经能起到这个作用,所以现在“组”已经被弃用。
    以刚才的数据结构为例子,将test.a=1,test.b=2
    通过protobuffer编码后的二进制数据是:0000 1000 0000 0001 0001 0000 0000 0010
    第一个字节和第三个字节解析如下
    这里写图片描述
    一个数字只需要一个字节即可描述,对于我们的业务来说大部分都是小数字,所以带来的压缩效果非常明显。
    但这里的标识号只有4位,能描述的最大标示号为15,当标识号大于15时,它是如何描述的?
    假设标识号为16,编码后得到的二进制数据是:1000 0000 0000 0001
    解析过程如下图:
    这里写图片描述

  • value的编码方式

    例如有以下结构:

message test{
     repeated int32 a=1;
     optional int32 b=2;
}
  1. 设 a的值为空,b的值为1
    编码后的二进制数据是 0001 0000 0000 0001
    通过上面的解析方式得知,第一个字节的值是16,描述的是Key标识号为2的数据,第二个字节的值是1,表示Value=1

  2. 设a的值是内容为1,2,3的数组,b的值为4
    编码后得到二进制数据:0000 1000 0000 0001 0000 1000 0000 0010 00001000 0000 0011 0001 0000 0000 0001
    按字节转换为十进制得:8,1,8,2,8,3,16,4
    此处是将数组的每个字段平铺,可以理解为:k=1,v=1,k=1,v=2,k=1,v=3,k=2,v=4

  3. 多层嵌套的数据结构,例如新增下面这个结构,数组里包含了数组:
    message test2{
    repeated test a=1;
    optional int32 b=2;
    }
    随便填入一些数据,将编译后的二进制数据按字节转换成十进制得:
    10,6,8,1,8,1,16,1,10,6,8,2,8,2,16,2,16,4
    这里主要为了解释前两个字节:
    第一个字节10:0 0001 010 标示第一个字段,后面跟的val是个列表结构
    第二个字节6:标示后面需要读取6个字节作为value

  4. 紧凑型的数组数据
    message test{
    repeated int32 a=1[packed=true];
    optional int32 b=2;
    }
    给数组字段增加一个[packed=true]的属性,可以是数组类型的数据压缩率更高。
    也是设a的值是内容为1,2,3的数组,b的值为4,得到的字节数据是:10,3,1,2,3,16,4
    第一个字节10表示后面是列表结构
    第二个字节3表示需要读取后面多少个字节
    明显比之前的方式压缩比率更高

  5. zigzag编码解决负数问题
    对于负数,常见的处理方式是用符号位去标识,这样就导致了数据极大
    -1的编码结果:8,255,…. 255,255,1
    zigzag如图,就是正负转换成正数交替递增
    这里写图片描述
    -1的编码结果:8,1 效果显著

  6. IEEE754编码
    protobuf还使用了IEEE754标准作为数据编码方式,主要应对长整型浮点数等,既然protobuf有用到,这里做个大概分析
    这里写图片描述
    以单精度为例:

    • Sign:符号位
    • Exponent:指数偏移值(2^(e-1)-1),单精度是8位,那么指数偏移值是2^(8-1)-1=127(因为指数有可能是正数有可能是负数,需要保证上下范围基本对称,所以需要偏移值)
    • Fraction:有效数字

    • 现在假设原始数据是:10.625

      1. 转换为二进制是1010.101
      2. 科学技术法 1.010101*2^3
      3. 按照IEEE转换数据
        Sign:0
        Exponent:127+3=>130=>10000010
        Fraction:01010100000000000000000
        拼凑起来可得数据:65,42,0,0
      4. little-endian
        最终数据:0,0,42,65

综上所述:
撇开ProtocolBuffer的性能不谈,ProtocolBuffer压缩率还是很明显的,比较适用于大型结构化数据,可以通过其编码特性对不使用的字段以及常用数据进行高度压缩,对于传输大串的字符数据则不赞成用这种方式。

参考资料:
http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值