一、基础

1、序列化与反序列化

序列化:指将结构化的数据按一定的编码规范转成指定格式的过程;

反序列化:指将转成指定格式的数据解析成原始的结构化数据的过程;

举个例子:Person是一个表示人的对象类型,person是一个Person类型的对象,将person存到一个对应的XML文档中的过程就是一种序列化,而解析XML生成对应Person类型对象person的过程,就是一个反序列化的过程。在这里结构化数据指的就是Person类型的数据,一定的编码规范指的就是XML文档的规范。XML是一种简单的序列化方式,用XML序列化的好处是,XML的通用性比较好,另外,XML是一种文本格式,对人阅读比较友好,但是XML方式比较占空间,效率也不是很高。通常,比较高效的序列化都是采用二进制方式的;将要序列化的结构化数据,按一定的编码规范,转成为一串二进制的字节流存储下来,需要用的时候再从这串二进制的字节流中反序列化出对应的结构化的数据。

2、TLV编码格式:即Tag-Length-Value(其中Length可选)的编码格式。

每个字段都使用TLV的方式进行序列化,一个消息就可以看成是多个字段的TLV序列拼接成的一个二进制字节流。其实这种方式很像Key-Value的方式,所以Tag一般也可以看做Key。显然,这种方式组织的数据并不需要额外的分隔符来划分数据,所以序列化的效率非常高(空间效率)。

二、Protobuf编码基础

1、Varints:varints是一种将一个整数序列化为一个或者多个Bytes的方法。越小的整数,使用的Bytes越少。Varints规则如下:

1)每个Bytes的最高位(msb)是标志位。如果该值为1,表示该Bytes后面还有其他Byte;如果该位为0,表示该Byte是最后一个Byte。

2)每个Byte的低7位是用来存数值的位。

3)Varints方法使用小端字节序(反解数值的时候后面的字节方前面)。

举几个例子:

a)以数字1为例:二进制是0000 0001,最高位是0代表后面没有更多字节,剩下的7位就是数值位,"000 0001"显然就是1。

b)以数字300为例:它在Varints规则下的表示形式是1010 1100 0000 0010。

第一个字节是 1010 1100,最高位是1,表示后面还有更多字节;第一个字节内容是后7位即 010 1100;

第二个字节是 0000 0010,最高危是0,表示后面没有更多字节;第二个字节内容是后7位即 000 0010;

因为是“低字节序”,所以实际字节是 000 0010 010 1100=1 0010 1100=300。

2、字段编号(field_num):就是.proto文件中每个字段都有的顺排的那个编号;

3、传输类型(wire_type):每个字段都有一个对应的字段(传输)类型,如下表:

Type

Meaning

Used For

0

Varint

int32, int64, uint32, uint64, sint32, sint64, bool, enum

1

64-bit

fixed64, sfixed64, double

2

Length-delimited

string, bytes, embedded messages, packed repeated fields

3

Start group

groups (deprecated)

4

End group

groups (deprecated)

5

32-bit

fixed32, sfixed32, float

4、field:message由一个个字段组成,一个字段的完整的二进制描述即<<编号,传输类型>,值>通常称为一个field,如下图。

5、具体而言每个field的构成为Tag-[Length]-Value;这里的[Length]是否需要是依据Tag最后三位的wire_type来决定的。

wire_type 含义 二进制结构
0 Varints Tag-Value
1、5 64-bits/32bits Tag-Value
2 string/嵌套/repeated Tag-[Length]-Value

6、Tag:Tag的组成是“field_num << 3 | wire_type”(也称为每个filed的key或者键)。

4、pb编解码关键点

 

1)在消息流中每个Tag(key/键)都是varint,编码方式为:field_num << 3 | wire_type。即,Tag(key/键)由 .proto文件中字段的编号(field_num)传输类型(wire_type)两部分组成。

注:Tag也是Varints编码,其后三位是传输类型(wire_type),之前的数值为是字段编号(field_num)。

注意并不是说Tag只能是一个字节,这里说了Tag也是用Varint编码,显然使用Varint编码方式几千/几万的字段序号(field_num)都是可以被表示的。

2)在对一条消息(message)进行编码的时候是把该消息中所有的key-value对序列化成二进制字节流key和value分别采用不同的编码方式。

