当前位置:   article > 正文

protobuf底层原理深度分析_protobuf原理

protobuf原理


前言

前面利用的到protobuf作为rpc的通讯协议,
c++与golang利用protobuf进行通讯
但是没有真正得深入研究过。
本文将不再介绍如何使用它而是深入研究protobuf协议,本文从编解码方式、数据存储方式以及序列化原理三个方面出发深度分析其底层原理。

一、protobuf是什么?

Protobuf(Protocol Buffers)是一种轻量级、高效、可扩展的数据交换格式,由 Google 开发和维护。它采用二进制编码,能够将数据进行高效压缩,同时具有良好的跨语言性和平台无关性,可以在不同的编程语言和操作系统之间进行数据交换。
常见数据交互格式对比:
JSON (JavaScript Object Notation):一般用于WEB项目中,因为浏览器对JSON格式的数据支持非常好,大部分编程语言有很多内建函数支持,而且JSON几乎支持所有编程语言。
XML:XML在WebService中的应用比较多,相比于JSON,它的数据更加冗余,因为需要成对的闭合标签,而JSON使用了键值对的方式,不仅压缩了一定的数据空间,同时也有更好的可读性。
Protobuf:谷歌公司新开发的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为Protobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性,因此只能反序列化得到可读数据。
相对于其他数据格式Protobuf的优势:
1、序列化后体积比JSON和XML小,适合网络传输。
2、序列化反序列化速度快,比JSON的处理速度快。
3、消息格式升级和兼容性还不错。
Protobuf的优点和缺点

  1. 高效性:Protobuf 使用二进制编码,相比于 XML 和 JSON 等文本格式,可以将数据压缩到更小的体积,从而减少网络传输的带宽和时间。同时,Protobuf 采用了“变长编码”机制,可以根据数据的大小动态调整编码长度,从而提高编解码的效率。
  2. 可扩展性:Protobuf 的数据结构非常灵活,可以方便地添加、删除和修改字段,从而支持数据结构的版本升级和兼容性。同时,Protobuf 支持定义嵌套和枚举类型,可以表示更复杂的数据结构。
  3. 跨语言性:Protobuf 支持多种编程语言,包括 C++、Java、Python、Go 等,可以在不同的编程语言之间进行数据交换。而且,由于 Protobuf 使用 IDL(接口定义语言)来描述数据结构,因此可以自动生成不同编程语言的代码,方便使用者进行开发和集成。
  4. 平台无关性:Protobuf 的数据格式与操作系统和硬件平台无关,可以在不同的操作系统和硬件平台之间进行数据交换。
  5. 易用性:Protobuf 的 API 简单易用,可以方便地序列化和反序列化数据,从而快速实现数据的传输和存储。
    缺点:
    Protobuf功能简单,无法用来表示复杂的概念。
    相比xml,xml具有某种程度的自解释性,因为最终是转成二进制流,不像xml和json能够直接查看明文。

二、Protobuf编解码方式

Varints 编码

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

0011010 | 0000101
  • 1

加上 msb,则

1 0011010 | 0 0000101 (0x9a 0x05)
  • 1

解码过程:
编码结果为 1#0011010 0#000 0101 (9a 05),与第一个例子类似,但是这里的第一个字节 msb = 1,所以需要再读一个字节,第二个字节的 msb = 0,则读取两个字节后停止。读到两个字节后先去掉两个 msb,剩下:

0011010  000 0101
  • 1

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

000 0101 0011010
  • 1

然后还原其原码为 666。
这里编码数字 666,Varints 只使用了 2 个字节。而正常情况下 int32 将使用 4 个字节存储数字 666,已经进行了压缩,摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率。
存在问题:
是负数的情况下:
因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。

ZigZag 编码

使用 ZigZag 编码之后,我们就可以将得到的无符号整数使用 Varints 编码方式进行编码了。在解码时,我们需要对 Varints 编码的数据进行解码,然后按照以下方式将其还原为有符号整数:

解码后的 x = (x >> 1) ^ -(x & 1)
  • 1

例如,我们要解码经过 ZigZag 编码和 Varints 编码的数据 0x84 0x01,首先使用 Varints 的解码方法还原出无符号整数 132,然后按照以上公式计算得出有符号整数 -66。

三、Protobuf数据存储方式

protobuf采用TLV格式存储数据,现在我们可以理解一个 message 编码将由一个个的 field 组成,每个字段可划分为 Tag - [Length] - Value。其中Tag表示字段的标识符,Length表示字段值的长度,Value表示字段值。Tag和Value是必选字段,而Length对于某些类型(如int32)是可选字段在这里插入图片描述

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
   };

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

编码过程:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

nameTag = 0001 0010
field_number = nameTag >> 3
field_number = 0010
wire_type = nameTag & 3
wire_type = 010
  • 1
  • 2
  • 3
  • 4
  • 5

四、Protobuf序列化原理

1、Protocol Buffer将消息中的每个字段进行编码后,利用T - L - V 存储方式进行数据的存储,最终得到一个二进制字节流。
2、ProtoBuf对于不同数据类型采用不同的序列化方式(数据编码方式与数据存储方式)
Protobuf对于不同的字段类型采用不同的编码和数据存储方式对消息字段进行序列化,以确保得到高效紧凑的数据压缩。不同类型的数据采用的编码方式和存储方式如图对于Varint编码数据的存储,不需要存储字节长度Length,使用T-V存储方式进行存储;对于采用其它编码方式(如LENGTH_DELIMITED)的数据,使用T-L-V存储方式进行存储。
3、ProtoBuf对于数据字段值的独特编码方式与T-L-V数据存储方式,使得 ProtoBuf序列化后数据量体积极小
在这里插入图片描述
WireType=0的序列化
WireType=0的类型包括int32,int64,uint32,unint64,bool,enum以及sint32和sint64。
编码方式采用Varint编码(如果为负数,采用Zigzag辅助编码),数据存储方式使用T-V方式存储二进制字节流。

WireType=1的序列化
WireType=1的类型包括fixed64,sfixed64,double。
编码方式采用64bit编码(编码后数据大小为64bit,高位在后,低位在前),数据存储方式使用T-V方式存储二进制字节流。

WireType=2的序列化
WireType=2的类型包括string,bytes,嵌套消息,packed repeated字段。
对于编码方式,标识符Tag采用Varint编码,字节长度Length采用Varint编码,string类型字段值采用UTF-8编码,嵌套消息类型的字段值根据嵌套消息内部的字段数据类型进行选择,
数据存储方式使用T-L-V方式存储二进制字节流。

WireType=5的序列化
WireType=5的类型包括fixed32,sfixed32,float。
WireType=5的类型包括fixed32,sfixed32,float。
编码方式采用32bit编码(编码后数据大小为32bit,高位在后,低位在前),数据存储方式使用T-V方式存储二进制字节流。

五、Protobuf序列化示例

message Test
{
    required string str = 2;
}

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

// 经过protobuf编码序列化后的数据以二进制的方式输出
// 输出为:18, 7, 116, 101, 115, 116, 105, 110, 103
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号