Protobuf 用法小记

Protocol Buffers(简称 Protobuf)是 Google 开发的一种数据描述语言,它通常被用来序列化、反序列化结构化的数据和在网络间传输。它类似于 XML、JSON,但是更小、更快、更简单。

Protobuf 有以下几个关键优势:

  1. 更小的数据量:Protobuf 的二进制编码通常比 XML 和 JSON 小 3-10 倍,因此在网络传输和存储数据时可以节省带宽和存储空间。
  2. 高性能:由于 Protobuf 使用二进制格式,所以序列化和反序列化速度比 XML 和 JSON 快得多。
  3. 语言无关性:Protobuf 支持多种主流的编程语言,可以使用不同的编程语言编写客户端和服务端。
  4. 平台无关性:Protobuf 可以在不同的操作系统和设备上无缝使用。
  5. 易于使用:Protobuf 使用 .proto 文件定义数据结构,这些文件比 XML 和 JSON 更容易阅读和维护。
  6. 易于扩展:可以轻松添加和删除字段,具有良好的兼容性。

Protobuf 的使用步骤:

  1. 编写数据结构描述文件 .proto
  2. .proto 文件编译成特定的编程语言代码
  3. 序列化、反序列化

Protobuf 文件定义

使用 Protobuf 之前必须先编写 .proto 文件,这个文件是 Protobuf 的数据结构描述文件,它使用一种简洁的文本格式来描述数据结构,包括消息类型、字段以及它们的类型。这些文件被 Protobuf 编译器用来生成特定语言的数据访问类,这些类可以对 Protobuf 消息进行序列化和反序列化操作。

.proto 文件语法比较简单,下面是一个基于官网示例文件的改动版本,此文件使用 proto3 版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
syntax = "proto3";

package tutorial;

option java_multiple_files = true;
option java_package = "cc.cui.tutorial.protos";
option java_outer_classname = "AddressBookProtos";

message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;

enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}

message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2;
}

repeated PhoneNumber phones = 4;
}

message AddressBook {
repeated Person people = 1;
}

syntax 用于指定 Protobuf 的版本。

package 指定包名,类似于 Java 的包声明,有助于防止不同项目之间的命名冲突,但又不同于 Java 包,Protobuf 的包名通常不包含域名。

在包声明之后,有三个 Java 专用的 option:

  • java_multiple_files: 为 true 时表示为每个 message 生成单独的 Java 类文件,而不是在一个整体的 Java 类中。
  • java_package:指定生成的 Java 类的包名,未指定时,生成的 Java 类包名和 package 定义一致,但是 package 定义的包名是 Protobuf 的包名,并不适合当作 Java 类的包名,所以这个字段很有必要指定。
  • java_outer_classname:生成的 Java 类名,未指定时,生成的 Java 类名会使用 .proto 的文件名转换为大驼峰命名。例如 .my_proto.proto 将会使用 MyProto 作为 Java 类名。

message 是一组类型化字段的聚合,它包含了一些字段和字段类型。message 可以嵌套。一个 message 对应一个 Java 类。

enum 用于定义枚举类型,枚举类型不能有嵌套。

message 中定义的每个字段都有一个唯一的标识,不能省略,此标识是序列化之后的二进制编码中的唯一标识。标识通常从 1 开始。在 enum 中,第一个字段的标识必须为 0.

message 中,每个字段必须使用下列修饰符之一进行修饰:

  • optional :表示这个字段的值可以设置也可以不设置,不设置值时使用默认值。
  • repeated:表示这个字段可以重复任意次数(包括零次),可以将 repeated 修饰的字段视为集合。

在 proto2 中,修饰符还有一个 required 字段,用来表示必填,但因为存在很大的问题,所以在 proto3 版本已被删除。官方建议对必填字段的控制应该在应用层实现。

在 proto2 中,还支持设置字段的默认值,在 proto3 中同样不受支持。我个人理解 proto3 更专注于序列化和反序列化本身。

编译 proto 文件

使用 protoc 编译器编译 .proto 文件,生成对应语言的类或结构体。

1
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

-I=$SRC_DIR 用于指定 .proto 文件所在目录,--java_out=$DST_DIR 表示生成 Java 代码到指定的目录。

除了 --java_out,还支持其他语言:

  • --cpp_out
  • --csharp_out
  • --kotlin_out
  • --objc_out
  • --php_out
  • --pyi_out
  • --python_out
  • --ruby_out
  • --rust_out

如果命令没有报错,则不会显示任何输出。

编译器为每个 message 都生成了类和对应的 Builder 类,可以使用 Builder 类创建该类的实例。其中,实体类只有 getter 方法,Builder 类同时具有 gettersetter 方法。以生成的 Person 类为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// required string name = 1;
public boolean hasName();
public String getName();

// required int32 id = 2;
public boolean hasId();
public int getId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();

// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);

Person.Builder 中每个字段都有 gettersetter 方法,同时生成了相应的 clear 方法。对于标量类型,生成了 has 方法,用于判断是否设置了值。对于集合类型,生成了 add 方法,用于添加元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();

// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();

// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
public Builder setPhones(int index, PhoneNumber value);
public Builder addPhones(PhoneNumber value);
public Builder addAllPhones(Iterable<PhoneNumber> value);
public Builder clearPhones();

使用构造器

首先添加 maven 依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.26.1</version>
</dependency>

要往生成的 Java 类中写入字段值,则使用对应的 Builder 类,因为实体类没有提供设置值的方法。

使用构造器创建 Person 类的实例:

1
2
3
4
5
6
7
8
9
10
11
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhones(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.PHONE_TYPE_HOME)
.build());
.build();

常用方法

  • toString() 返回人类可读的格式,方便调试
  • byte[] toByteArray() 序列化消息返回字节数组
  • parseFrom(byte[] data) 从序列化的字节数组中反序列化
  • writeTo(OutputStream output) 序列化消息并将其写入 OutputStream
  • parseFrom(InputStream input)InputStream 反序列化

Protobuf 用法小记
https://cui.cc/protobuf/
作者
南山崔崔
发布于
2024年4月25日
许可协议