3)消息的二进制格式只使用消息字段的字段编号(field_num)作为Tag(key/键)的一部分,字段名和声名类型只能在解析端通过引用参考消息类型的定义(即.proto文件)才能确定。

4)解码的时候解码程序(解码器)读入二进制的字节流,解析出每一个key-value对;如果解码过程中遇到识别不出来的filed_num就直接跳过这样的机制保证了即使该消息(message)添加了新的字段,也不会影响旧的编/解码程序正常工作。

举例说明:

对于如下message,如果应用程序创建了一个Test1的对象,并把a字段赋值150,那么protobuf会把它编码成这样三个字节:“08 96 01”。接下来解析:

message Test1 {required int32 a = 1;
}

(1)在二进制流里面第一个数字都是key(至少是key的一部分;若首位为1表示后面还有字节,为0表示后面没有字节了),即key是08,对应二进制0000 1000;根据前面所说知道后三位是000,代表传输类型是0即Varint;前五位是0000 1就是数字1,代表序号field_num是1。

注:因此通过传输类型知道后面传输的是Varint,通过序号知道在message里面的tag是1。

(2)接下来就使用Varint方式对后面的“96 01”解码即可。对应二进制是1001 0110 0000 0001:

第一个字节是1001 0110,最高位是1,代表后面还有更多字节;第一个字节内容是后7位即 001 0110;

第二个字节是0000 0001,最高位是0,代表后面没有更多字节;第二个字节内容是后7位即 000 0001;

因为是采用“低字节序”,所以实际的字节是: 000 0001 001 0110 = 1001 0110 = 150。

编码方式其他的问题:

1、有符号整数编码的问题与zigzag优化

由上面可以知道protocol buffer中所有与传输类型0关联的类型都会被编码为Varints。但是在编码负数的时候,带符号的int类型(sint32和sint64)与“标准”int类型(int32和int64)之间存在着巨大区别。如果将int32或int64用作负数的类型,则结果varint总是十个字节;也就是说像-1、-2这样的负数也会占用比较多的Bytes。实际上他被视为一个非常大的无符号整数。如果使用有符号类型(sint32和sint64)之一,则生成的varint会采用一种改进的ZigZag编码,效率更高。ZigZag编码将有符号数映射到无符号数以便具有较小绝对值的数字(如-1)也具有较小的varint编码值。这样做的方式是通过正整数和负整数来回“曲折”,将-1编码为1,将1编码为2,将-2编码为3…………以此类推。如下表所示:

Signed Original

Encoded As

0

0

-1

1

1

2

-2

3

2

4

-3

5

2147483647

4294967294

-2147483648

4294967295

2、64-bit(wire_type=1)和32-bit(wire_type=5) 等非varint数字的编码

这两种的编码方式比较简单,直接在key后面跟上64bits或32bits,采用“小端”字节序。

3、Length-delimited(wire_type=2)字符串的编码。

key-value的格式为 key+length+content ;key的编码方式就是前面所说的,length采用varints编码方式,content就是有length指定的长度的Bytes。

message Test2 {optional string b = 2;
}

如上结构:实例化一个对象并设置b的值为“testing”,我们可以得到这条消息(message)对应的二进制数据为 “12 07 74 65 73 74 69 6e 67

1)key是12,二进制为0001 0010;后三位是010,即传输类型为2(Length-delimited);前五位是00010即2,表示序号为2。

2)length是07,代表value的长度是7;

3)再后面代表的就是value值了;例如74(Hex)对应116(D),就是字符‘t’。

4、内嵌消息

如下图是一个拥有内嵌消息的结构Test3,内嵌的消息类型就是上面定义的Test1。

message Test3 {optional Test1 c = 3;
}

如果对其实例化一个对象并将Test1中的a设置为150,对象编码后的二进制流为“1a 03 08 96 01”。

1)1a二进制为0001 1010,后三位是010,即传输类型为2(Length-delimited);前五位是11100即3表示序号为3。

2)03就表示后面value的长度为3字节;

3)“08 96 01”和第一个例子中编码后的结果一样了。

结论:也就是说内嵌消息会和字符串一样被视为Length-delimited;编码方式亦相同(key+length+content)。

5、可选和可重复元素

这里再剖析一个protobuf编码实例:

对于如下结构 我们实例化一个对象给num1字段赋值10、num2字段赋值1073741824。序列化后的结果十六进制输出和二进制输出分别如下:(过程见 这里)

