分布式-序列化之protobuf

1.MAC安装步骤

  1. 从github上下载protobuf3

protobuf3下载地址

有很多语言版本的,mac下选择第一个。

  1. 下载下来后解压压缩包,并进入目录
1
cd protobuf-3.7.0/

3.设置编译目录

1
./configure --prefix=/usr/local/protobuf

4.切换到root用户

1
sudo -i

5.安装

1
2
make
make install

6.配置环境变量

找到用户目录/Users/pauljiang 的 .bash_profile文件并编辑

1
vim .bash_profile

底部添加

1
2
export PROTOBUF=/usr/local/protobuf 
export PATH=$PROTOBUF/bin:$PATH

source一下使文件生效

1
source .bash_profile

7.测试安装结果

1
protoc --version

2. java使用

使用 protobuf 开发的一般步骤是

  1. 配置开发环境,安装 protocol compiler 代码编译器
  2. 编写.proto 文件,定义序列化对象的数据结构
  3. 基于编写的.proto 文件,使用 protocol compiler 编译器生成对应的序列化/反序列化工具

  1. 基于自动生成的代码,编写自己的序列化应用

2.1 编写proto文件

数据类型
string / bytes / bool / int32(4 个字节) /int64/float/double

enum 枚举类

message 自定义类

修饰符

required 表示必填字段

optional 表示可选字段

repeated 可重复,表示集合

1,2,3,4 需要在当前范围内是唯一的,表示顺序

demo:

1
2
3
4
5
6
7
8
9
10
syntax="proto2";
package com.pd.serial;

option java_package = "com.gupaoedu.serial";
option java_outer_classname="UserProtos";

message User {
required string name=1;
required int32 age=2;
}

使用命令生成实体类:

1
.\protoc.exe --java_out=./ ./user.proto

在项目中可以使用插件:

gradle构建,引入以下插件和依赖

1
2
3
4
5
6
plugins {
id "com.google.protobuf" version "0.8.14"
}
dependencies {
implementation 'com.google.protobuf:protobuf-java:3.9.2'
}

2.2 调用

序列化:

1
2
3
4
5
UserProtos.User user = UserProtos.User.newBuilder()
.setAge(300)
.setName("Mic")
.build();
byte[] bytes = user.toByteArray();

反序列化:

1
UserProtos.User = UserProtos.User.parseFrom(bytes);

3. protobuf原理

protobuf优势是空间开销小,性能也相对较好。它里面用到的一些算法还是值得我们去学习的

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
UserProtos.User user=UserProtos.User.newBuilder()
.setAge(300)
.setName("Mic")
.build();
byte[] bytes=user.toByteArray();
for(byte bt:bytes){
System.out.print(bt+" ");
}
}

打印出的序列为:10 3 77 105 99 16 -84 2

序列化出来的数字基本看不懂,但是序列化以后的数据确实很小,那我们接下来带大家去了解一下底层的原理

正常来说,要达到最小的序列化结果,一定会用到压缩的技术,而 protobuf 里面用到了两种压缩算法,一种是 varint,另一种是 zigzag

varint

先说第一种,我们先来看 age=300 这个数字是如何被压缩的

protobuf1

这两个字节字节分别的结果是:-84 、2

-84 怎么计算来的呢? 我们知道在二进制中表示负数的方法,高位设置为 1, 并且是对应数字的二进制取反以后再计算补码表示(补码是反码+1)

所以如果要反过来计算

  1. 【补码】10101100 -1 得到 10101011
  2. 【反码】01010100 得到的结果为 84. 由于高位是 1,表示负数所以结果为-84

字符如何转化为编码

“Mic”这个字符,需要根据 ASCII 对照表转化为数字。

M =77、i=105、c=99

所以结果为 77 105 99

这里的结果为什么直接就是 ASCII 编码的值呢?怎么没有做压缩呢

原因是,varint 是对字节码做压缩,但是如果这个数字的二进制只需要一个字节表示的时候, 其实最终编码出来的结果是不会变化的

还有两个数字,3 和 16 代表什么呢?那就要了解 protobuf 的存储格式了

存储格式

protobuf 采用 T-L-V 作为存储方式

protobuf2

protobuf3

tag 的计算方式是 field_number(当前字段的编号) << 3 | wire_type

比如Mic的字段编号是1 ,类型wire_type的值为 2 所以 : 1<<3|2=10

age=300 的字段编号是 2,类型 wire_type 的值是 0, 所以 : 2<<3|0 =16

