Advertisement

protobuf底层原理深度分析

阅读量:

文章目录

  • 前言
    • 一节: Protobuf 是何物?

    • 二节: Protobuf 的编码与解码机制

      • 子部分1.1 路径: Varints 编码
      • 子部分1.2 路径: ZigZag 编码
    • 三、Protobuf数据存储方式

      • T-V(Tag-Value)存储方式
      • Tag
    • 四、Protobuf序列化原理

    • 五、Protobuf序列化示例


前言

之前应用了protobuf来进行RPC通信。
c++与golang利用protobuf进行通讯 但并未对其工作原理进行透彻了解。
本文不打算讲解其具体使用方法,
本篇文章将从编解码方式、数据存储方式以及序列化原理三个方面深入探讨其底层机制。

一、protobuf是什么?

**Protocol Buffers(协议缓冲)**是一种轻量级且高效的二进制数据交换格式

高效率:Protobuf采用二进制编码方案,在数据压缩方面表现出色。相较于其他常见的文本格式如XML和JSON,则能够实现数据体积的显著缩减。这种压缩能力不仅降低了存储空间的需求,还显著减少了网络传输所需的带宽和时间。 Protobuf还通过其高效的编解码机制实现了对数据流量的优化控制。
灵活性:该协议不仅支持基本的数据类型操作还提供了灵活的数据组织方式。相对于传统的方式而言它能以更加便捷的方式完成增删改查等操作从而提升了系统的运行效率。
跨语言兼容性: Protobuf的设计目标就是实现不同编程语言之间的无缝对接其强大的接口定义能力使其能够轻松地与其他各种编程语言进行数据交互。
无平台依赖性:由于其基于纯二进制文件的设计原则 Protobuf完全独立于任何特定的操作系统或硬件架构因此可以在各种不同的操作系统环境下稳定运行。
易用性和扩展性结合:该协议通过简洁直观的操作界面以及高效的算法保证了开发者的使用体验同时也在性能上达到了很高的水平。
然而该协议存在一定的局限主要体现在以下几个方面:
首先其功能相对单一难以表示复杂的抽象概念;
其次与XML相比 在协议设计上存在一定的局限;
再者由于其不支持嵌套关系这一特性使得某些复杂的数据模型无法得到完整的表达;
最后由于缺乏对元数据的支持导致在某些特定场景下应用范围受到限制。

二、Protobuf编解码方式

Varints 编码

Varints的编码方式可以解决在编码整数类型时,数据位数不确定的问题。在编码Varints时,整数类型的每个字节的最高位都用于表示后面的7位是否表示该数值的最后一个字节。如果在该字节的最高位为0,则表示下一个字节也属于该数值的一部分;反之,最高位为1,则表示该数值的最后一个字节。
Varints 编码的规则如下:
1、存储数字对应的二进制补码,二进制数按照7位一组进行分组,并按照从低到高的顺序进行处理
2、在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节
编码过程:
666 的补码为 000 … 101 0011010,从后依次向前取 7 位组并反转排序,则得到:

复制代码
    0011010 | 0000101

加上 msb,则

复制代码
    1 0011010 | 0 0000101 (0x9a 0x05)

解码过程:
编码输出为 1\#_{48}6D\,2\#_{3C}36\,3\#_{2D}59(即9A-59),其解码逻辑与前一实例相似。然而,在本例中发现其中第一个字符的最高位(MSB)设为高电平(H),因此需补充下一个字符;而第二个字符的MSB则设为低电平(L),至此完成数据的有效传输并终止解码流程。在接收完整的数据块后应先去除掉所补充的两个高位校验位即可得到最终的有效信息内容。

复制代码
    0011010  000 0101

将这两个 7-bit 组反转得到补码:

复制代码
    000 0101 0011010

随后将其解码回十进制数值 ...

ZigZag 编码

通过 ZigZag 编码的方式之后 ,我们便能够将获得的无符号整数采用 Varints 编码方式进行表达**;在解码过程中** ,我们需要先对 Varints 格式的编码数据进行解码操作** ,接着按照以下步骤将其转换回有符号整数** :