message Test
{required int32 num1 = 1;required fixed32 num2 = 2;
}
十六进制表示为:
080a 1500 0000 40
二进制表示为:
0000‭1000 00001010 00010101 00000000 00000000 00000000 01000000

利用上面说的编码规则对得到的二进制数据进行分析,如下:

第一个字段的Tag解码:

由上面知道Tag也是采用Varint编码的因此最开始依据第一个msb位读取第一个字节(00001000)为第一个字段的Tag,最后3位(000)表示wire_type=0指示了接下来Value的编码采用Varint方式。剩余5位(00001)表示field_number=1表示第一个字段的编号为1;

第一个字段的Value解码:

由wire_type=0可知Value是采用Varint编码。故读取下一个字节(00001010),该字节的第一位msb位为0。故接下来的7位(0001010)表示第一个字段的值。因为只有一个字节因此逆序还是0001010,二进制0001010表示数字为:10

第二个字段的Tag解码:

由同样的办法得第二个字段field_numer=2,wire_type=5。wire_type=5对应fixed32类型的编码方式。

第二个字段的Value解码:

fixed32类型的编码方式因为已经固定取32位,因此不需要msb位。但为了移位方便,还是有按字节逆序编码。因此解码的时候也要逆序回来。

fixed32的编码如下:
00000000 00000000 00000000 01000000
按字节逆序回来:
01000000‬ 00000000 00000000 00000000
二进制表示的值为:‭1073741824

更多case参见:Protobuf编码 - 只取一瓢饮 - 博客园

Protocol Buffers编码详解,例子,图解_fullsail的博客-CSDN博客

序列化与反序列化

序列化

protobuf生成的类中,其继承体系涉及的主要是::google::protobuf::MessageLite和Message这两个类,其中Message是::google::protobuf::MessageLite的子类。我们自动生成的类可能继承自这两个类中的一个,这取决于在proto描述文件中的配置,如果设置option optimize_for = LITE_RUNTIM,则编译生成的类继承自::google::protobuf::MessageLite。这两个类都拥有基本的功能的代码,而Message是扩展出来的子类,增加了一些特性功能,然而实际中如果用不到这些功能,则开启这个优化可以使得我们生成的文件更小。

反序列化

对于反序列化而言其实其本质就是从一个输入流里一次读取tag值然后根据wire_type判断他是那种类型类型的数据,然后再调用对应的方法读取对应的值。整个处理过程其实就在一个while循环中,直到数据出来完毕才终止。

重点参考: https://www.136.la/tech/show-149798.html

常见问题:

(1)不要随便调整已有字段的顺序。

(2)结尾某几个字段是序号,靠上的字段都正常。

原因:这种一看就是打包方新增pb字段了,但是解包方的pb依赖没更新。

(3)反序列化后发现所有的字段名称都变成字段序号了。

原因:打包方所用的pb和解包所用的根本不是一个pb。原因很多。除了考虑原始打包方和解包方pb文件是否一致外,还应该考虑中间会不会被别人改动过。举个例子,中间某环节把打包方打包后的buf又作为某个结构的字段值打包传过来了。这时候解包方用原始和打包方约定的pb来反序列化肯定是有问题的了。