第一个数字 10,代表的是 key,剩下的都是 value。

负数的存储

在计算机中,负数会被表示为很大的整数,因为计算机定义负数符号位为数字的最高位,所 以如果采用 varint 编码表示一个负数,那么一定需要 5 个比特位。所以在 protobuf 中通过 sint32/sint64 类型来表示负数,负数的处理形式是先采用 zigzag 编码(把符号数转化为无符号数),在采用 varint 编码。

sint32:(n << 1) ^ (n >> 31)

sint64:(n << 1) ^ (n >> 63)

比如存储一个(-300)的值

1
2
3
4
5
6
7
8
9
10
11
12
-300
原码:0001 0010 1100
取反:1110 1101 0011
加 1 :1110 1101 0100
n<<1: 整体左移一位,右边补 0 -> 1101 1010 1000
n>>31: 整体右移 31 位,左边补 1 -> 1111 1111 1111
(n<<1) ^ (n >>31)
1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111
十进制: 0010 0101 0111 = 599
varint 算法: 从右往做,选取 7 位,高位补 1/0(取决于字节数) 得到两个字节
1101 0111 0000 0100
-41 、 4

总结

Protocol Buffer 的性能好,主要体现在序列化后的数据体积小 & 序列化速度快,最终使得 传输效率高,其原因如下:

序列化速度快的原因:

a. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)

b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

序列化后的数据量体积小(即数据压缩效果好)的原因:

a. 采用了独特的编码方式,如 Varint、Zigzag 编码方式等等

b. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑

4. 序列化技术的选型

技术层面

  1. 序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能

  2. 序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间

  3. 序列化协议是否支持跨平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信

    需求,那么这个是必须要考虑的

  4. 可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新,

    这就要求我们采用的序列化协议基于良好的可扩展性/兼容性,比如在现有的序列化数据结

    构中新增一个业务字段,不会影响到现有的服务

  5. 技术的流行程度,越流行的技术意味着使用的公司多,那么很多坑都已经淌过并且得到了解决,技术解决方案也相对成熟

    1. 学习难度和易用性

选型建议

  1. 对性能要求不高的场景,可以采用基于 XML 的 SOAP 协议

  2. 对性能和间接性有比较高要求的场景,那么 Hessian、Protobuf、Thrift、Avro 都可以。

  3. 基于前后端分离,或者独立的对外的 api 服务,选用 JSON 是比较好的,对于调试、可读

    性都很不错

  4. Avro 设计理念偏于动态类型语言,那么这类的场景使用 Avro 是可以的

各个序列化技术的性能比较

这 个 地 址 有 针 对 不 同 序 列 化 技 术 进 行 性 能 比 较 : https://github.com/eishay/jvm- serializers/wiki

其他序列化方式介绍

XML 序列化框架介绍

XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大, 而且效率不高,适用于对性能不高,而且 QPS 较低的企业级内部系统之间的数据交换的场景, 同时 XML 又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟 知的 Webservice,就是采用 XML 格式对数据进行序列化的。XML 序列化/反序列化的实现方 式有很多,熟知的方式有 XStream 和 Java 自带的 XML 序列化和反序列化两种

JSON 序列化框架

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于 XML 来说,JSON 的字节流更小,而且可读性也非常好。现在 JSON 数据格式在企业运用是最普遍的
JSON 序列化常用的开源工具有很多

  1. Jackson (https://github.com/FasterXML/jackson)

  2. 阿里开源的 FastJson (https://github.com/alibaba/fastjon)

  3. Google 的 GSON (https://github.com/google/gson)

    这几种 json 序列化工具中,Jackson 与 fastjson 要比 GSON 的性能要好,但是 Jackson、 GSON 的稳定性要比 Fastjson 好。而 fastjson 的优势在于提供的 api 非常容易使用

Hessian 序列化框架

Hessian 是一个支持跨语言传输的二进制序列化协议,相对于 Java 默认的序列化机制来说, Hessian 具有更好的性能和易用性,而且支持多种不同的语言

实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构, 性能更高

Avro 序列化

Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持 二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro 提供的机制使动 态语言可以方便地处理 Avro 数据。

kyro 序列化框架

Kryo 是一种非常成熟的序列化实现,已经在 Hive、Storm)中使用得比较广泛,不过它不能 跨语言. 目前 dubbo 已经在 2.6 版本支持 kyro 的序列化机制。它的性能要优于之前的 hessian2