ProtocolBuffer学习
原文链接:
目录
在学习的过程当中,参考了很多高质量的博客,在此进行感谢,如果在内容中引用您的内容不当,请联系我删除,表示抱歉。
一、它是什么?
1、简介
Protocol Buffer(以下简称Protobuf)是由Google设计的一种高效、轻量级的信息描述格式,它具有语言中立、平台中立、高效、可扩展等特性。Protocol Buffer诞生之初是为了解决索引服务器端的请求、响应新旧协议(高低版本)兼容性问题,正如它的字面意思所示-“协议缓冲区”;后被Google开源出来,逐渐发展成用于传输数据场景。相比于json、xml,Protobuf的编码长度更短、传输效率更高。它非常适合用来做数据存储、RPC数据通信等工作。
Protobuf目前有两个版本:proto2和proto3,本章按照proto3的版本来进行学习记录的。
2、作用
Protobuf起初用来解决谷歌内部服务器端的请求与响应新旧协议兼容性问题,后被谷歌(2008年)开源出来,经过不断优化,现在Protobuf非常适合用来做数据存储、RPC数据通信等工作。
Protobuf的工作流程很简单,一句话概括就是:通过将结构化数据
进行序列化,从而实现数据存储
和RPC数据交换
,对端拿到数据做反序列化处理加载到内存进行数据使用的功能。
3、特点
简介里有提到Protobuf的优点,这里将优缺点详细罗列下。
a.优点
- 性能方面:
- 体积小:序列化后,数据可缩小约3倍;
- 序列化速度快:比XML、json快,从吞吐量角度来讲,Protobuf要比json高5倍,比XML速度就更快了;
- 传输速度快:由于序列化后数据体检变小,所以在传输时,同样带宽下,数据包变小,传输速度也相应变少
- 使用方面:
- 使用方便:本地安装protoc编译器或者在idea上安装protoc插件,即可编译proto文件生成各语言的源码进行使用;
- 维护成本低:多个平台只需要维护一套对象协议文件(.proto文件)即可;
- 兼容性好,即扩展性好,在不改变旧协议格式内容的前提下,就可以直接对数据结构进行更新;
- 向前兼容性好,老协议可以解析新协议内容,但是解析新的内容时,会丢掉协议中新增部分的数据
- 向后兼容性好,新协议可以解析旧协议内容,对于新增字段的内容将会使用默认值
- 加密型好:HTTP传输内容抓包只能看到字节;
- 使用范围:
- 跨平台
- 跨语言
b.缺点
- 功能方面:
- 不适合用于对基于文本的标记语言(如HTML)建模,因为文本不适合描述数据结构
- 数据结构不够丰富,不能支持Java的一些数据结构,例如Java中Date等对象
- 其他方面:
- 解释性较差,以二进制的形式进行存储和传输,可读性非常差,只能通过查看.proto文件才能够了解到数据结构
4、性能对比
使用JMH性能测试工具,对Protobuf、XML、fastjson、jackson等序列化协议进行了测试,以吞吐量为单位,来对比下各序列化协议性能的优劣
a.测试基准环境
硬件资源
机型: MacBook Pro(2021年)
芯片: Apple M1 Pro
内存: 16 GB
java
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
序列化协议工具版本
protobuf版本: <protobuf.java.version>3.19.4</protobuf.java.version>
protobuf框架protostuff版本
<protostuff.core.version>1.8.0</protostuff.core.version>
<protostuff.runtime.version>1.8.0</protostuff.runtime.version>
fastjson版本: <fastjson.version>1.2.78</fastjson.version>
jackson版本: <jackson.version>2.12.3</jackson.version>
kryo版本: <kryo.version>5.3.0</kryo.version>
fst版本: <fst.version>2.57</fst.version>
xstream版本: <xstream.version>1.4.19</xstream.version>
b.测试结果
测试源码我放到自己的github上了,有兴趣的话可以将其克隆到本地运行一下,这里就直接上图了。
序列化结果图
结论如下图所示,比较直观
反序列化结果图
序列化后字节数对比
结论(空间占比从小到大):kryo < fst < protobuf < protostuff < fastjson < jackson < jdk < xml
kryo serialized data size:1253
fst serialized data size:1468
protobuf serialized data size:1500
protostuff serialized data size:1513
protostuff from utils serialized data size:1513
fastjson serialized data size:2968
jackson serialized data size:2984
jdk serialized data size:4049
xml serialized data size:4610
5、应用场景
- 跨平台的RPC数据传输
- 对数据量大小有要求
- 对性能有要求
二、如何使用?
1、使用流程
使用步骤如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-42ptBg1D-1652596106144)(http://cdn.markuszhang.com/img/Protobuf%E4%BD%BF%E7%94%A8%E6%B5%81%E7%A8%8B%E5%9B%BE.png)]
a.IDEA插件
对于通过IDEA插件
的方式源码中有,就不详细说了。在编写完.proto文件后,直接通过maven插件点击编译即可完成编译。
b.命令行
-
下载安装包:安装包下载,选择一个你需要的版本进行下载,
-
安装——打开终端,依次执行以下命令
// pre 如果电脑没有安装HOMEBREW工具,就先安装该工具,安装过的就跳过此步
➜ ~ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
// 安装protobuf环境的命令
➜ ~ brew install autoconf automake libtool curl
// Step1:安装 Protocol Buffer 依赖
// 注:Protocol Buffer 依赖于 autoconf、automake、libtool、curl
➜ ~ cd env/path/protobuf-3.19.4
➜ protobuf-3.19.4
// Step2:进入 Protocol Buffer安装包 解压后的文件夹(我的解压文件放在桌面)
➜ protobuf-3.19.4 ./autogen.sh
// Step3:运行 autogen.sh 脚本
➜ protobuf-3.19.4 ./configure
// Step4:运行 configure.sh 脚本
➜ protobuf-3.19.4 make
// Step5:编译未编译的依赖包
➜ protobuf-3.19.4 make check
// Step6:检查依赖包是否完整
➜ protobuf-3.19.4 make install
// Step7:开始安装Protocol Buffer
- 检验Protobuf是否安装成功
➜ my-blog protoc --version
libprotoc 3.19.4
2、实战记录
- 编写 .proto 文件
- 使用IDEA插件编译 or 使用终端命令编译 文件
- 编译后的源码加入到工程中
a.编写.proto文件
在编写.proto文件之前,先学习下proto的语法(基于proto3)
protobuf支持的数据类型
.proto Type | Notes | Java Type |
---|---|---|
double | double | |
float | float | |
int32 | 使用可变长编码方式,但是对于负数的编码效率是非常低的,如果你的数据中有负数,请采用sint32 | int |
int64 | 使用可变长编码方式,但是对于负数的编码效率是非常低的,如果你的数据中有负数,请采用sint64 | long |
uint32 | 使用可变长编码方式 | int[1] |
uint64 | 使用可变长编码方式 | long[1] |
sint32 | 使用可变长编码方式,有符号的int类型值,该类型比普通的int32类型在编码上更高效 | int |
sint64 | 使用可变长编码方式,有符号的long类型值,该类型比普通的int64类型在编码上更高效 | long |
fixed32 | 使用固定4字节编码方式,如果你的数据值经常比228更大的话,此类型编码会比uint32类型更高效 | int[1] |
fixed64 | 使用固定8字节编码方式,如果你的数据值经常比256更大的话,此类型编码会比uint64类型更高效 | long[1] |
sfixed32 | 使用固定4字节编码方式 | int |
sfixed64 | 使用固定8字节编码方式 | long |
bool | boolean | |
string | 字符串类型,它总是包含 UTF-8编码 或者7-bit ASCII文本,并且长度不能超过232 | String |
bytes | 可以包含长度不超过232的任意字节序列 | ByteString |
protobuf语法
protobuf语法可以通过这边文章【戳此】来学习,比较详细了,这里就不赘述了。
编写.proto文件
编写一个简单示例,用于编译演示:
/*
ProtoStudy.proto
头部相关声明
*/
syntax = "proto3"; // 语法版本为protobuf3.0
package com.markus.bp.domain; // 定义包名
//import "common.proto"; // 导入common.proto
option java_package = "com.markus.bp.domain"; // 指定java包
message TypeStudy{
double double_type = 1;
int32 int_type = 2;
int64 long_type = 3;
}
b.编译.proto文件
上面提到了用IDEA插件进行文件编译,下面记录下用终端命令来进行文件编译:
# 进入到对应编写文件的路径中
➜ resources git:(master) ✗ cd proto
➜ proto git:(master) ✗ ls
ProtoStudy.proto UserProto.proto
# 对ProtoStudy.proto文件进行编译,输出到指定目录下
➜ proto git:(master) ✗ protoc --java_out=../../java/ ProtoStudy.proto
下图即为编译成功后的Java文件
c.使用编译后的Java类实现数据序列化与反序列化
package com.markus.bp.study;
import com.google.protobuf.InvalidProtocolBufferException;
import com.markus.bp.domain.ProtoStudy;
/**
* @author: markus
* @Description: protobuf 使用学习
* @Blog: http://markuszhang.com/
*/
public class ProtoUseStudy {
public static void main(String[] args) throws InvalidProtocolBufferException {
ProtoStudy.TypeStudy typeStudy = ProtoStudy.TypeStudy.newBuilder()
.setDoubleType(1.03D)
.setIntType(123)
.setLongType(123L)
.build();
byte[] bytes = typeStudy.toByteArray();// 将对象序列化为字节数据
ProtoStudy.TypeStudy typeStudyFromDeserialize = ProtoStudy.TypeStudy.parseFrom(bytes);// 反序列化
System.out.println(typeStudyFromDeserialize);
}
}
/*
double_type: 1.03
int_type: 123
long_type: 123
Process finished with exit code 0
*/
三、原理分析
1、知识储备
a.网络通信协议
TODO–>网络相关的文章
OSI七层模型、TCP/IP四层模型
b.序列化与反序列化
序列化与反序列化的过程是属于TCP/IP四层模型中的应用层、OSI七层模型的表示层。
序列化与反序列化的作用就是在两台机器通信之间建立的一种数据表示的一种协议,能够让提供信息的机器制造的数据在接收信息的机器上能够正确的解析识别。
- 序列化:把应用层的对象转换成二进制串(编码+存储);
- 反序列化:将二进制串转换成应用层的对象(解码+赋值)。
c.数据结构、对象与二进制串
从Java维度来讲,对象 = Object = 类实例;二进制串 = byte[];数据结构类似于POJO(Plain Old Java Object)或者JavaBean(只有getter和setter方法)
d.T-L-V数据存储格式
Tag-Length-Value是T-L-V的全称,即标识-长度-值的存储方式(其中长度length是可选的,比如Varint编码数据就不需要存储Length,下面会提到)
数据可以通过Tag-Length-Value的方式来进行表示,多个数据以此种类型表示并进行拼接最终形成一个字节流,从而能实现数据的存储和通信
示意图如下:
Tag是经过PB采用Varint & Zigzag编码后的消息字段标识号 & 数据类型的值;字段标识号就是.proto文件中的字段序号,数据类型值就是Wire Type值
public final class WireFormat {
// wire type取值如下
public static final int WIRETYPE_VARINT = 0;
public static final int WIRETYPE_FIXED64 = 1;
public static final int WIRETYPE_LENGTH_DELIMITED = 2;
public static final int WIRETYPE_START_GROUP = 3;
public static final int WIRETYPE_END_GROUP = 4;
public static final int WIRETYPE_FIXED32 = 5;
}
通过这种格式进行数据拼凑有以下特点:
- 不需要分隔符就能区分字段,避免了分割符的使用;
- 数据十分的紧凑,空间利用率非常高;
- 对于没有设置值的字段不会进行编码,在字节流中完全不存在。
- 只有在解码的时候,如果这个字段不存在,会设置默认值
e.小端序与大端序
这里就是说明一下 x86系列CPU是小端序,即低位字节存入低地址、高位字节存入高地址;Java数据和网络传输采用大端序,即高位字节存入低地址、低位字节存储高地址
什么是高地址?什么是低地址?
:根据不同机型的堆栈设计,有两种情况:一是每入栈一个数,栈顶地址加1,每出栈一个数,栈顶地址减1,即堆栈区是由内存的低地址向高地址。另一种是每入栈一个数,栈顶地址减1,每出栈一个数,栈顶地址加1,即堆栈区是由内存的高地址向低地址。
[java – Big Endian and Little Endian 大端和小端概念讲解及如何转换](https://blog.csdn.net/penriver/article/details/124765592#:~:text=java 全部为大端 (与平台无关) : Java二进制文件中的所有内容都以 大端顺序,存储。 这意味着如果您只使用Java,那么所有文件在所有平台 (Mac、PC、UNIX等)上的处理方式都是相同的。 C语言默认是小端模式 :用C语言编写的程序通常使用 小端顺序)
2、PB的序列化与反序列化过程
序列化与反序列化过程直接通过自身框架和编译器就能完成,编译器将编写的.proto文件编译成相应语言的代码,通过方法调用就能实现序列化和反序列化。
a.PB序列化过程
序列化使用:protoObject.toByteArray();
- 判断每个字段是否设置了值,只有在有值的情况下才会进行相应的编码;
- 根据字段的
标识号
以及数据类型
,使用相对应的数据编码方式进行编码。
b.PB反序列化过程
反序列化使用:ProtoObject.parseFrom(byte[]);
- 通过将字节流读入,将字节流解码;
- 将解析出来的数据,转换成相应语言的对象对于的数据结构里。
3、PB的序列化与反序列化原理
上面简单说了下Protobuf对数据进行序列化和反序列化的过程,接下来记录下它内部的原理。
核心的类:CodedOutputStream、CodedInputStream
前面我们说到,序列化就是对数据编码+存储的一个过程。Protobuf内部不同的数据类型,其编码格式以及存储方式如下图所示:
Wire Type值 | 编码方式 | 编码长度 | 存储方式 | 相应的数据类型 |
---|---|---|---|---|
0 | Varint (负数时,采用Zigzag辅助编码) | 变长(1-10字节) | T-V | int32,int64,uint32,uint64,bool,enum,sint32,sint64 |
1 | 64-bit | 固定8字节 | T-V | fixed64,sfixed64,double |
2 | Length-delimi | 变长 | T-L-V | string,bytes,embedded messages,packed repeated fields |
3 | Start Group | 已弃用 | 已弃用 | Group(已弃用) |
4 | End Group | 已弃用 | 已弃用 | Group(已弃用) |
5 | 32-bit | 固定4字节 | T-V | fixed32,sfixed64,float |
a.Varint编码(wiretype=0)
- 一种可变长的编码方式
- 值越小,使用它编码后字节数越少,这样就起到了对数据进行压缩的作用
- 不适合对负数用此方式编码
接下来我们看下源码中的核心流程
// proto编译器编译后的类,比如编写的.proto文件有三个字段,分别是double类型、int32类型以及int64类型,对应编译器就生成了这样的一段代码
// 下面这段代码也阐述之前的一个点:字段没有被设置值时,则会跳过不管
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
if (java.lang.Double.doubleToRawLongBits(doubleType_) != 0) {
// 字段值不为空时,调用此方法对double类型数值进行编码
output.writeDouble(1, doubleType_);
}
if (intType_ != 0) {
// 字段值不为空时,调用此方法对int32类型数值进行编码
output.writeInt32(2, intType_);
}
if (longType_ != 0L) {
// 字段值不为空时,调用此方法对int64类型数值进行编码
output.writeInt64(3, longType_);
}
unknownFields.writeTo(output);
}
// 这里以int32类型来看Varint编码流程
// CodedOutputStream#writeInt32
public final void writeInt32(final int fieldNumber, final int value) throws IOException {
//写入标识,也就是Tag-Length-Value中的Tag(此方法内部就是将字段号+wiretype用一个或多个字节表示,低三位表示wiretype,高位表示字段号,字段越大,占用的字节数越多)
writeTag(fieldNumber, WireFormat.WIRETYPE_VARINT);
//开始对数据进行编码
writeInt32NoTag(value);
}
// CodedOutputStream#writeInt32NoTag
public final void writeInt32NoTag(int value) throws IOException {
if (value >= 0) {
// 正数,直接使用Varint编码,我们先分析这个方法
writeUInt32NoTag(value);
} else {
// 负数,就会通过int64类型Varint编码来做,会始终使用10字节表示数据(想要避免这种情况就采用sint32来编码负数)
// Must sign-extend.
writeUInt64NoTag(value);
}
}
public final void writeUInt32NoTag(int value) throws IOException {
if (HAS_UNSAFE_ARRAY_OPERATIONS
&& !Android.isOnAndroidDevice()
&& spaceLeft() >= MAX_VARINT32_SIZE) {
// xxx 此处代码不关心
} else {
// Varint编码的核心流程
try {
while (true) {
// 判断value(31bit-8bit)是否还有有效数字,如果没有的话,说明这次循环是最后一次右移操作
if ((value & ~0x7F) == 0) {
// 将value转为byte类型值,低七位为有效数字,第八位补0得到1字节数据
buffer[position++] = (byte) value;
return;
} else {
// <1> (value & 0x7F) --> 得到字节串低七位数据
// <2> (value & 0x7F) | 0x80 --> 高位补1得到1字节数据
buffer[position++] = (byte) ((value & 0x7F) | 0x80);
// <3> 将字节串无符号右移7位
value >>>= 7;
}
}
} catch (IndexOutOfBoundsException e) {
throw new OutOfSpaceException(
String.format("Pos: %d, limit: %d, len: %d", position, limit, 1), e);
}
}
}
// CodedInputStream#readRawVarint64SlowPath 解码的核心代码
// 在调用此方法的时候就已经确定了当前字段值的字节串的起始位置
long readRawVarint64SlowPath() throws IOException {
long result = 0;
for (int shift = 0; shift < 64; shift += 7) {
//<1> 顺序读入字节
final byte b = readRawByte();
//<2> (b & 0x7F) 将当前字节最高位置为0
//<3> (b & 0x7F) << shift 左移shift位 (这里也是Protobuf采用小端序存储,转换为Java大端序存储的处理方法)
//<4> 将result 与(long) (b & 0x7F) << shift做或等操作
result |= (long) (b & 0x7F) << shift;
if ((b & 0x80) == 0) {
return result;
}
}
throw InvalidProtocolBufferException.malformedVarint();
}
看完源码后,我们可能会有一个疑问:在编码时,在高位有数据时,用7bit有效位+0组成1字节数据;高位无数据时用7bit有效位+1组成1字节数据呢?
这个问题其实也侧面反映了为什么Varint编码不需要T-L-V
中的L
了,它会将数值每7位为一组,第8位表示符号位,告诉程序当前字段数据是否到最后了,在解析时,遍历字节时,当判断第8位为1的时候说明此字段数据还没有结束,直到遍历到一个字节的高8位是0时结束,这样就省去Length来代表数据的长度了。
下面通过两个例子来顺一下源码的意思:
从上图可以看出:
- 对于int32类型值来说,普通存储的话得需要4字节,而通过Varint编码可以让其降到更低;
- 当数据超过228时,才会使用5字节来表示4字节数据;当数据在小于221时,数据会使用3字节以内来表示数据;也就是说当数据小于221时,就达到了数据压缩的效果。
接下来再来看下Varint解码的过程
b.Zigzag编码(wiretype=0)
上面记录了Varint编码的过程,我们知道越小的数据,编码后占用的字节数越少,那么问题来了,当数据表示的是负数时怎么办呢?负数的最高位为1,如果单纯的用Varint编码的话,负数会被认为成一个很大的数,会使用10字节表示数据,那怎么办呢?Zigzag就解决了这个问题。
- Zigzag也是一种可变长编码方式
- 通过使用
无符号数
来表示有符号数字
,这样使得绝对值小的数据就可以采用较少字节
来表示
针对上述情况,protobuf通过将数据先进行Zigzag编码再进行Varint编码,我们将字段类型更换为sint32类型:
/*
头部相关声明
*/
syntax = "proto3"; // 语法版本为protobuf3.0
package com.markus.bp.domain; // 定义包名
//import "common.proto"; // 导入common.proto
option java_package = "com.markus.bp.domain"; // 指定java包
message TypeStudy{
double double_type = 1;
int32 int_type = 2;
int64 long_type = 3;
sint32 sint_type = 4;// 本次新增
}
我们来看下示例:
public class ProtoUseStudy {
public static void main(String[] args) throws InvalidProtocolBufferException {
ProtoStudy.TypeStudy typeStudy = ProtoStudy.TypeStudy.newBuilder()
.setDoubleType(1.03D)
.setIntType(123)
.setLongType(123L)
.setSintType(-2) // 本次调试这个源码
.build();
byte[] bytes = typeStudy.toByteArray();// 将对象序列化为字节数据
ProtoStudy.TypeStudy typeStudyFromDeserialize = ProtoStudy.TypeStudy.parseFrom(bytes);
System.out.println(typeStudyFromDeserialize);
}
}
我们来看下源码:
// CodedOutputStream#writeSInt32
public final void writeSInt32(final int fieldNumber, final int value) throws IOException {
//<1> encodeZigZag32(value) --> 使用ZigZag编码
//<2> 和int32流程一样(本次不赘述了)
writeUInt32(fieldNumber, encodeZigZag32(value));
}
// CodedOutputStream#encodeZigZag32
public static int encodeZigZag32(final int n) {
// Note: the right-shift must be arithmetic --> 右移操作必须是算术右移,也就是有符号右移,最高位为1时,右移高位补1,否则补0
return (n << 1) ^ (n >> 31);
//<1> n << 1 --> 将二进制数据左移1位
//<2> n >> 31 --> 将二进制数据右移31位
//<3> 两者结果做异或运算
}
// CodedInputStream#decodeZigZag32
public static int decodeZigZag32(final int n) {
return (n >>> 1) ^ -(n & 1);
}
Zigzag编码流程图如下(更直观)
Zigzag编码就是对Varint编码的补充,从而更好的进行数据压缩。所以当我们提前已经知道我们的数据可能有负数时,应该提早采用sint32/sint64
数据类型
c.64(32)-bit固定编码(wiretype=1&5)
64(32)-bit编码方式:编码后的数据具备固定大小 = 64位(8字节)/32位(4字节)
使用类型:
- 64-bit: fixed64 sfixed64 double
- 32-bit: fixed32 sfixed32 int
这里我们拿double(64-bit编码)来举例:
// CodedOutputStream#writeDouble
public final void writeDouble(final int fieldNumber, final double value) throws IOException {
//<1> Double.doubleToRawLongBits(value) 将double值转为8字节表示
//<2> writeFixed64()进行固定64-bit编码
writeFixed64(fieldNumber, Double.doubleToRawLongBits(value));
}
// CodedOutputStream#writeFixed64
public final void writeFixed64(final int fieldNumber, final long value) throws IOException {
//<1> 写入Tag
writeTag(fieldNumber, WireFormat.WIRETYPE_FIXED64);
//<2> 对Value进行编码
writeFixed64NoTag(value);
}
// 固定编码比较简单(高位在后,低位在前)
public final void writeFixed64NoTag(long value) throws IOException {
try {
// 从低位开始,依次进行编码
buffer[position++] = (byte) ((int) (value) & 0xFF);
buffer[position++] = (byte) ((int) (value >> 8) & 0xFF);
buffer[position++] = (byte) ((int) (value >> 16) & 0xFF);
buffer[position++] = (byte) ((int) (value >> 24) & 0xFF);
buffer[position++] = (byte) ((int) (value >> 32) & 0xFF);
buffer[position++] = (byte) ((int) (value >> 40) & 0xFF);
buffer[position++] = (byte) ((int) (value >> 48) & 0xFF);
buffer[position++] = (byte) ((int) (value >> 56) & 0xFF);
} catch (IndexOutOfBoundsException e) {
throw new OutOfSpaceException(
String.format("Pos: %d, limit: %d, len: %d", position, limit, 1), e);
}
}
通过源码,我们也可以看到固定编码方式也是通过Tag-Value的方式进行存储的。
d.Length-delimi编码(wiretype=2)
- string、bytes类型编码
- message嵌套编码
- repeated编码
string、bytes类型编码:
这次我们通过string类型来分析源码:
// GeneratedMessageV3#writeString
protected static void writeString(
CodedOutputStream output, final int fieldNumber, final Object value) throws IOException {
// 两种编码方式
// string类型
if (value instanceof String) {
output.writeString(fieldNumber, (String) value);
} else {
// bytes类型
output.writeBytes(fieldNumber, (ByteString) value);
}
}
// GeneratedMessageV3#writeStringNoTag
public final void writeStringNoTag(String value) throws IOException {
final int oldPosition = position;
try {
// UTF-8 byte length of the string is at least its UTF-16 code unit length (value.length()),
// and at most 3 times of it. We take advantage of this in both branches below.
// UTF-16 固定2字节
// UTF-8 有可能1字节、2字节、3字节,但最多不超过3个。
// 如果全部为英文或大部分都是英文的话,那么UTF-8占优势;相反如果全部是中文或者大部分是中文的话,UTF-16占优势
final int maxLength = value.length() * Utf8.MAX_BYTES_PER_CHAR;
final int maxLengthVarIntSize = computeUInt32SizeNoTag(maxLength);
final int minLengthVarIntSize = computeUInt32SizeNoTag(value.length());
if (minLengthVarIntSize == maxLengthVarIntSize) {
position = oldPosition + minLengthVarIntSize;
int newPosition = Utf8.encode(value, buffer, position, spaceLeft());
// Since this class is stateful and tracks the position, we rewind and store the state,
// prepend the length, then reset it back to the end of the string.
position = oldPosition;
int length = newPosition - oldPosition - minLengthVarIntSize;
// 长度采用Varint编码
writeUInt32NoTag(length);
position = newPosition;
} else {
int length = Utf8.encodedLength(value);
// 长度采用Varint编码
writeUInt32NoTag(length);
position = Utf8.encode(value, buffer, position, spaceLeft());
}
} catch (UnpairedSurrogateException e) {
// Roll back the change - we fall back to inefficient path.
position = oldPosition;
// TODO(nathanmittler): We should throw an IOException here instead.
inefficientWriteStringNoTag(value, e);
} catch (IndexOutOfBoundsException e) {
throw new OutOfSpaceException(e);
}
}
message嵌套编码
// CodedOutputStream#writeMessage
public final void writeMessage(final int fieldNumber, final MessageLite value)
throws IOException {
writeTag(fieldNumber, WireFormat.WIRETYPE_LENGTH_DELIMITED);
writeMessageNoTag(value);
}
// CodedOutputStream#writeMessageNoTag
public final void writeMessageNoTag(final MessageLite value) throws IOException {
//<1> 采用varint编码写入长度
writeUInt32NoTag(value.getSerializedSize());
//<2> 其实就递归调用write,进行嵌套message里的类型编码
value.writeTo(this);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HI8woxmf-1652596106146)(http://cdn.markuszhang.com/img/483dc0b911ff535efba0c42d8a75d49e.png)]
repeated编码
message TypeStudy{
double double_type = 1;
int32 int_type = 2;
int64 long_type = 3;
sint32 sint_type = 4;
repeated int32 int_list = 5 [packed = true];// 通过packed修饰,proto3默认为true
string string_type = 6;
Learn learn_type = 7;
}
message Learn{
int32 int_type = 1;
int64 long_type =2;
}
// packed=true
// proto类的writeTo方法
if (getIntListList().size() > 0) {
output.writeUInt32NoTag(42);// 只设置一遍
output.writeUInt32NoTag(intListMemoizedSerializedSize);
}
for (int i = 0; i < intList_.size(); i++) {
// 就不用设置集合内每一个数值的Tag了。
output.writeInt32NoTag(intList_.getInt(i));
}
// packed=false
// proto类的writeTo方法
for (int i = 0; i < intList_.size(); i++) {
// 每个值都设置相同的Tag标识
output.writeInt32(5, intList_.getInt(i));
}
四、总结
对于protobuf的学习记录就这些了,下面对Protobuf做一个简单的总结。
- 对于性能分析那块,吞吐量的值不一定正确,不同机器环境不同,测试的结果也有不同,我在尽量保证版本发布时期一致的情况下给出了各序列化协议的性能分析
- 序列化速度:protobuf > jackson > kryo > protostuff > fst > fastJson > jdk > xml
- 反序列化速度:protobuf > fst > protostuff > kryo > jackson > fastJson > jdk > xml
- 当然,我这里测试代码可能会使用的不当,如果有不同意见的朋友也可评论一下原因和真实情况,再附上我的测试代码:源码链接
- 当我们的系统需要使用序列化,并且对性能和空间有要求的话,可以了解下protobuf的解决方案
- 我们经常所说的protobuf具有数据压缩的效果就是通过它的Varint的编码方式以及T-L-V紧凑的存储方式来实现的
- protobuf序列化&反序列化速度快的原因是它的编码&解码方式简单(只需要进行简单的数学运算操作来完成的)
- 本文测试的对象进行序列化:protobuf的压缩效果是fastjson的1/3,和protostuff的压缩效果接近,当我们对系统的性能要求不是很大的情况下,可以考虑protostuff来解决
- protostuff是面向Java语言的protobuf框架,它能支持Java的复杂对象(无法通过proto文件编写的),也就是说数据结构支持的更好;对Java语言友好