当波及到网络通信和数据存储时,数据序列化 始终都是一个重要的话题;特地是当初很多公司都在推广微服务,数据序列化更是重中之重,通常会抉择应用 JSON 作为数据交换格局,且 JSON 曾经成为业界的支流。然而 Google 这么大的公司应用的却是一种被称为 Protobuf 的数据交换格局,它是有什么劣势吗?这篇文章介绍 Protobuf 的相干常识。
GitHub:https://github.com/protocolbuffers/protobuf
官网文档:https://protobuf.dev/overview/
Protobuf 介绍
Protobuf(Protocol Buffers)是由 Google 开发的一种 轻量级、高效 的数据交换格局,它被用于结构化数据的序列化、反序列化和传输。相比于 XML 和 JSON 等文本格式,Protobuf 具备更小的数据体积、更快的解析速度和更强的可扩展性。
Protobuf 的核心思想是 应用协定(Protocol)来定义数据的构造和编码方式 。应用 Protobuf,能够先定义数据的构造和各字段的类型、字段等信息, 而后应用 Protobuf 提供的编译器生成对应的代码 , 用于序列化和反序列化数据 。因为 Protobuf 是基于二进制编码的,因而能够在数据传输和存储中实现更高效的数据交换,同时也能够 跨语言 应用。
相比于 XML 和 JSON,Protobuf 有以下几个劣势:
- 更小的数据量:Protobuf 的二进制编码通常比 XML 和 JSON 小 3-10 倍,因而在网络传输和存储数据时能够节俭带宽和存储空间。
- 更快的序列化和反序列化速度:因为 Protobuf 应用二进制格局,所以序列化和反序列化速度比 XML 和 JSON 快得多。
- 跨语言:Protobuf 反对多种编程语言,能够应用不同的编程语言来编写客户端和服务端。这种跨语言的个性使得 Protobuf 受到很多开发者的欢送(JSON 也是如此)。
- 易于保护可扩大:Protobuf 应用 .proto 文件定义数据模型和数据格式,这种文件比 XML 和 JSON 更容易浏览和保护,且能够在不毁坏原有协定的根底上,轻松增加或删除字段,实现版本升级和兼容性。
编写 Protobuf
应用 Protobuf 的语言定义文件(.proto)能够定义要传输的信息的数据结构,能够包含各个字段的名称、类型等信息。同时也能够互相嵌套组合,结构出更加简单的音讯构造。
比方想要结构一个地址簿 AddressBook 信息结构。一个 AddressBook 能够蕴含多个人员 Person 信息,每个 Person 信息能够蕴含 id、name、email 信息,同时一个 Person 也能够蕴含多个电话号码信息 PhoneNumber,每个电话号码信息须要指定号码品种,如手机、家庭电话、工作电话等。
如果应用 Protobuf 编写定义文件如下:
// 文件:addressbook.proto
syntax = "proto3";
// 指定 protobuf 包名,避免有雷同类名的 message 定义
package com.wdbyte.protobuf;
// 是否生成多个文件
option java_multiple_files = true;
// 生成的文件寄存在哪个包下
option java_package = "com.wdbyte.tool.protos";
// 生成的类名,如果没有指定,会依据文件名主动转驼峰来命名
option java_outer_classname = "AddressBookProtos";
message Person {
// =1,=2 作为序列化后的二进制编码中的字段的惟一标签,也因而,1-15 比 16 会少一个字节,所以尽量应用 1-15 来指定常用字段。optional int32 id = 1;
optional string name = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message AddressBook {repeated Person people = 1;}
Protobuf 文件中的语法解释。
头部全局定义
syntax = "proto3";
指定 Protobuf 版本为版本 3(最新版本)package com.wdbyte.protobuf;
指定 Protobuf 包名,避免有雷同类名的message
定义,这个包名是生成的类中所用到的一些信息的前缀,并非类所在包。option java_multiple_files = true;
是否生成多个文件。若false
,则只会生成一个类,其余类以内部类模式提供。option java_package =
生成的类所在包。option java_outer_classname
生成的类名,若无,主动应用文件名进行驼峰转换来为类命名。
音讯构造具体定义
message Person
定一个了一个 Person 类。
Person 类中的字段被 optional
润饰,被 optional
润饰阐明字段能够不赋值。
- 修饰符
optional
示意可选字段,能够不赋值。 - 修饰符
repeated
示意数据反复多个,如数组,如 List。 - 修饰符
required
示意必要字段,必须给值,否则会报错RuntimeException
,然而在 Protobuf 版本 3 中被移除。即便在版本 2 中也应该慎用,因为一旦定义,很难更改。
字段类型定义
修饰符前面紧跟的是字段类型,如 int32
、string
。罕用的类型如下:
int32、int64、uint32、uint64
:整数类型,包含有符号和无符号类型。float、double
:浮点数类型。bool
:布尔类型,只有两个值,true 和 false。string
:字符串类型。bytes
:二进制数据类型。enum
:枚举类型,枚举值能够是整数或字符串。message
:音讯类型,能够嵌套其余音讯类型,相似于构造体。
字段前面的 =1,=2
是作为序列化后的二进制编码中的字段的对应标签,因为 Protobuf 音讯在序列化后是不蕴含字段信息的,只有对应的字段序号,所以 节俭了空间 。也因而,1-15 比 16 会少一个字节,所以 尽量应用 1-15 来指定常用字段 。且一旦定义, 不要随便更改,否则可能会对不上序列化信息。
编译 Protobuf
应用 Protobuf 提供的编译器,能够将 .proto
文件编译成各种语言的代码文件(如 Java、C++、Python 等)。
下载编译器:https://github.com/protocolbuffers/protobuf/releases/latest
装置实现后能够应用 protoc
命令编译 proto
文件,如编译示例中的 addressbook.proto
.
protoc --java_out=./java ./resources/addressbook.proto
# --java_out 指定输入 java 格式文件,输入到 ./java 目录
# ./resources/addressbook.proto 为 proto 文件地位
生成后能够看到生产的类文件。
./
├── java
│ └── com
│ └── wdbyte
│ └── tool
│ ├── protos
│ │ ├── AddressBook.java
│ │ ├── AddressBookOrBuilder.java
│ │ ├── AddressBookProtos.java
│ │ ├── Person.java
│ │ ├── PersonOrBuilder.java
└── resources
├── addressbook.proto
应用 Protobuf
应用 Java 语言操作 Protobuf,首先须要引入 Protobuf 依赖。
Maven 依赖:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.22.3</version>
</dependency>
结构音讯对象
// 间接构建
PhoneNumber phoneNumber1 = PhoneNumber.newBuilder().setNumber("18388888888").setType(PhoneType.HOME).build();
Person person1 = Person.newBuilder().setId(1).setName("www.wdbyte.com").setEmail("xxx@wdbyte.com").addPhones(phoneNumber1).build();
AddressBook addressBook1 = AddressBook.newBuilder().addPeople(person1).build();
System.out.println(addressBook1);
System.out.println("------------------");
// 链式构建
AddressBook addressBook2 = AddressBook
.newBuilder()
.addPeople(Person.newBuilder()
.setId(2)
.setName("www.wdbyte.com")
.setEmail("yyy@126.com")
.addPhones(PhoneNumber.newBuilder()
.setNumber("18388888888")
.setType(PhoneType.HOME)
)
)
.build();
System.out.println(addressBook2);
输入:
people {
id: 1
name: "www.wdbyte.com"
email: "xxx@wdbyte.com"
phones {
number: "18388888888"
type: HOME
}
}
------------------
people {
id: 2
name: "www.wdbyte.com"
email: "yyy@126.com"
phones {
number: "18388888888"
type: HOME
}
}
序列化、反序列化
序列化:将内存中的数据对象序列化为二进制数据,能够用于网络传输或存储等场景。
反序列化:将二进制数据反序列化成内存中的数据对象,能够用于数据处理和业务逻辑。
上面演示应用 Protobuf 进行字符数组和文件的序列化及反序列化过程。
package com.wdbyte.tool.protos;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
*
* @author www.wdbyte.com
*/
public class ProtobufTest2 {public static void main(String[] args) throws IOException {PhoneNumber phoneNumber1 = PhoneNumber.newBuilder().setNumber("18388888888").setType(PhoneType.HOME).build();
Person person1 = Person.newBuilder().setId(1).setName("www.wdbyte.com").setEmail("xxx@wdbyte.com").addPhones(phoneNumber1).build();
AddressBook addressBook1 = AddressBook.newBuilder().addPeople(person1).build();
// 序列化成字节数组
byte[] byteArray = addressBook1.toByteArray();
// 反序列化 - 字节数组转对象
AddressBook addressBook2 = AddressBook.parseFrom(byteArray);
System.out.println("字节数组反序列化:");
System.out.println(addressBook2);
// 序列化到文件
addressBook1.writeTo(new FileOutputStream("AddressBook1.txt"));
// 读取文件反序列化
AddressBook addressBook3 = AddressBook.parseFrom(new FileInputStream("AddressBook1.txt"));
System.out.println("文件读取反序列化:");
System.out.println(addressBook3);
}
}
输入:
字节数组反序列化:people {
id: 1
name: "www.wdbyte.com"
email: "xxx@wdbyte.com"
phones {
number: "18388888888"
type: HOME
}
}
文件读取反序列化:people {
id: 1
name: "www.wdbyte.com"
email: "xxx@wdbyte.com"
phones {
number: "18388888888"
type: HOME
}
}
Protobuf 为什么高效
在剖析 Protobuf 高效之前,咱们先确认一下 Protobuf 是否真的高效,上面将 Protobuf 与 JSON 进行比照,别离 比照序列化和反序列化速度 以及 序列化后的存储占用大小。
测试工具:JMH,FastJSON,
测试对象:Protobuf 的 addressbook.proto
,JSON 的一般 Java 类。
Maven 依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.33</version>
<scope>provided</scope>
</dependency>
先编写与addressbook.proto
构造雷同的 Java 类 AddressBookJava.java
.
public class AddressBookJava {
List<PersonJava> personJavaList;
public static class PersonJava {
private int id;
private String name;
private String email;
private PhoneNumberJava phones;
// get...set...
}
public static class PhoneNumberJava {
private String number;
private PhoneTypeJava phoneTypeJava;
// get....set....
}
public enum PhoneTypeJava {MOBILE, HOME, WORK;}
public List<PersonJava> getPersonJavaList() {return personJavaList;}
public void setPersonJavaList(List<PersonJava> personJavaList) {this.personJavaList = personJavaList;}
}
序列化大小比照
别离在地址簿中增加 1000 个人员信息,输入序列化后的数组大小。
package com.wdbyte.tool.protos;
import java.io.IOException;
import java.util.ArrayList;
import com.alibaba.fastjson.JSON;
import com.wdbyte.tool.protos.AddressBook.Builder;
import com.wdbyte.tool.protos.AddressBookJava.PersonJava;
import com.wdbyte.tool.protos.AddressBookJava.PhoneNumberJava;
import com.wdbyte.tool.protos.AddressBookJava.PhoneTypeJava;
import com.wdbyte.tool.protos.Person.PhoneNumber;
import com.wdbyte.tool.protos.Person.PhoneType;
/**
* @author https://www.wdbyte.com
*/
public class ProtobufTest3 {public static void main(String[] args) throws IOException {AddressBookJava addressBookJava = createAddressBookJava(1000);
String jsonString = JSON.toJSONString(addressBookJava);
System.out.println("json string size:" + jsonString.length());
AddressBook addressBook = createAddressBook(1000);
byte[] addressBookByteArray = addressBook.toByteArray();
System.out.println("protobuf byte array size:" + addressBookByteArray.length);
}
public static AddressBook createAddressBook(int personCount) {Builder builder = AddressBook.newBuilder();
for (int i = 0; i < personCount; i++) {builder.addPeople(Person.newBuilder()
.setId(i)
.setName("www.wdbyte.com")
.setEmail("xxx@126.com")
.addPhones(PhoneNumber.newBuilder()
.setNumber("18333333333")
.setType(PhoneType.HOME)
)
);
}
return builder.build();}
public static AddressBookJava createAddressBookJava(int personCount) {AddressBookJava addressBookJava = new AddressBookJava();
addressBookJava.setPersonJavaList(new ArrayList<>());
for (int i = 0; i < personCount; i++) {PersonJava personJava = new PersonJava();
personJava.setId(i);
personJava.setName("www.wdbyte.com");
personJava.setEmail("xxx@126.com");
PhoneNumberJava numberJava = new PhoneNumberJava();
numberJava.setNumber("18333333333");
numberJava.setPhoneTypeJava(PhoneTypeJava.HOME);
personJava.setPhones(numberJava);
addressBookJava.getPersonJavaList().add(personJava);
}
return addressBookJava;
}
}
输入:
json string size:108910
protobuf byte array size:50872
可见测试中 Protobuf 的序列化后果比 JSON 小了将近一倍左右。
序列化速度比照
应用 JMH 进行性能测试,别离测试 JSON 的序列化和反序列以及 Protobuf 的序列化和反序列化性能状况。每次测试前进行 3 次预热,每次 3 秒。接着进行 5 次测试,每次 3 秒,收集测试状况。
package com.wdbyte.tool.protos;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import com.alibaba.fastjson.JSON;
import com.google.protobuf.InvalidProtocolBufferException;
import com.wdbyte.tool.protos.AddressBook.Builder;
import com.wdbyte.tool.protos.AddressBookJava.PersonJava;
import com.wdbyte.tool.protos.AddressBookJava.PhoneNumberJava;
import com.wdbyte.tool.protos.AddressBookJava.PhoneTypeJava;
import com.wdbyte.tool.protos.Person.PhoneNumber;
import com.wdbyte.tool.protos.Person.PhoneType;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
/**
* @author https://www.wdbyte.com
*/
@State(Scope.Thread)
@Fork(2)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 3)
@BenchmarkMode(Mode.Throughput) // Throughput: 吞吐量,SampleTime:采样工夫
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ProtobufTest4 {
private AddressBookJava addressBookJava;
private AddressBook addressBook;
@Setup
public void init() {addressBookJava = createAddressBookJava(1000);
addressBook = createAddressBook(1000);
}
@Benchmark
public AddressBookJava testJSON() {
// 转 JSON
String jsonString = JSON.toJSONString(addressBookJava);
// JSON 转对象
return JSON.parseObject(jsonString, AddressBookJava.class);
}
@Benchmark
public AddressBook testProtobuf() throws InvalidProtocolBufferException {
// 转 JSON
byte[] addressBookByteArray = addressBook.toByteArray();
// JSON 转对象
return AddressBook.parseFrom(addressBookByteArray);
}
public static AddressBook createAddressBook(int personCount) {Builder builder = AddressBook.newBuilder();
for (int i = 0; i < personCount; i++) {builder.addPeople(Person.newBuilder()
.setId(i)
.setName("www.wdbyte.com")
.setEmail("xxx@126.com")
.addPhones(PhoneNumber.newBuilder()
.setNumber("18333333333")
.setType(PhoneType.HOME)
)
);
}
return builder.build();}
public static AddressBookJava createAddressBookJava(int personCount) {AddressBookJava addressBookJava = new AddressBookJava();
addressBookJava.setPersonJavaList(new ArrayList<>());
for (int i = 0; i < personCount; i++) {PersonJava personJava = new PersonJava();
personJava.setId(i);
personJava.setName("www.wdbyte.com");
personJava.setEmail("xxx@126.com");
PhoneNumberJava numberJava = new PhoneNumberJava();
numberJava.setNumber("18333333333");
numberJava.setPhoneTypeJava(PhoneTypeJava.HOME);
personJava.setPhones(numberJava);
addressBookJava.getPersonJavaList().add(personJava);
}
return addressBookJava;
}
}
JMH 吞吐量测试后果(Score 值越大吞吐量越高,性能越好):
Benchmark Mode Cnt Score Error Units
ProtobufTest3.testJSON thrpt 10 1.877 ± 0.287 ops/ms
ProtobufTest3.testProtobuf thrpt 10 2.813 ± 0.446 ops/ms
JMH 采样工夫测试后果(Score 越小,采样工夫越小,性能越好):
Benchmark Mode Cnt Score Error Units
ProtobufTest3.testJSON sample 53028 0.565 ± 0.005 ms/op
ProtobufTest3.testProtobuf sample 90413 0.332 ± 0.001 ms/op
从测试后果看,不论是吞吐量测试,还是采样工夫测试,Protobuf 都优于 JSON。
为什么高效?
Protobuf 是如何实现这种高效紧凑的数据编码和解码的呢?
首先,Protobuf 应用二进制编码,会进步性能;其次 Protobuf 在将数据转换成二进制时,会对字段和类型从新编码,缩小空间占用。它采纳 TLV
格局来存储编码后的数据。TLV
也是就是 Tag-Length-Value,是一种常见的编码方式,因为数据其实都是键值对模式,所以在 TAG
中会存储对应的 字段和类型 信息,Length
存储内容的长度,Value
存储具体的内容。
还记得下面定义构造体时每个字段都对应一个数字吗?如 =1
,=2
,=3
.
message Person {
optional int32 id = 1;
optional string name = 2;
optional string email = 3;
}
在序列化成二进制时候就是通过这个数字来标记对应的字段的,二进制中只存储这个数字,反序列化时通过这个数字找对应的字段。这也是下面为什么说尽量应用 1-15 范畴内的数字,因为一旦超过 15,就须要多一个 bit 位来存储。
那么类型信息呢?比方 int32
怎么标记,因为类型个数无限,所以 Protobuf 规定了每个类型对应的二进制编码,比方 int32
对应二进制 000
,string
对应二进制 010
,这样就能够只用三个比特位存储类型信息。
这里只是举例形容大略思维,具体还有一些变动。
详情能够参考官网文档:https://protobuf.dev/programming-guides/encoding/
其次,Protobuf 还会采纳一种 变长编码的形式来存储数据 。这种编码方式可能保证数据占用的空间最小化,从而缩小了数据传输和存储的开销。具体来说,Protobuf 会将整数和浮点数等类型变换成一个或多个字节的模式,其中每个字节都蕴含了一部分数据信息和一部分标识符信息。这种编码方式能够 在数据值比拟小的状况下,只应用一个字节来存储数据,以此来进步编码效率。
最初,Protobuf 还能够通过 采纳压缩算法来缩小数据传输的大小。比方 GZIP 算法可能将原始数据压缩成更小的二进制格局,从而在网络传输中可能节俭带宽和传输工夫。Protobuf 还提供了一些可选的压缩算法,如 zlib 和 snappy,这些算法在不同的场景下可能适应不同的压缩需要。
综上所述,Protobuf 在实现高效编码和解码的过程中,采纳了多种优化形式,从而在理论利用中可能无效地晋升数据传输和解决的效率。
总结
ProtoBuf 是一种 轻量、高效 的数据交换格局,它具备以下长处:
- 语言中立,能够反对多种编程语言;
- 数据结构清晰,易于保护和扩大;
- 二进制编码,数据 体积小,传输效率高;
- 主动生成代码,开发效率高。
然而,ProtoBuf 也存在以下毛病:
- 学习老本较高,须要把握其语法规定和应用办法;
- 须要先定义数据结构,而后能力对数据进行序列化和反序列化,减少了肯定的开发成本;
- 因为二进制编码,可读性较差,这点不如 JSON 能够间接浏览。
总体来说,Protobuf 适宜用于数据传输和存储等场景,可能进步数据传输效率和缩小数据体积。但对于须要人类可读的数据,或须要实时批改的数据,或者对数据的传输效率和体积没那么在意的场景,抉择更加通用的 JSON 未尝不是一个好的抉择。
参考:https://protobuf.dev/overview/
判若两人,文章代码都寄存在 Github.com/niumoo/javaNotes.
文章继续更新,能够微信搜一搜「程序猿阿朗」或拜访「程序猿阿朗博客」第一工夫浏览。本文 Github.com/niumoo/JavaNotes 曾经收录,有很多系列文章,欢送 Star。