Protobuf 用法小记
Protocol Buffers(简称 Protobuf)是 Google 开发的一种数据描述语言,它通常被用来序列化、反序列化结构化的数据和在网络间传输。它类似于 XML、JSON,但是更小、更快、更简单。
Protobuf 有以下几个关键优势:
- 更小的数据量:Protobuf 的二进制编码通常比 XML 和 JSON 小 3-10 倍,因此在网络传输和存储数据时可以节省带宽和存储空间。
- 高性能:由于 Protobuf 使用二进制格式,所以序列化和反序列化速度比 XML 和 JSON 快得多。
- 语言无关性:Protobuf 支持多种主流的编程语言,可以使用不同的编程语言编写客户端和服务端。
- 平台无关性:Protobuf 可以在不同的操作系统和设备上无缝使用。
- 易于使用:Protobuf 使用
.proto
文件定义数据结构,这些文件比 XML 和 JSON 更容易阅读和维护。 - 易于扩展:可以轻松添加和删除字段,具有良好的兼容性。
Protobuf 的使用步骤:
- 编写数据结构描述文件
.proto
- 将
.proto
文件编译成特定的编程语言代码 - 序列化、反序列化
Protobuf 文件定义
使用 Protobuf 之前必须先编写 .proto
文件,这个文件是 Protobuf 的数据结构描述文件,它使用一种简洁的文本格式来描述数据结构,包括消息类型、字段以及它们的类型。这些文件被 Protobuf 编译器用来生成特定语言的数据访问类,这些类可以对 Protobuf 消息进行序列化和反序列化操作。
.proto
文件语法比较简单,下面是一个基于官网示例文件的改动版本,此文件使用 proto3 版本:
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 |
|
-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
类同时具有 getter
和 setter
方法。以生成的 Person 类为例:
1 |
|
Person.Builder
中每个字段都有 getter
和 setter
方法,同时生成了相应的 clear
方法。对于标量类型,生成了 has
方法,用于判断是否设置了值。对于集合类型,生成了 add
方法,用于添加元素。
1 |
|
使用构造器
首先添加 maven 依赖:
1 |
|
要往生成的 Java 类中写入字段值,则使用对应的 Builder
类,因为实体类没有提供设置值的方法。
使用构造器创建 Person 类的实例:
1 |
|
常用方法
toString()
返回人类可读的格式,方便调试byte[] toByteArray()
序列化消息返回字节数组parseFrom(byte[] data)
从序列化的字节数组中反序列化writeTo(OutputStream output)
序列化消息并将其写入OutputStream
parseFrom(InputStream input)
从InputStream
反序列化