protobuf序列化库

protobuf(全称Protocol Buffers)是Google编写的一个序列化库。我们通过定义一个标准的.proto描述文件,就可以生成常见编程语言中该数据结构的序列化和反序列化代码,使用起来非常方便。protobuf原生支持生成C、Go、Java三种编程语言的源代码,对于NodeJS、C#、Python等其它编程语言的支持则是基于C语言版本来间接支持的。

protobuf在应用层网络协议的制定上应用很广泛,除此之外,Google还推出了基于protobuf序列化的远程调用解决方案gRPC,它作为云原生架构的标准解决方案之一,在微服务架构中应用很多。相比于JSON、XML等基于文本的序列化方式,protobuf采用二进制序列化,具有高性能、网络开销小的特点;相比hessian2(Java)等其它常见的二进制序列化方式,protobuf则具有跨语言的通用性。

官方主页:https://protobuf.dev/

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中可以定义枚举类型,它比直接定义int32string等更直观,对应代码也会生成对应的枚举类型,下面是一个例子。

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_classnamejava_packagejava_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相关的内容。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。