protobuf序列化库
protobuf(全称Protocol Buffers)是Google编写的一个序列化库。我们通过定义一个标准的.proto描述文件,就可以生成常见编程语言中该数据结构的序列化和反序列化代码,使用起来非常方便。protobuf原生支持生成C、Go、Java三种编程语言的源代码,对于NodeJS、C#、Python等其它编程语言的支持则是基于C语言版本来间接支持的。
protobuf在应用层网络协议的制定上应用很广泛,除此之外,Google还推出了基于protobuf序列化的远程调用解决方案gRPC,它作为云原生架构的标准解决方案之一,在微服务架构中应用很多。相比于JSON、XML等基于文本的序列化方式,protobuf采用二进制序列化,具有高性能、网络开销小的特点;相比hessian2(Java)等其它常见的二进制序列化方式,protobuf则具有跨语言的通用性。
Github地址:https://github.com/protocolbuffers/protobuf
安装protoc编译器
使用protobuf需要先安装protoc编译器,该编译器可以按照我们编写的.proto文件生成对应平台的数据结构、序列化和反序列化代码。对于Windows系统,我们可以在官方Github仓库中的releases下,找到预编译的protoc编译器。至于Linux操作系统,我们可以直接从软件源中安装或者下载一个预编译的最新版,这里就不多做介绍了。
安装完成后,我们需要将其bin目录加入到环境变量。配置好后,我们可以用如下命令输出protoc的版本信息检查安装是否成功。
protoc --version
protobuf语法详解
protobuf有一套自己的IDL(Interface Definition Language),即接口描述语言,我们使用protobuf时需要按照该语言的规范编写.proto文件,然后再使用protoc编译器根据该文件生成代码。这里我们先对protobuf语言进行介绍。
定义消息结构
下面例子我们定义了User结构。
syntax = "proto3";
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
代码中,syntax = "proto3"表示我们使用proto3版本语法,这里注意由于一些历史原因protobuf默认使用proto2语法,但该版本已经极少使用了,因此我们要在文件开头指定使用proto3版本语法。
protobuf中定义的数据结构基本单位是消息,代码中使用message关键字来声明。消息内部定义字段的格式为数据类型 字段名 = 标识号,每个消息字段都要分配唯一的标识号,它用于在二进制格式中标识字段。
关于标识号要注意,标识号从1开始定义,1至15号占用1字节,16至2047占用2字节,因此我们需要将最常出现的那些字段设置到1至15号。除此之外,标识号19000至19999这个区间是protobuf预留的,我们不能使用这些预留的标识号。
数据类型
下面表格是官方文档中,有关protobuf数据类型和对应平台数据类型的说明。我们定义消息结构的字段时应该遵照该表进行定义。
| .proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | Float | double | float | double | |
| float | float | float | float | float32 | Float | float | float | double | |
| int32 | 变长编码的整形,适合正值 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| int64 | 变长编码的长整型,适合正值 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| uint32 | 无符号变长编码的整形 | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
| uint64 | 无符号变长编码的长整形 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
| sint32 | 变长编码的整形,适合负值 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| sint64 | 变长编码的长整形,适合负值 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| fixed32 | 定长整形 | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
| fixed64 | 定长长整型 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
| sfixed32 | 定长整形 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
| sfixed64 | 定长长整型 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
| bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
| string | 必须为UTF-8编码或7位ASCII码 | string | String | str/unicode[4] | string | String (UTF-8) | string | string | String |
| bytes | 长度最大值为232 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string | List |
枚举类型
protobuf中可以定义枚举类型,它比直接定义int32或string等更直观,对应代码也会生成对应的枚举类型,下面是一个例子。
message User {
int64 id = 1; // 用户ID
string name = 2; // 用户名
string email = 3; // 用户邮箱
enum UserType { // 枚举定义
GOOD_USER = 0;
BAD_USER = 1;
}
UserType userType = 4; // 用户类型
}
注意:枚举类型定义中,标号必须从0开始。
singular和repeated
如果某个字段是可以有多个元素的(类似数组),可以用repeated修饰,下面是一个例子。
message User {
int64 id = 1;
string name = 2;
string email = 3;
repeated string hobby = 4;
}
对应的,如果某个字段只有一个元素,可以用singular修饰,不过我们的非数组字段都是singular的,protobuf允许singular省略不写,因此我们没必要把singular显示的写出来,大家知道有这样一个关键字即可。
注释
proto文件中,使用双斜线//可以实现单行注释。
message User {
int64 id = 1; // 用户ID
string name = 2; // 用户名
string email = 3; // 用户邮箱
repeated string hobby = 4; // 用户爱好
}
此外也可以使用/* */进行多行注释。
消息嵌套定义
.proto文件中支持消息的嵌套定义,适当的划分和嵌套消息能提升代码的可读性和可复用性,下面是一个例子。
message Hobby {
int64 id = 1; // 爱好ID
string name = 2; // 爱好名
}
message User {
int64 id = 1; // 用户ID
string name = 2; // 用户名
string email = 3; // 用户邮箱
repeated Hobby hobby = 4; // 用户爱好
}
上面代码中,我们在User消息中嵌套了Hobby消息。
文件导入
我们定义的.proto文件可以分割成多个文件以便于代码组织,引用另一个文件中的内容可以使用import关键字。
my_protocol.proto
syntax = "proto3";
import "my_protocol2.proto";
message User {
int64 id = 1; // 用户ID
string name = 2; // 用户名
string email = 3; // 用户邮箱
repeated Hobby hobby = 4; // 用户爱好
}
my_protocol2.proto
syntax = "proto3";
message Hobby {
int64 id = 1; // 爱好ID
string name = 2; // 爱好名
}
protobuf使用例子(Java)
这里我们以Java中使用protobuf为例,介绍protobuf的用法。我们这里采用Maven创建项目,工程目录结构如下。
src/main
|_java // Java源代码
|_proto // protobuf消息定义文件
|_resources // 其它资源文件
引入Maven依赖
Java中使用protobuf需要引入protobuf-java依赖。
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.24.3</version>
</dependency>
具体开发时,根据protoc编译器引入对应版本的protobuf-java依赖即可。
定义消息
我们编写如下代码定义User消息。
my_protocol.proto
syntax = "proto3";
option java_outer_classname = "MyProtocol";
option java_package = "com.gacfox.demo.proto";
option java_multiple_files = false;
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
注意代码中我们使用了option关键字,该关键字用于指定一些扩展信息,使用不同语言进行开发时可能需要指定不同的option配置。比如这里我们用该关键字指定java_outer_classname、java_package和java_multiple_files这3个配置,分别用于设置输出的Java代码类名、输出的包名以及是否输出到多个文件(不输出到多个文件意味着protoc会输出单个.java文件,所有生成的类都是一个大类的内部类。个人比较习惯这种方式,实际开发中应当遵循团队的代码规范)。
使用protoc编译
指定以下命令编译my_protocol.proto到Java代码。
protoc src/main/proto/my_protocol.proto --java_out=src/main/java
编译完成后,我们就可以看到生成了src/main/java/com/gacfox/demo/proto/MyProtocol.java文件。
实现序列化和反序列化
下面代码中,我们调用了protobuf生成对象并进行了序列化和反序列化。
package com.gacfox.demo;
import com.gacfox.demo.proto.MyProtocol;
import com.google.protobuf.InvalidProtocolBufferException;
public class Main {
public static void main(String[] args) {
// 使用builder方法生成user对象
MyProtocol.User.Builder userBuilder = MyProtocol.User.newBuilder();
userBuilder.setId(1L);
userBuilder.setName("Tom");
userBuilder.setEmail("tom@gacfox.com");
MyProtocol.User user = userBuilder.build();
// 序列化为二进制
byte[] data = user.toByteArray();
System.out.println("Data serialized in " + data.length + " Bytes");
try {
// 反序列化并输出
MyProtocol.User srcUser = MyProtocol.User.parseFrom(data);
System.out.println(srcUser.getId());
System.out.println(srcUser.getName());
System.out.println(srcUser.getEmail());
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
}
代码中,MyProtocol是我们通过protoc编译器生成的代码,我们调用其中的Builder类并构建了User消息,然后我们调用toByteArray()方法将其序列化为了二进制byte[]数组。之后我们调用了parseFrom()方法将二进制数组还原为了Java对象。
关于gRPC
有关protobuf的知识上面我们就介绍完了,不过我们的代码中还没有涉及网络通信,如果需要了解基于protobuf进行TCP/UDP通信可以参考Netty相关章节,本系列笔记之后的章节我们会继续介绍gRPC相关的内容。