Protobuf编码规则详解相关推荐

  1. python编码问题无法复现_Python编码问题详解

    1. 基本概念 字符集(Character set) 解释:文字和符合的总称 常见字符集: Unicode字符集 ASCII字符集(Unicode子集) GB2312字符集 编码方法(Encoding ...

  2. php各种编码集详解和在什么情况下进行使用 [php 字符集 显示]

    http://blog.cnsunrun.com/member/blog/blog_info/30/84 ----------------------------------------------- ...

  3. java web编码详解_java web 开发 编码问题详解

    java web 开发 编码问题详解 浏览器 IE/FireFox ------------->Servlet容器-------------------------->显示页面 编码   ...

  4. Computer:字符编码(ASCII编码/GBK编码/BASE64编码/UTF-8编码)的简介、案例应用(python中的编码格式及常见编码问题详解)之详细攻略

    Computer:字符编码(ASCII编码/GBK编码/BASE64编码/UTF-8编码)的简介.案例应用(python中的编码格式及常见编码问题详解)之详细攻略 目录 符串编码(ASCII编码/GB ...

  5. linux下防火墙iptables用法规则详解

    linux下防火墙iptables用法规则详解 分享者: du52.com 邮件: wangaibo168@163.com 主页: http://www.du52.com linux下防火墙iptab ...

  6. xss编码绕过详解(更像是在介绍实体编码和JS编码的解析过程)

    xss编码绕过详解(更像是在介绍实体编码和JS编码的解析过程) 注:本文通过研究各种情况下实体编码和JS编码是否生效,进而总结了哪些情况下能够进行编码后,javascript代码依然能够正常执行. 解 ...

  7. python中unicode编码表_Python中的字符串操作和编码Unicode详解

    本文主要给大家介绍了关于 Python中的字符串操作和编码Unicode的一些知识,下面话不多说,需要的朋友们下面来一起学习吧. 字符串类型 str:Unicode字符串.采用''或者r''构造的字符 ...

  8. php各种编码集详解和以及在什么情况下进行使用 发布:mdxy-dxy 字体:[增加 减小] 类型:转载 字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。 字符集是多个字符的集

    php各种编码集详解和以及在什么情况下进行使用 发布:mdxy-dxy 字体:[ 增加 减小] 类型:转载 字符是各种文字和符号的总称,包括各国家文字.标点符号.图形符号.数字等. 字符集是多个字符的 ...

  9. Suricata IDS 入门 — 规则详解

    suricata是一款开源高性能的入侵检测系统,并支持ips(入侵防御)与nsm(网络安全监控)模式,用来替代原有的snort入侵检测系统,完全兼容snort规则语法和支持lua脚本. 1.规则配置 ...

  10. css样式继承规则详解

    css样式继承规则详解 一.总结 一句话总结:继承而发生样式冲突时,最近祖先获胜(最近原则). 1.继承中哪些样式不会被继承? 多数边框类属性,比如象Padding(补白),Margin(边界),背景 ...

最新文章

  1. 参数检验——当总体分布已知(如总体为正态分布),根据样本数据对总体分布的统计参数进行推断 非参数检验——利用样本数据对总体分布形态等进行推断的方法。...
  2. python~爬虫~1
  3. 研究僵局–第4部分:修复代码
  4. TCL微型计算机如何投屏,TCL电视怎么投屏?3个办法帮助你完美解决
  5. C++ unsigned long 转化为 unsigned char*
  6. 学python可以做什么职业-Python可以做的5大功能和就职5大高薪职业
  7. IP 协议报文格式 【IPv4】
  8. imageai--自动机器学习初体验
  9. xjoi 1543 我自闭了
  10. SVN Clean up 失败和SVN is already locked情况解决
  11. 超越函数e^(-x^2)的定积分
  12. 华硕ac68u无线最佳设置_华硕AC86U,AC88U的掉线、断流问题何时彻底解决?
  13. indirect引用单元格/单元格区域/跨表引用(适用于引用不同工作表的相同单元格或者区域)
  14. scrapy 简单教程
  15. 一年有50万主播入驻淘宝,宇宙的尽头是编制,直播的尽头是淘宝?
  16. 计算机快捷键任务管理器,任务管理器快捷键,教您win10怎么打开任务管理器
  17. 2020ICPR-化妆演示攻击
  18. jdk1.8,64位linux版本下载路径.永久免费.这是一个爱分享的世界...
  19. 电脑蓝屏解决方案:DPC WATCHDOG VIOLATION
  20. 为MCU在Qt上运行Doom

热门文章

  1. Android 视频直播 ( 从快播到直播,从高清到无码 )十年视频开发项目
  2. 计算机硬盘型号怎么看,硬盘编号怎么看
  3. 安卓应用出海指南--发布到Google Play
  4. php fckeditor,FCKeditor的安装(PHP)
  5. u盘dos启动盘制作工具 v9.36正式版
  6. 好用的android剪辑软件,最好用的视频剪辑app软件有哪些?自媒体人都在用的六款app软件...
  7. 抽象代数之拉格朗日定理的证明
  8. c语言如何用气泡法编程最大 最小值,[c语言冒泡排序法]C语言冒泡排序法详解
  9. 激光计算机的基本原理和特点,3D激光传感器的原理及特点
  10. 雷赛acc68c说明书_DMC2410C-A四轴通用型点位卡