复制代码
    解码后的 x = (x >> 1) ^ -(x & 1)

我们需对已进行ZigZag编码和Varints编码的数据0x84, 0x01进行解码操作。随后应用Varints编码的解码方法恢复为无符号整数132,并代入上述公式计算得到有符号整数-66。

三、Protobuf数据存储方式

Protobuf通过TLV格式实现数据存储机制 ,我们可以从消息编码中发现其由多个字段组成 。每个字段可以分解为带有标签、可选长度和值的部分 。其中Tag标识该字段 ,Length指示该字段值所占位数 ,而Value则代表具体的数值信息 。需要注意的是 ,Tag和Value均为必要项 ,但某些情况下(例如整数类型int32)可能省略Length这一部分

在这里插入图片描述

T-V(Tag-Value)存储方式

消息字段标识号及其数据类型和字段值经过Protobuf遵循Varint和Zigzag编码方案后,在T-V(Tag-Value)模式下完成数据存储过程。对于通过Varint和Zigzag编码的数据信息,在T-L-V结构中未包含字节数量Length这一信息项。

在这里插入图片描述

Tag

该 Tag 借助 Varints 编码方案实现了编码。
Tag 的结构包含两个关键组成部分:field_number 和 wire_type。
field_number 表示在定义 message 时所使用的字段编号。
wire_type 则决定了编码的具体方式,它是依据 ProtoBuf 标准来选择最优的 Value 编码策略。

在这里插入图片描述

代码如下,

复制代码
    Tag = (field_number << 3) | 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
       };

编码过程:

复制代码
    message person
    { 
       required int32     id = 1;  
       // wire type = 0,field_number =1 
       required string    name = 2;  
       // wire type = 2,field_number =2 
     }
    
    nameTag = 2 << 3 | 2
    nameTag = 0001 0010

解码时,Protobuf根据Tag将Value对应于消息中的字段。

复制代码
    nameTag = 0001 0010
    field_number = nameTag >> 3
    field_number = 0010
    wire_type = nameTag & 3
    wire_type = 010

四、Protobuf序列化原理

Protocol Buffer通过每个字段进行编码之后, 利用T-L-V存储方式进行存储, 最终生成一个二进制字节流.
ProtoBuf在处理多种数据类型时, 分别采取不同的编码方式.
在处理不同数值时, ProtoBuf会分别采取独特的编码方法与T-L-V存储方式进行操作, 从而实现高效的数据压缩. 在这一过程中, Varint类型的编码主要特点是没有记录字节长度Length, 而是直接使用T-V存储方式进行操作; 对于其他类型的编码(如LENGTH_DELIMITED), 则会采用T-L-V的方式进行存储.
通过这种独特的压缩方法, ProtoBuf能够在不丢失原始信息的情况下显著减少序列化的体积.

在这里插入图片描述

WireType=0的序列化
该方案对WireType=0的数据类型进行编码。
其中包含以下数据类型:int32、int64、uint32、unint64、bool、enum以及sint32和sint64。
该方案遵循Varint编码规则(其中当数值为负时使用Zigzag辅助编码)。
数据存储方面,则遵循T-V方法以存储二进制字节流。

序列化处理中采用WireType=1方案。该方案支持的数据类型主要包括fixed64、sfixed64和double三种类型。其中采用的是基于64位的编码机制(编码后的数据量为固定长度64位,并遵循高位后续低位先存储的原则)。对于二进制流数据的处理,则采用了T-V存储策略。

序列化方案中的WireType=2参数

WireType=5的序列化
WireType=5由fixed32、sfixed32和float三种类型构成。
其中经过压缩后的数据长度为32位(即编码后的数据长度为C_{\text{max}}),其高位位于前面并遵循T-V模式存储连续的二进制字节流序列。

五、Protobuf序列化示例

复制代码
    message Test
    {
    required string str = 2;
    }
    
    // 将str设置为:testing
    Test.setStr(“testing”)
    
    // 经过protobuf编码序列化后的数据以二进制的方式输出
    // 输出为:18, 7, 116, 101, 115, 116, 105, 110, 103
在这里插入图片描述

全部评论 (0)

还没有任何评论哟~