很多朋友对于深度解析序列化技术:Protocol Buffer原理详解和不太懂,今天就由小编来为大家分享,希望可以帮助到大家,下面一起来看看吧!
目录
目录
1. 定义
结构化数据的一种数据存储格式(类似于XML、Json)
Google出品的Protocol Buffer(开源)目前有两个版本:proto2和proto3。因为proto3还是beta版本,所以这个解释是proto2
2. 作用
。通过序列化结构化数据(序列化),实现数据存储 / RPC 数据交换特征
序列化:将数据结构或对象转换为二进制字符串的过程反序列化:将序列化过程中生成的二进制字符串转换为数据结构或对象的过程
3. 特点
与常见的XML 和Json 数据存储格式相比,Protocol Buffer 有以下特点特性:Protocol Buffer 特性
4. 应用场景
大数据量传输、网络环境不稳定的需求场景数据存储、RPC 数据交换
如即时IM(QQ、微信)需求场景
总结
在传输数据量较大的需求场景中,Protocol Buffer比XML、Json更小、更快、更容易使用和维护!
5. 使用流程
关于Protocol Buffer的使用过程,详细请看我写的文章:快来看看Google出品的Protocol Buffer。不要只使用Json 和XML。
6. 知识基础
6.1 网络通信协议
序列化和反序列化是通信协议的一部分。通信协议采用分层模型: TCP/IP 模型(四层) OSI 模型(七层) 通信协议结构序列化/反序列化属于TCP/IP 模型应用层和OSI`模型表示层主要功能:
(序列化)将应用层的对象转换为二进制字符串(反序列化)将二进制字符串转换为应用层的对象因此,Protocol Buffer 属于TCP/IP 模型的应用层和OSI 的表示层模型
6.2 数据结构、对象与二进制串
在不同的计算机语言中,数据结构、对象和二进制字符串以不同的方式表示。
a. 对于数据结构和对象
对于面向对象语言(如Java):Object=Object=类的实例化; Java 中最接近的数据结构是POJO(普通Java 对象)或Javabean(仅具有setter/getter 方法的类)
对于半面向对象语言(如C++),对象=类,数据结构=结构体
b. 二进制串
对于C++来说,由于它有内存运算符,二进制字符串很容易理解:C++字符串可以直接被传输层使用,因为它们本质上是存储在内存中以" "结尾的二进制字符串。对于Java来说,二进制字符串=字节数组=byte[]byte属于Java的八种基本数据类型。二进制字符串很容易与字符串混淆:字符串是一种特殊的对象(Object)。对于跨语言通信来说,序列化的数据当然不能是某种语言的特殊数据类型。
6.3T - L - V的数据存储方式
定义
即Tag-Length-Value,标识 - 长度 - 字段值存储方式函数
用标识 - 长度 - 字段值代表单个数据,最后将所有数据拼接成一个字节流,从而实现数据存储的功能。长度可选地被存储。例如,存储Varint编码数据时不需要存储Length。
最终存储的字节流优势示意图
从上图可以看出,T-L-V存储方式的优点是可以将分隔符分开,不用字段,这样就减少了分隔符的使用。每个字段存储得非常紧凑,存储空间利用率非常高。如果未设置该字段值,则序列化时数据中不存在该字段。即对应字段不需要编码,解码时会设置为默认值。
7. 序列化原理解析
请记住Protocol Buffer三个关于数据存储的重要结论:
结论1
Protocol Buffer 对消息中的每个字段进行编码后,然后使用T-L-V 存储方法来存储数据。最终结果是二进制字节流序列化=编码数据+存储
结论2
Protocol Buffer针对不同的数据类型采用不同的序列化方式(编码方式和数据存储方式),如下图:
从上表可以看出该数据类型对应的编码方式:
对于存储Varint编码的数据,不需要存储字节长度Length,因此Protocol Buffer的实际存储方式是T - V;结论3:由于Protocol Buffer的数据字段值为独特编码方式T - L - V数据存储方式,这使得Protocol Buffer序列化后的数据量协议缓冲区这么小。下面,我将一一解释不同的编码方式和数据存储方式。
7.1 Wire Type = 0时的编码 数据存储方式
Wire Type=0 时的编码数据存储方法
7.1.1 编码方式:VarintZigzag
A.Varint编码方式介绍
i. 简介
定义:一种变长编码方法原理:用字节来表示数字:值越小的数字,使用越少的字节数表示功能:通过减少字符数执行数据压缩的部分,例如:
对于int32类型的数字,一般需要4个字节来表示;如果使用Varint编码,小的int32类型数字可以用1个字节表示。虽然大数字需要5 个字节来表示,但大多数情况下,消息的数字并不大,因此使用Varint 方法总是可以使用较少的字节来表示数字
ii. 原理介绍
源码分析private void writeVarint32(int n) {
int idx=0;
而(真){
如果((n ~0x7F)==0) {
i32buf[idx++]=(字节)n;
休息;
} 别的{
i32buf[idx++]=(字节)((n0x7F) |0x80);
//步骤1:取出字节串的最后7位
//对于上面取出的7位:最高位加1,形成一个字节
//如果是最后一次取出,则最高位加0,形成1个字节
n=7;
//步骤2:通过将整个字节串右移7位,继续从字节串末尾选择7位,直到全部取完。
}
}
trans_.write(i32buf, 0, idx);
//第三步:将上面形成的每个字节按顺序拼接成字节串
//即字节串是Varint编码的字节
}从上面可以看出:Varint中每个字节的最高位都有特殊的含义:
如果为1,则表示后续字节也是数字的一部分。如果为0,则表示这是最后一个字节,剩下的7位用来表示数字。因此,在使用Varint 解码时,只要读取到最高位,当该字节为0 时,就表示是Varint 的最后一个字节。
所以:
小于128的数字可以用1个字节表示;大于128的数字,比如300,会用两个字节来表示: 10101100 00000010 下面,我将用两个例子来说明Varint编码的使用。
用途:对数据类型Int32 的字段值296 和字段值104 进行Varint 编码。下面是编码过程。从上面可以看出Varint编码过程:
对于int32类型的数字,一般需要4个字节来表示;但是,使用Varint方法,对于非常小的Int32类型数字(小于256),可以用1个字节来表示;等等,比如300只能用1个字节来表示。需要2个字节
虽然大数字会需要5个字节来表示,但大多数情况下,消息不会有大数字,所以使用Varint 方法总是可以使用更少的字节来表示数字,从而更好地实现数据压缩,继续看如何解析Varint-编码字节。
Varint编码方式的不足
背景:在计算机中,负数一般表示为大整数,因为计算机将负数的符号位定义为该数的最高位。
问题:如果使用Varint编码来表示负数,那么必须需要5个字节(因为负数的最高位为1,会被视为大整数)解决方案:Protocol Buffer定义了sint32/sint64类型来表示负数。通过首先使用Zigzag 编码(将有符号数转换为无符号数),然后使用Varint 编码,用于减少编码字节数
Zigzag 编码用于表示负数。对于int32/int64类型(正数)的字段值,Protocol Buffer直接使用Varint编码。对于sint32/sint64类型(负数)的字段值,Protocol Buffer会首先使用Zigzag编码,然后使用Varint编码。总结:为了更好地减少表示负数时的字节数,Protocol Buffer 在Varint 编码的基础上添加了Zigzag 编码。下面将详细介绍Zigzag编码方法
B.Zigzag编码方式详解
i. 简介
定义:一种变长编码方法原理:用无符号数来表示有符号数;作用:允许绝对值小的数字用更少的字节来表示;特别是表示负数的数据可以更好地压缩
b. 原理
源码分析public int int_to_zigzag(int n)
//传入的参数n=传入的字段值的二进制表示(这里以负数为例)
//负数的二进制=符号位为1,其余数字为数绝对值原码按位取反;然后整个二进制数+1
{
返回(n1) ^ (n 31);
//对于sint 32数据类型,Zigzag编码过程如下:
//1.将二进制表示左移1位(左移=将整个二进制左移,并将低位补0)
//2. 将二进制表示右移31 位
//对于右移:
//第一位是二进制数1(有符号数),是算术右移,即右移后左边加1
//第一位是0的二进制数(无符号数),是逻辑左移,即右移后左边加0
//3. 对以上两者进行异或
//对于sint 64数据类型: return (n 1^ (n 63);
}
//附件:将Zigzag 值解码为整数值
公共int zigzag_to_int(int n)
{
返回(n 1)^-(n 1);
//右移时需要使用无符号移动,否则如果第一个数据位为1,则会加1。
}示例说明:Zigzag 编码-2:Zigzag 编码Zigzag 编码补充了Varint 编码在表示负数方面的缺点,从而更好地帮助Protocol Buffer 压缩数据。因此,如果提前预知字段值是可能取负数的时候,记得采用sint32 / sint64数据类型
总结
Protocol Buffer 采用Varint 和Zigzag 编码后,字段值占用的字节数大大减少。
7.1.2 存储方式:T - V
消息字段的标识号和数据类型。字段值在Protocol Buffer 中经过VarintZigzag 编码后,数据以T - V 模式存储。对于VarintZigzag 编码,T - L - V 中的字节长度Length 被省略。
Varint Zigzag 数据存储方式T-V 存储方式中的存储细节下面会详细介绍:TagValue
1. Tag
定义:Protocol Buffer 采用VarintZigzag 编码后报文字段标识号数据类型的取值函数:标识字段存储该字段的标识号(field_number)和数据类型(wire_type),即Tag=字段数据type (wire_type) + 标识号(field_number) 占用1个字节长度(如果标识号超过16,则多占用1个字节)。解包时,Protocol Buffer根据Tag将Value对应到消息中的字段。 //Tag的具体表达
标签=(field_number 3) |电线类型
//参数说明:
//field_number:对应.proto文件中消息字段的标识号,表示这是消息中的哪个字段
//field_number 3:表示field_number=Tag的二进制表示右移三位后的值
//将field_num左移3位不会导致数据丢失,因为表示范围仍然足够大,可以表示消息中的字段数量
//wire_type:表示字段的数据类型
//wire_type=Tag的二进制表示的最低三位
//wire_type的值
枚举WireType {
电线类型_VARINT=0,
电线类型_FIXED64=1,
WIRETYPE_LENGTH_DELIMITED=2,
WIRETYPE_START_GROUP=3,
WIRETYPE_END_GROUP=4,
电线类型_固定32=5
};
//从上面可以看出,`wire_type`最多占用3位内存空间(因为3位足以表示0-5二进制)
//以下是wire_type对应的数据类型。表wire_type对应数据类型示例说明//消息对象
留言人
{
需要int32 id=1;
//电线类型=0,field_number=1
所需的字符串名称=2;
//线路类型=2,field_number=2
}
//如果标签的二进制数=0001 0010
标识号=field_number=field_number 3=右移3 位=0000 0010=2
数据类型=wire_type=最低三位数字代表=010=2
2. Value
Protocol Buffer使用VarintZigzag编码的消息字段的值
7.1.3 实例说明
下面通过一个例子来说明整个编码过程:
消息描述消息测试
{
需要int32 id1=1;
需要int32 id2=2;
}
//在代码中给id1附加一个字段值:296
//在代码中给id2附加一个字段值:296
测试.setId1(300);
测试.setId2(296);
//编码结果为:二进制字节流=[8, -84, 2, 16, -88, 2] 整个编码过程如下编码过程
7.2 Wire Type = 1 5时的编码数据存储方式
Wire Type=1 5 64 时的编码数据存储方式( 32)位编码方式比较简单:编码后的数据有固定大小=64位(8字节)/32位(4字节)。在这两种情况下,高位最后出现,低位先出现。
使用T-V模式进行数据存储,同上。
7.3 Wire Type = 2时的 编码 数据存储方式
Wire Type=2 时的编码数据存储方式对于编码方式:编码方式数据存储方式:T - L - V 数据存储图这里主要讲解三种数据类型:
字符串类型嵌套消息类型(Message)通过packed修饰的重复字段(即打包重复字段)
1. String类型
字段值(即V)采用UTF-8编码
编码存储方式示例:message Test2
{
所需字符串str=2;
}
//将str 设置为:测试
Test2.setStr("测试")
//将protobuf编码序列化后的数据以二进制形式输出
//输出为:18,7,116,101,115,116,105,110,103实例
2. 嵌套消息类型(Message)
存储方式:T - L - V 外部消息的V为T中嵌套消息的字段-L - V中嵌套了一系列的T-L-V编码方式:字段值(即V)根据字段的数据类型使用不同的编码方式来编码存储方式示例。
定义以下嵌套消息:message Test2
{
必需的字符串str=1;
需要int32 id1=2;
}
消息测试3 {
要求的测试2 c=1;
}
//将Test2中的字段str设置为:testing
//将Test2中的字段id1设置为:296
//编码后的字节为:10, 12, 18, 7, 116, 101, 115, 116, 105, 110, 103, 16, -88, 2编码存储方式如下
编码存储方式
3. 通过packed修饰的repeat字段
重复修改字段有两种表达方式:
留言测试
{
重复int32 汽车=4;
//表达式1:不带packed=true
重复int32 汽车=4 [packed=true];
//表达式2:with Packed=true
//proto 2.1 可用
//区别在于:是否连续存储重复类型数据
}
//给代码中的`repeated int32 Car`附加3个字段值:3, 270, 86942
测试.setCar(3);
测试.setCar(270);
测试.setCar(86942);背景:对于相同的重复字段和多个字段值,它们的Tags是相同的,即数据类型和标识号相同。重复类型可以看成是一个数组。
问题:如果使用传统的多T-V对存储(不带packed=true),会导致标签冗余,即同一个标签被存储多次;不使用pack的存储方式的解决办法:使用packed=True的重复字段存储方式是,同一个Tag只存储一次,将重复字段下所有字段值的长度相加,连续存储重复字段值到形成一个大的Tag-Length-Value-Value-Value对,即T-L-V-V-V对。带pack的存储方式采用packed=true的重复字段存储方式,可以更好的压缩序列化数据长度。
特别注意
Protocol Buffer的打包修改仅用于重复字段或基本类型的重复字段用于其他字段。编译.proto文件时会报错。
8. 特别注意
注1:如果必填字段尚未设置为字段值,则在IsInitialized( )中执行初始化检查时会报错并提示失败,因此必须将必填字段设置为字段值。
注2:序列化顺序是按照Tag标识号从小到大编码的,与.proto文件中字段定义的数据无关。
注3:T-V数据存储方式保证了Protobuf版本兼容性:高-低或低-高可以适应。如果新版本添加了必填字段,旧版本在解码数据时会认为IsInitialized() 失败,所以请谨慎使用必填字段
9. 使用建议
基于上面序列化原理的分析,我总结了以下Protocol Buffer的使用建议
以下建议可以有效减少序列化后数据的大小:
建议1:多用optional或repeated修饰符因为如果可选或重复字段没有设置字段值,那么序列化时数据中根本不存在该字段,即不需要对对应字段进行编码,会被设置为解码时的默认值。
建议2:字段标识号(Field_Number)尽量只使用 1-15,且不要跳动使用因为Tag中的Field_Number需要字节空间。如果Field_Number为16,则Field_Number的编码会占用2个字节,编码时Tag也会占用更多的字节;如果将字段标识号定义为一个不断增加的值,将获得更好的编码和解码性能
建议3:若需要使用的字段值出现负数,请使用sint32 / sint64,不要使用int32 / int64因为当使用sint32/sint64数据类型表示负数时,先使用Zigzag编码,然后使用Varint编码,可以更有效地压缩数据。
建议4:对于repeated字段,尽量增加packed=true修饰由于添加了packed=true来修改repeated字段,因此采用连续数据存储方式,即T - L - V - V -V方式。
10. 序列化 反序列化过程
Protocol Buffer 除了序列化和反序列化后数据体积小之外,序列化和反序列化的速度也很快。下面我讲解一下序列化和反序列化的序列化过程
10.1Protocol Buffer的序列化 反序列化过程
序列化过程如下:判断各个字段是否有设定值,只有有值才会进行编码。根据字段标识号数据类型,字段值将采用不同的编码方法进行编码。因为:
一个。编码方法简单(只需简单的数学运算=位移等)
b.使用Protocol Buffer自身的框架代码 和 编译器完成接头
所以协议B
uffer的序列化速度非常快。反序列化过程如下:调用 消息类的parseFrom(input)解析从输入流读入的二进制字节数据流从上面可知,Protocol Buffer解析过程只需要通过简单的解码方式即可完成,无需复杂的词法语法分析,因此 解析速度非常快。 将解析出来的数据 按照指定的格式读取到Java、C++、Phyton对应的结构类型中由于: a. 解码方式简单(只需要简单的数学运算 = 位移等等) b. 采用Protocol Buffer自身的框架代码 和 编译器共同完成 所以Protocol Buffer的反序列化速度非常快。10.2 对比于XML的序列化 & 反序列化过程
XML的反序列化过程如下: 从文件中读取出字符串将字符串转换为XML文档对象结构模型从XML文档对象结构模型中读取指定节点的字符串将该字符串转换成指定类型的变量上述过程非常复杂,其中,将XML文件转换为文档对象结构模型的过程通常需要完成词法文法分析等大量消耗 CPU 的复杂计算。 因为序列化 & 反序列化过程简单,所以序列化 & 反序列化过程速度非常快,这也是Protocol Buffer效率高的原因11.总结
Protocol Buffer的性能好,主要体现在序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其原因如下: 序列化速度快的原因: a. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等) b. 采用Protocol Buffer自身的框架代码 和 编译器共同完成 序列化后的数据量体积小(即数据压缩效果好)的原因: a. 采用了独特的编码方式,如Varint、Zigzag编码方式等等 b. 采用T - L - V的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑Carson带你学序列化Protocol Buffer系列文章快来看看Google出品的Protocol Buffer,别只会用Json和XML了 Carson带你学序列化:手把手教你如何安装Protocol Buffer Carson带你学序列化:全面详解ProtocolBuffer语法 Carson带你学序列化:Google出品的序列化神器Protocol Buffer使用指南 Carson带你学序列化:Protocol Buffer序列化原理大揭秘-为什么性能这么好? Carson带你学序列化:深入源码分析Protocol Buffer Carson带你学序列化:深入分析JSON多种解析方式(Gson、AS自带org.json、Jackson) Carson带你学序列化:深入分析XML多种解析方式(DOM、SAX、PULL)好了,关于深度解析序列化技术:Protocol Buffer原理详解和的问题到这里结束啦,希望可以解决您的问题哈!
【深度解析序列化技术:Protocol Buffer原理详解】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
终于有个大佬讲清楚了 Protocol Buffer 的序列化原理!我在这方面一直困惑很久。
有9位网友表示赞同!
感觉 Carson 一直都讲解的很到位,这次也不例外,我已经开始学习 Protobuf 了!
有20位网友表示赞同!
Protocol Buffer 是个非常实用的工具,这次的课程肯定能派上大用场。
有19位网友表示赞同!
希望能详细讲解一下 Protocol Buffers 的使用场景和优势。
有8位网友表示赞同!
我之前对序列化不太了解,希望 Carson 可以简单科普一下相关概念。
有6位网友表示赞同!
期待学习 Protocol Buffer 究竟是怎么把数据压缩成高效格式的!
有10位网友表示赞同!
Protobuf 在实际项目中应用广泛吗?可以分享一些经典案例吗?
有10位网友表示赞同!
Carson 的讲解总是很清晰易懂,相信这次也不例外!
有8位网友表示赞同!
我也想过学习 Protocol Buffer,现在课程开始真是太好了!
有16位网友表示赞同!
期待能了解 Protocol Buffers 在不同编程语言下的使用方式。
有10位网友表示赞同!
Protocol Buffer 真的比其他序列化方案更优吗?可以分享一下比较结果?
有16位网友表示赞同!
最近在研究分布式系统, Protobuf 听起来很有用,一定要学学!
有9位网友表示赞同!
我有点基础的编程知识,应该能跟得上 Carson 的讲解吧?
有10位网友表示赞同!
希望课程能够提供一些实战案例,加深我的理解。
有16位网友表示赞同!
可以分享一下学习 Protocol Buffer 的最佳资源吗?
有6位网友表示赞同!
Protocol Buffer 学习起来会不会很难?需要什么前提知识?
有18位网友表示赞同!
我对序列化技术一直很感兴趣,这次课程正好可以让我更深入了解!
有13位网友表示赞同!
Carson 的课程总是质量很高,期待这次也不例外!
有20位网友表示赞同!
希望学习完 Protocol Buffer 之后能自己开发一些应用测试。
有14位网友表示赞同!