ProtoBuf的学习与使用(一):初识ProtoBuf【序列化概念、介绍、使用特点】、安装 ProtoBuf【Linux-Ubuntu、Windows】、学习思路、快速上手ProtoBuf

di-Dora 2024-07-31 14:07:03 阅读 79

目录

初识 ProtoBuf

序列化概念

序列化和反序列化

什么情况下需要序列化?

如何实现序列化?

ProtoBuf 是什么

ProtoBuf 的使用特点

安装 ProtoBuf(Linux-Ubuntu)

1、下载 ProtoBuf

2、安装 ProtoBuf

3、检查是否安装成功

安装 ProtoBuf(Windows)

1、下载 ProtoBuf 编译器

2、配置环境变量

3、检查是否配置成功 

学习思路

快速上手ProtoBuf

准备工作

创建 .proto 文件

指定 proto3 语法

package 声明符

添加 JAVA 选项

定义消息(message)

定义消息字段

编译 contacts.proto 文件,生成 JAVA 文件

编译 contacts.proto 文件后会生成什么?

序列化与反序列化的使用


初识 ProtoBuf

序列化概念

序列化和反序列化

序列化和反序列化是计算机领域中非常重要的概念。它们涉及将对象在内存中的表示形式转换为可以在网络上传输或者永久存储的字节序列,并且能够将这些字节序列重新转换回原始对象。

序列化是指将对象转换为字节序列的过程。在序列化过程中,对象的状态被编码成字节流的形式,这样它们可以被保存到文件中、发送到网络上,或者以其他形式进行持久化存储。

反序列化则是序列化的逆过程,它将字节序列转换回原始对象的过程。在反序列化过程中,字节流被解码并转换为原始对象的内存表示形式,使得对象可以在程序中重新使用或者进一步处理。

这两个过程通常用于需要在不同系统之间传输数据或者在系统的不同运行实例之间共享数据的情况下。序列化和反序列化为分布式系统、网络通信以及持久化存储提供了重要的支持,使得数据可以在不同的环境中进行有效地传输和处理。

什么情况下需要序列化?

在实际应用中,序列化通常用于以下情况:

总的来说,序列化是一种通用的机制,适用于许多不同的场景,能够实现对象在不同系统之间的传输、共享和持久化存储。

如何实现序列化?

存储数据:当需要将内存中的对象状态保存到文件中或者存储到数据库中时,就需要进行序列化。通过序列化,对象的状态可以被持久化保存,以便之后进行恢复或者传输。

网络传输:在网络通信中,直接传输对象并不可行,因为网络通信的基本单元是字节流。因此,在网络传输过程中,需要将对象序列化为字节序列进行传输,然后在接收端进行反序列化以还原对象。这种方式在诸如Socket编程中是常见的做法。

进程间通信(IPC):当不同的进程需要进行通信时,它们之间无法直接共享内存中的对象。因此,需要将对象序列化为字节序列,以便在进程之间进行传输,然后在接收端进行反序列化。

缓存:在一些缓存系统中,为了提高性能和减少数据传输的开销,会将对象序列化后存储在缓存中。这样,当需要获取对象时,可以直接从缓存中读取序列化的字节序列,并在需要时反序列化为对象。

跨平台数据交换:当不同平台或者编程语言之间需要进行数据交换时,通常会选择一种通用的序列化格式,如JSON或者XML。通过将对象序列化为这些通用格式,可以实现跨平台的数据交换。

持久化存储:在一些应用中,需要将对象的状态持久化保存到磁盘上,以便在系统重启或者应用重新启动时能够恢复之前的状态。通过序列化对象并将其保存到文件或者数据库中,可以实现这种持久化存储的需求。

实现序列化的方式多种多样,每种方式都有其独特的特点和适用场景。以下是常见的序列化实现方式:

XML(eXtensible Markup Language)

XML是一种通用的标记语言,可用于表示和存储复杂的数据结构。通过定义XML Schema或者使用DOM(Document Object Model)等技术,可以将对象序列化为XML格式。反序列化时,可以解析XML并根据定义的规则重新构建对象。XML适用于需要人类可读的、结构化的数据表示的场景,但相对于其他格式来说,它可能会产生较大的数据体积。

JSON(JavaScript Object Notation)

JSON是一种轻量级的数据交换格式,易读易写。许多编程语言都提供了内置的JSON序列化和反序列化库。对象可以序列化为JSON字符串,也可以从JSON字符串反序列化为对象。JSON在Web开发中被广泛应用,尤其适用于RESTful API和前后端数据交互。

Protobuf(Protocol Buffers)

Protobuf是由Google开发的高效的数据序列化格式,使用二进制编码,具有高效、紧凑的特点。通过定义Protobuf的消息结构和协议,可以将对象序列化为二进制格式。Protobuf的序列化和反序列化效率高,生成的数据体积小,适用于对性能和带宽有较高要求的场景。由于其二进制格式,Protobuf并不适用于人类阅读和编辑,但在大规模分布式系统中的数据通信方面表现优异。

这些序列化方式各有特点,选择合适的方式取决于具体的应用场景和需求,包括数据大小、性能要求、易用性等方面的考量。XML和JSON适用于通用的数据交换和存储场景,而Protobuf则更适用于对性能和带宽有较高要求的场景,如大规模分布式系统中的数据通信。

ProtoBuf 是什么

我们先来看看官方给出的答案是什么:Overview | Protocol Buffers Documentation

 

翻译一下就是:

“协议缓冲区是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。

它就像JSON,只是它更小更快,而且它生成本地语言绑定。您可以定义一次数据的结构化方式,然后可以使用特殊生成的源代码,使用各种语言,轻松地在各种数据流中写入和读取结构化数据。

协议缓冲区是定义语言(在.proto文件中创建)、proto编译器生成的与数据接口的代码、特定于语言的运行库以及写入文件(或通过网络连接发送)的数据的序列化格式的组合。”

所以,简单来概括就是——ProtoBuf(Protocol Buffers)是一种由Google开发的用于结构化数据序列化的方法。

它是一种轻量级、高效的数据交换格式,旨在提供一种简单、高效、可扩展的序列化机制,用于在不同系统之间进行数据传输和存储。

ProtoBuf使用一种语言无关的接口描述语言(IDL)来描述数据结构,然后根据这个描述生成特定语言的类文件,使得在不同编程语言之间能够轻松地进行数据交换。ProtoBuf的数据序列化格式是二进制的,相比于文本格式的序列化方法,它更加紧凑和高效。

ProtoBuf支持数据结构的版本化、向前和向后兼容,这使得系统升级和演化变得更加容易。此外,ProtoBuf还支持对数据结构的字段添加标签、注释以及默认值等元信息,以提供更丰富的数据描述和语义。

由于ProtoBuf具有高效、紧凑和跨语言等特点,它被广泛应用于各种领域,包括大规模分布式系统、网络通信、持久化存储等方面。通过使用ProtoBuf,开发者可以更轻松地实现跨平台、跨语言的数据交换和通信,从而提高系统的效率和可扩展性。

ProtoBuf(Protocol Buffers)具有以下主要特点:

语言无关、平台无关:ProtoBuf支持多种编程语言(如Java、C++、Python等)以及多个平台,使得在不同语言和平台之间进行数据交换和通信变得更加简单和灵活。

高效:相比于一些其他序列化格式(如XML),ProtoBuf生成的数据更为紧凑,传输效率更高。它采用二进制格式进行数据序列化,能够显著减少数据体积和传输开销。

扩展性好:ProtoBuf支持数据结构的版本化和扩展,可以在不影响现有代码和数据的情况下更新数据结构。新增字段或者更改字段类型等操作都不会破坏已有的旧程序,这种良好的扩展性使得系统的演化变得更加容易。

兼容性好:由于ProtoBuf支持数据结构的版本化和扩展,因此它具有良好的向前和向后兼容性。即使在更新了数据结构后,旧版本的程序仍然可以正确地解析和处理新版本的数据,而新版本的程序也能够正确地处理旧版本的数据。

总的来说,ProtoBuf是一种功能强大、高效、灵活的数据序列化格式,适用于各种不同的应用场景,包括大规模分布式系统、网络通信、持久化存储等。其语言无关、平台无关的特点使得它成为了跨语言、跨平台数据交换和通信的理想选择。

ProtoBuf 的使用特点

编写 .proto 文件:首先需要编写.proto文件,其中定义了结构对象(message)及其属性内容。这些定义包括了数据类型、字段名称等信息,用于描述要序列化的数据结构。

使用 protoc 编译器编译 .proto 文件:利用ProtoBuf提供的编译器protoc,对.proto文件进行编译,生成一系列接口代码。这些接口代码包括了对定义在.proto文件中的数据结构的操作接口,以及序列化和反序列化的方法。

依赖生成的接口代码进行操作:开发者可以利用生成的接口代码,实现对.proto文件中定义的字段进行设置和获取,以及对message对象进行序列化和反序列化操作。通过调用生成的接口方法,可以方便地进行数据的序列化和反序列化。

总的来说,ProtoBuf需要依赖通过编译生成的特定语言(如Java)的代码来使用。通过这种代码生成机制,开发者不再需要手动编写繁琐的协议解析代码,从而提高了开发效率,减少了开发工作量。

安装 ProtoBuf(Linux-Ubuntu)

1、下载 ProtoBuf

下载 ProtoBuf 前一定要安装依赖库:autoconf automake libtool curl make g++ unzip 如未安装,安装命令如下:

<code>sudo apt-get install autoconf automake libtool curl make g++ unzip -y

ProtoBuf 下载地址: Releases · protocolbuffers/protobuf (github.com)

对于使用ProtoBuf的不同编程语言,可以选择相应的代码生成工具和库来进行开发。通常,针对不同语言的支持会提供相应的压缩文件,其中包含了生成的代码以及相关的库文件。

具体来说:

如果要在C++下使用ProtoBuf,可以选择提供了C++代码生成的cpp.zip压缩文件。如果要在Java下使用ProtoBuf,可以选择提供了Java代码生成的java.zip压缩文件。对于其他语言,可以选择对应的压缩文件,如Python、Go等。

如果希望支持全部语言,可以选择提供了所有语言代码生成的all.zip压缩文件。这样就可以获得所有支持的语言所需的生成代码和库文件,从而方便在不同的开发环境中使用ProtoBuf。

 

下载完成后,解压zip包:

unzip protobuf-all-21.11.zip

解压完成后,会生成 protobuf-21.11 文件

进入文件:cd protobuf-21.11:

2、安装 ProtoBuf

进⼊解压好的文件,执行以下命令:

autogen.sh 用于生成配置所需的文件,但如果你下载的是特定语言的代码,不需要执行这一步。configure 脚本用于配置编译选项,其中 --prefix 参数用于指定安装目录,根据个人需求进行配置即可。

<code># 如果下载的是具体的某一门语言,不需要执行 autogen.sh

./autogen.sh

# 第二步执行 configure,有两种执行方式,任选其一即可,如下:

# 1、protobuf默认安装在 /usr/local 目录,lib、bin都是分散的

./configure

# 2、修改安装目录,统一安装在 /usr/local/protobuf 下

./configure --prefix=/usr/local/protobuf

 

执行成功之后就会在当前目录下生成一个Make File文件

我们就可以执行make编译了:

执行 make 命令将编译源代码生成可执行文件,make check 命令用于运行测试用例检查程序是否正常工作。最后,使用 sudo make install 命令将编译后的程序安装到系统中。

<code># 执行 make 命令,编译源代码,大约需要 15 分钟左右的时间

make

# 执行 make check 命令,进行测试,确保编译后的程序运行正常,大约需要 15 分钟左右的时间

make check

# 使用 sudo make install 命令,以管理员权限安装编译后的程序

sudo make install

 

这个错误是因为在运行测试用例时发生了失败。可能是由于服务器环境的限制导致的,测试用例对系统资源的要求较高,特别是对于内存资源。

要解决这个问题,可以尝试以下方法:

增加 Swap 分区:根据错误信息,增加 Swap 分区可以提供额外的虚拟内存空间,从而帮助系统处理更多的内存需求。可以通过增加 Swap 分区的大小来应对测试用例对内存的需求。

优化测试用例:如果测试用例过于繁琐或者对服务器资源的要求过高,可以考虑优化测试用例,减少其对资源的占用。可以尝试排除一些不必要的测试用例,或者修改测试用例使其更加轻量化。

增加物理内存:如果可能的话,也可以考虑增加服务器的物理内存,从而提供更多的系统资源供测试用例使用。

检查其他问题:除了资源限制外,还可能有其他问题导致测试用例失败。可以查看测试日志(test-suite.log)以获取更多详细信息,并尝试解决其中的问题。

综上所述,首先可以尝试增加 Swap 分区来提高系统的内存资源,以应对测试用例对内存的需求。如果问题仍然存在,我们再进一步探索其他可能的解决方案。

具体可参考:Ubuntu 18.04 swap分区扩展_ubuntu18.04 如何查看swapfile文件路径-CSDN博

建议可以先扩大3G,再执行 make check 。如果还是报错,再扩⼤到5G重新执行 make check 

我直接扩充到5G执行,但是还是没有成功,查看具体测试日志:

<code>cat test-suite.log

从日志中可以看出,测试套件中共有 2375 个测试用例,其中 2373 个通过了,1 个被跳过了,1 个失败了。

失败的测试用例是 IoTest.LargeOutput,因为它输出了大量内容,导致测试失败。这还是由于测试环境的限制或者资源不足导致的。

 

禁用Swap分区:在扩容之前,首先需要禁用Swap分区。可以使用以下命令:

<code>sudo swapoff /swapfile

修改Swap文件大小:使用 fallocate 命令为Swap文件分配更大的空间。我们将Swap文件扩展到 10GB,可以运行以下命令:

sudo fallocate -l 10G /swapfile

重新启用Swap分区:将Swap文件设置为Swap分区并启用它:

sudo mkswap /swapfile sudo swapon /swapfile

验证Swap分区:确保Swap分区已经成功启用并且大小已经扩容:

sudo swapon --show

更新 /etc/fstab:如果Swap分区扩容成功,可以将新的Swap分区信息更新到 /etc/fstab 文件中,以便系统在下次启动时自动启用Swap分区。可以运行以下命令:

sudo cp /etc/fstab /etc/fstab.bak # 备份原始文件

sudo sed -i '/swapfile/d' /etc/fstab # 删除旧的Swap信息

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 添加新的Swap信息

完成以上步骤后,我们的Swap分区就成功扩容了。

再次执行make check 后 ,出现以下内容就可以执行 sudo make install 。

这个时候需要你回忆一下当时选择第几种方式执行的configure?

果当时选择了第⼀种执行方式,也就是 ./configure ,那么到这就可以正常使⽤protobuf了。

但是如果你执行了 ./configure 并选择了第二种方式,即修改了安装目录,需要在 /etc/profile 中添加一些内容以确保系统能够正确地找到安装的 Protocol Buffers。

<code># 打开 /etc/profile 文件以编辑

sudo vim /etc/profile

# 添加内容如下:

# (动态库搜索路径) 程序加载运行期间查找动态链接库时指定除了系统默认路径之外的其他路径

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib/

# (静态库搜索路径) 程序编译期间查找动态链接库时指定查找共享库的路径

export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/protobuf/lib/

# 执行程序搜索路径

export PATH=$PATH:/usr/local/bin/

# C程序头文件搜索路径

export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/local/protobuf/include/

# C++程序头文件搜索路径

export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/local/protobuf/include/

# pkg-config 路径

export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/

最后一步,重新执行 /etc/profile 文件:

source /etc/profile

3、检查是否安装成功

输入 protoc --version 查看版本,有显示说明安装成功。

安装 ProtoBuf(Windows)

1、下载 ProtoBuf 编译器

下载地址:Releases · protocolbuffers/protobuf (github.com)

下载之后将压缩包解压到本地⽬录下。解压后的⽂件内包含 bin、include文件,以及⼀个 readme.txt。 

2、配置环境变量

把解压后⽂件中的bin目录配置到系统环境变量的Path中去:

3、检查是否配置成功 

打开cmd,输入:protoc --version

查看版本,有显示说明成功!

学习思路

Protocol Buffers(ProtoBuf)是由许多小的知识点组成的,所以对于完整的ProtoBuf学习,我们选择采用项目驱动的方式逐步深入。这种方法通过逐步升级项目的不同版本来讲解ProtoBuf的各个知识点,使学习过程更加系统和有条理。

在后续的内容中,我们将会实现一个通讯录项目。

在通讯录项目中,我们将使用Protocol Buffers(ProtoBuf)来定义联系人的数据结构,并使用其进行序列化和反序列化。随着项目的不断升级,我们将逐步深入了解和应用ProtoBuf的各种功能和特性。

项目的第一个版本可能会简单地定义一个基本的联系人数据结构,包括姓名、电话等基本属性。随着项目的不断升级,我们将会添加更多的功能和特性,比如支持联系人的分组、添加地址信息、自定义字段等等。

通过这个项目的推进,我们将会逐步学习和掌握ProtoBuf的以下知识点:

定义消息类型:学习如何使用ProtoBuf语言定义文件(.proto文件)来描述消息类型的结构。序列化和反序列化:学习如何使用ProtoBuf库将消息对象序列化为二进制格式并反序列化回消息对象。使用ProtoBuf编译器:学习如何使用protoc编译器将.proto文件编译成对应编程语言的代码文件。支持自定义选项:学习如何在.proto文件中添加自定义选项以控制生成代码的行为。支持扩展:学习如何在.proto文件中定义扩展点以支持后续的扩展。支持枚举类型:学习如何在消息类型中使用枚举类型定义常量。支持嵌套消息类型:学习如何在消息类型中嵌套定义其他消息类型。支持默认值:学习如何在.proto文件中为消息字段指定默认值。支持字段规则:学习如何在.proto文件中定义字段的规则,如必需字段、可选字段和重复字段。支持服务定义:学习如何在.proto文件中定义RPC服务以支持远程过程调用。

通过这个项目,我们将会逐步掌握ProtoBuf的各种功能,并将其应用到实际的项目中,加深对其理解和应用。

快速上手ProtoBuf

在通讯录 1.0 版本中,我们将实现以下功能:

使用ProtoBuf对单个联系人信息进行序列化,并将序列化结果打印出来。使用ProtoBuf对序列化后的内容进行反序列化,解析出联系人信息并将其打印出来。每个联系人包含以下信息:姓名、年龄。

通过通讯录 1.0 版本,我们可以掌握以下内容:

如何定义ProtoBuf消息类型,以描述联系人的信息。如何使用ProtoBuf编解码器对消息进行序列化和反序列化。如何在Java Maven项目中配置ProtoBuf依赖和构建工具。

这个版本将帮助我们初步了解如何使用ProtoBuf,以及整个使用流程是如何进行的。同时,我们还需要创建一个Maven项目来组织我们的代码。

准备工作

首先可以下载一个插件——Protobuf Support,它提供了对 Protocol Buffers(简称 Protobuf)文件的支持和集成。其主要作用包括:

语法高亮: 插件会对 Protobuf 文件进行语法高亮,使其更易于阅读和理解。

代码提示和自动补全: 在编写 Protobuf 文件时,插件会提供代码提示和自动补全功能,帮助您快速输入正确的语法和关键字。

语法检查和错误提示: 插件会检查您的 Protobuf 文件,发现并提示语法错误,帮助您及时发现并修复问题。

跳转和导航: 插件允许您通过快捷键或右键菜单在不同的 Protobuf 文件和定义之间进行跳转和导航,提高了代码的可读性和可维护性。

生成代码: 插件可以将 Protobuf 文件编译成各种目标语言(如Java、Python、C++等)的代码,以便在应用程序中使用 Protobuf 数据结构。

我找了一下没找到,安装了其他几个效果相似的: 

引入 ProtoBuf 包:

对于 ${protobuf_version},和我们在安装 PB 时的版本保持⼀致,例如 3.21.11 。更多版本可以在 Maven Central 中查看。

创建 .proto 文件

将新建的 .proto 文件统一放在项目中的 /src/main/proto 目录下。

文件规范对于项目的组织和维护非常重要。在创建 .proto 文件时,遵循一致的命名和缩进规范可以增加代码的可读性和可维护性。以下是对文件规范的扩写完善:

文件命名规范:

.proto 文件应该使用全小写字母命名,多个单词之间使用下划线 _ 连接,以提高可读性和一致性。例如,如果我们的项目名为 start,我们为通讯录 1.0 在 /src/main/proto/start 目录下新建的 .proto 文件应该命名为 contacts.proto。

代码书写规范:

在编写 .proto 文件的代码时,应该使用统一的缩进风格以增强代码的可读性和一致性。推荐使用两个空格的缩进风格,这是一种常见的约定,并且适用于大多数编程语言。

通过遵循这些文件规范,我们可以保持项目文件的清晰、一致,并提高代码的可维护性和可读性。

创建好文件之后我们就可以开始编写代码了。

指定 proto3 语法

Protocol Buffers语言版本3,简称proto3,是.proto文件的最新语法版本。proto3简化了Protocol Buffers语言,既易于使用,又可以在更广泛的编程语言中使用。它允许我们使用Java、C++、Python等多种语言生成protocol buffer代码。

在.proto文件中,要使用syntax = "proto3";来指定文件语法为proto3,并且必须写在除去注释内容的第一行。如果没有指定,编译器会使用proto2语法。

在通讯录1.0的contacts.proto文件中,可以为文件指定proto3语法。

package 声明符

当我们编写大型项目时,可能会有多个.proto文件,每个文件中定义了各种消息类型和服务。如果不指定package,则所有定义的消息类型和服务都将处于默认的命名空间中,可能会导致不同.proto文件中定义的消息类型发生冲突。

因此,使用package关键字可以为.proto文件指定命名空间,以确保其中定义的消息类型和服务在项目中的唯一性。这样可以更好地组织和管理项目的代码,避免命名冲突的发生。

在通讯录1.0的contacts.proto文件中,我们可以使用package关键字声明其命名空间,例如:

这样就为contacts.proto文件指定了命名空间"start",确保其中定义的消息类型和服务在项目中的唯一性。 

添加 JAVA 选项

在.proto文件中,使用option关键字可以为.proto文件中的各种元素(如消息、字段等)声明选项,以控制ProtoBuf编译器的行为方式。选项可以影响生成的代码的结构、命名约定、序列化方式等方面。

对于通讯录1.0的contacts.proto文件,我们可以添加JAVA选项来定制生成Java代码的行为。具体来说,可以通过JAVA选项指定生成的Java类所在的包名、外部类名等信息。

例如,我们可以使用java_package选项指定生成的Java类所在的包名。这样做可以使生成的Java类与其他类进行良好的组织和管理。另外,通过java_outer_classname选项,我们可以指定生成的Java类的外部类名,这在一些情况下可以使生成的Java代码更加清晰和易读。

总之,通过在.proto文件中添加JAVA选项,我们可以对生成的Java代码进行定制,以满足项目的特定需求和规范。这样可以使生成的代码与项目的整体架构更加协调一致,提高代码的可维护性和可读性。

定义消息(message)

消息(message)在ProtoBuf中是用于定义结构化对象的,它类似于其他编程语言中的类或结构体。消息可以包含各种属性(字段),每个属性都有自己的类型和名称。

为什么要定义消息呢?在网络通信中,双方需要遵循一定的协议来进行通信。这个协议就是指定了通信双方交换的数据格式和内容。类似于TCP、UDP等网络协议的报文结构,都是事先定义好的消息格式。同样地,在将数据存储到数据库中时,也需要将数据组织成一定的结构,再进行存储。

ProtoBuf通过定义消息来支持定制化的协议字段。这些消息定义了通信双方交换的数据结构和内容,以便在通信过程中进行数据的序列化和反序列化。后续,ProtoBuf会根据这些消息定义生成相应的类和方法,以便在编程中使用。

在通讯录1.0中,我们需要定义一个消息来表示联系人的信息。这个消息将包含联系人的各种属性,比如姓名、年龄等。通过定义消息,我们可以清晰地组织联系人信息,并在通信中进行传输和存储。

<code>message 消息类型名{

}

消息类型命名规范:使⽤驼峰命名法,首字母大写。

消息类型的命名规范通常采用驼峰命名法,即首字母小写,后续单词的首字母大写,以此类推。这样的命名风格能够提高代码的可读性和可维护性。

在ProtoBuf中,消息类型是通过message关键字定义的,因此遵循相应的命名规范是很重要的。例如,如果我们要定义一个表示人员信息的消息类型,可以命名为PersonInfo,其中Person为单词的首字母大写,Info为单词的首字母小写。

这种命名规范有助于使代码结构清晰,易于理解。同时,遵循统一的命名规范也有利于团队协作和代码维护。

// 首行: 语法指定行

syntax = "proto3";

package start;

option java_multiple_files = true;

option java_package = "com.example.start";

option java_outer_classname = "ContactsProtos";

// 定义联系人 message

message PeopleInfo {

}

定义消息字段

在message中,我们可以定义其属性字段,每个字段由三部分组成:字段类型、字段名和字段编号。

字段定义的一般格式为:

字段类型 字段名 = 字段唯一编号;

字段名称命名规范: 字段名称通常采用全小写字母,多个单词之间使用下划线 _ 连接,以提高可读性。例如:first_name、age等。字段类型: 字段类型分为标量数据类型和特殊类型。标量数据类型包括int32、string、bool等,而特殊类型包括枚举、其他消息类型等。例如:string、int32、MyEnum等。字段唯一编号: 字段编号是一个整数,用于标识字段。一旦开始使用,就不能再更改。字段编号在message内部必须是唯一的。例如:1、2、3等。

通过定义这些字段,我们可以描述消息的结构和属性,使其具有更丰富的表达能力。在通讯录1.0项目中,我们将使用这些规范来定义联系人消息的属性字段,以便对其进行序列化和反序列化操作。

下表列出了在消息体中定义的标量数据类型,以及编译 .proto 文件后自动生成的类中对应的字段类型,这里展示了与Java语言对应的类型:

.proto Type Notes Java Type
double double
float float
int32 使用变长编码[1]。负数的编码效率较低,若字段可为负值,应使用sint32代替。 int
int64 使用变长编码[1]。负数的编码效率较低,若字段可为负值,应使用sint64代替。 long
uint32 使用变长编码[1]。 int
uint64 使用变长编码[1]。 long
sint32 使用变长编码[1]。符号整型。负值的编码效率高于常规的int32类型。 int
sint64 使用变长编码[1]。符号整型。负值的编码效率高于常规的int64类型。 long
fixed32 定长4字节。若值常大于2^28则会比uint32更高效。 int
fixed64 定长8字节。若值常大于2^56则会比uint64更高效。 long
sfixed32 定长4字节。 int
sfixed64 定长8字节。 long
bool boolean
string 包含UTF-8和ASCII编码的字符串,长度不能超过2^32。 String
bytes 可包含任意的字节序列但长度不能超过2^32。 ByteString

[1] 变长编码是一种编码方式,用于减少存储空间和提高传输效率。在 Protocol Buffers 中,对于一些数据类型如 int32、int64、uint32、uint64、sint32、sint64 等,采用了变长编码。这种编码方式可以根据数值大小动态地决定使用的字节数,从而节省存储空间。

[2] 在 Java 中,无符号 32 位整数和无符号 64 位整数没有专门的数据类型来表示,通常使用它们对应的有符号整数来表示。对于 uint32 和 uint64,可以使用 Java 中的 int 和 long 类型来表示,而对于 sint32 和 sint64,则可以使用相应的有符号整数类型。在编码时,原始数据的第一个 bit 位被简单地存储在符号位中,因此有符号整数的编码方式和无符号整数的编码方式略有不同。

更新 contacts.proto (通讯录 1.0),新增姓名、年龄字段:

// 首行: 语法指定行

syntax = "proto3";

package start;

option java_multiple_files = true;

option java_package = "com.example.start";

option java_outer_classname = "ContactsProtos";

// 定义联系人 message

message PeopleInfo {

// 字段类型 字段名 = 字段唯一编号;

string name = 1;

int32 age = 2;

}

在这里还要特别说明一下字段唯⼀编号的范围:

在 Protocol Buffers 中,每个消息中的字段都需要有一个唯一的标识符,这个标识符就是字段的编号。字段编号用于在序列化和反序列化过程中标识消息中的各个字段,确保数据能够正确地被解析和处理。

字段编号的范围是从 1 到 536,870,911(即 2^29 - 1)。这个范围可以满足绝大多数情况下的需求,但在 Protobuf 协议的实现中,部分编号范围被预留出来,不能在 .proto 文件中使用。具体来说,编号范围在 19000 到 19999 的编号是被预留的,这意味着在定义字段时应避免使用这些预留的编号。

如果在 .proto 文件中使用了预留的编号范围内的编号,例如将字段的编号设置为 19000,编译 Protobuf 文件时会产生警告信息,提示用户避免使用这些预留编号,以免引发潜在的冲突或警告。因此,为了保证消息的定义规范和正确性,在定义字段时应当注意避免使用预留的编号范围内的值。

另外,在 Protocol Buffers 中,字段编号的范围对于编码的效率也有一定的影响。具体来说,范围为 1 到 15 的字段编号在编码时只需要占用一个字节,而范围为 16 到 2047 的字段编号则需要占用两个字节。

由于编码后的字节不仅包含了字段的编号,还包含了字段的类型信息,因此范围为 1 到 15 的字段编号特别适合用来标记那些出现频率较高的字段。这样做可以提高编码的效率,因为使用一个字节的编码可以节省空间和传输成本。

因此,对于那些可能会频繁出现的字段,我们应当尽量将其编号分配在范围为 1 到 15 的区间内,以便于在编码时能够获得更好的性能表现。而对于不太频繁出现的字段,可以将其编号分配在范围更大的区间内,这样虽然会占用更多的字节,但可以更充分地利用字段编号的空间。

编译 contacts.proto 文件,生成 JAVA 文件

编译的方式有两种:

使用命令行编译: 在这种方式下,我们可以通过命令行直接调用 Protocol Buffers 的编译器 protoc,并指定相应的参数来进行编译。这个过程需要手动执行,并且需要确保在系统中安装了 Protocol Buffers 编译器。通过命令行编译的优点是灵活性强,你可以直接控制编译的过程和参数设置。

使用 Maven 插件编译: 如果我们的项目是基于 Maven 进行管理的,就可以使用 Maven 插件来自动化编译过程。在 Maven 的 pom.xml 配置文件中,我们可以添加相应的插件依赖,并配置插件的参数,以便在 Maven 构建过程中自动执行 Protocol Buffers 编译。使用 Maven 插件编译的优点是集成度高,与项目的构建过程紧密结合,可以方便地管理依赖和执行编译任务。

方式一:使用命令行编译

编译命令行格式为:

protoc [--proto_path=IMPORT_PATH] --java_out=DST_DIR path/to/file.proto

protoc: 这是 Protocol Buffers 提供的命令行编译工具。--proto_path=IMPORT_PATH: 这个选项用来指定被编译的 .proto 文件所在的目录路径。我们可以多次使用这个选项来指定多个目录。也可以简写成 -I IMPORT_PATH。如果不指定该参数,编译器将在当前目录进行搜索。这个选项通常用于当某个 .proto 文件导入了其他 .proto 文件,或者需要编译的 .proto 文件不在当前目录时,需要指定搜索目录。--java_out=DST_DIR: 这个选项指定编译后生成的文件为 Java 文件,并且指定了生成文件的目标路径。在 DST_DIR 参数中,我们需要提供一个目录路径,编译后的 Java 文件将会被放置在该目录下。path/to/file.proto: 这是要编译的 .proto 文件的路径,需要我们提供该文件的完整路径。

编译 contacts.proto 文件命令如下:

protoc -I src\main\proto\start\ --java_out=src\main\java contacts.proto

方式二:使用 maven 插件编译:

在 Maven 的项目中,我们通过在 pom.xml 文件中添加 Protobuf 编译插件,可以实现自动执行 Protocol Buffers 的编译,相比手动执行 protoc 命令,这种方式更加高效和省心。当手动执行 protoc 命令时,可能需要记住一长串参数,或者每次都要去查找之前记录的笔记或者搜索相关文档,而通过配置 Maven 插件,这些繁琐的步骤都可以省去。

通过添加 Protobuf 编译插件到 Maven 项目的 pom.xml 文件中,我们可以在 Maven 构建过程中自动地进行 Protocol Buffers 的编译。这样,只需要简单地执行 Maven 构建命令,就能够自动完成 .proto 文件的编译,生成相应的 Java 文件,大大提高了开发效率,减少了出错的可能性,让开发者可以更专注于业务逻辑的实现,而不必过多关注编译过程中的繁琐细节。

在pom中添加 porotbuf 编译插件:

<code><plugin>

<groupId>org.xolstice.maven.plugins</groupId>

<artifactId>protobuf-maven-plugin</artifactId>

<version>0.6.1</version>

<configuration>

<!-- 本地安装的 protoc.exe 的目录 -->

<protocExecutable>E:\Protobuffers\protoc-22.3-win64\bin\protoc.exe</protocExecutable>

<!-- proto 文件放置的目录,默认为/src/main/proto -->

<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>

<!-- 生成文件的目录,默认生成到 target/generated-sources/protobuf/ -->

<outputDirectory>${project.basedir}/src/main/java</outputDirectory>

<!-- 是否清空目标目录,默认值为 true。这个最好设置为 false,以免误删项目文件!!! -->

<clearOutputDirectory>false</clearOutputDirectory>

</configuration>

</plugin>

 

编译 contacts.proto 文件后会生成什么?

同时在我们的对应目录下也生成了Java代码:

在编译 contacts.proto 文件时,根据选择的语言(在这里是JAVA),会生成对应的代码文件。在这个例子中,我们选择了JAVA语言,因此会生成三个文件:ContactsProtos.java、PeopleInfo.java和PeopleInfoOrBuilder.java。

这里的java_multiple_files是一个选项,用于指定是否要将生成的Java类分成多个文件。如果设置为true,每个message会生成一个单独的Java文件;如果设置为false,则所有message会合并到一个Java文件中。默认情况下,该选项为true。

如果在contacts.proto文件中不设置option java_multiple_files = true;,或者将其设置为false,则生成的三个文件会合并成一个ContactsProtos.java文件。

ContactsProtos.java文件用于管理.proto文件生成的描述符和资源,而PeopleInfoOrBuilder.java文件定义了一个用于访问PeopleInfo消息字段的接口。

ContactsProtos.java

这个文件定义了一个名为ContactsProtos的Java类,用于管理与.proto文件相关的资源和描述符。它包含了注册扩展、获取描述符、以及一些内部静态属性的方法和代码。通过静态属性internal_static_start_PeopleInfo_descriptor和internal_static_start_PeopleInfo_fieldAccessorTable,它关联了一个名为PeopleInfo的message类型,该类型在contacts.proto文件中定义。该类的构造函数被私有化,意味着无法在外部实例化该类。

PeopleInfoOrBuilder.java

这个文件定义了一个名为PeopleInfoOrBuilder的接口,它是一个扩展了MessageOrBuilder接口的子接口。该接口包含了获取name和age字段值的方法,以及相应字段的描述注释。通过注释和方法签名,我们可以了解到name字段的类型为string,字段编号为1,而age字段的类型为int32,字段编号为2。

了解了上面两个文件的大致内容,我们就来看看最重要的一个文件:

当我们使用Protocol Buffers编译器(protoc)将.proto文件编译成Java代码时,针对每个在.proto文件中定义的message,编译器会生成对应的Java类。

其中,每个message类会有一个内部的Builder类,用于创建该message类的实例。

在生成的message类中,我们可以找到以下内容:

静态方法newBuilder():这是一个静态方法,用于创建对应message类的Builder实例。Builder类提供了一种流畅的方式来设置message中的字段值,并最终构建出一个完整的message对象。

静态方法newBuilder():这是一个静态方法,用于创建对应message类的Builder实例。Builder类提供了一种流畅的方式来设置message中的字段值,并最终构建出一个完整的message对象。获取字段值的方法(getters):对于每个字段,编译器都会生成一个对应的getter方法,用于获取该字段的值。这些方法允许我们从ProtoBuf消息中获取数据。

序列化和反序列化方法:编译器还会为每个message类生成用于序列化(将对象转换为字节序列)和反序列化(将字节序列转换为对象)的方法。这些方法允许我们在不同系统之间轻松地传输和存储数据。

在Builder类中,我们可以找到以下内容:

字段的设置方法:编译器为每个字段生成了相应的设置方法,以便在构建message对象时设置字段的值。这些方法通常具有链式调用的特性,使得代码更加清晰和易读。

build()方法:这个方法是用于构造最终的message对象的。一旦我们设置了所有字段的值,就可以调用build()方法来生成一个不可变的message实例。

这些自动生成的类和方法为我们提供了方便的接口,使得在Java中使用ProtoBuf变得更加简单和高效。通过这些类和方法,我们可以轻松地创建、序列化和反序列化ProtoBuf消息,从而在不同的系统之间进行数据交换和通信。

我们可以对PeopleInfo.java文件进行更具体的分析:

文件头部注释:提示该文件是由Protocol Buffer编译器生成的,不应手动编辑,并指定了.proto文件的源路径。

包声明和类定义

指定了Java包名为com.example.start,并定义了名为PeopleInfo的Java类。这个类继承自com.google.protobuf.GeneratedMessageV3类,表示它是一个由Protocol Buffer编译器生成的消息类,并实现了PeopleInfoOrBuilder接口。

私有构造函数:私有构造函数用于构造PeopleInfo类的实例。外部无法直接实例化该类,只能通过PeopleInfo.newBuilder()方法进行构造。

静态方法和字段

getDescriptor()方法返回该类的描述符。internalGetFieldAccessorTable()方法返回字段访问器表。定义了两个静态字段NAME_FIELD_NUMBER和AGE_FIELD_NUMBER,分别表示name和age字段的编号。

字段定义

name_和age_分别表示name和age字段。name_字段为String类型,存储联系人的姓名。age_字段为int类型,存储联系人的年龄。

Getter方法

getName()方法返回联系人的姓名。getAge()方法返回联系人的年龄。

构建器类

内部定义了一个名为Builder的构建器类,用于构建PeopleInfo实例。构建器类提供了链式调用的方法,用于设置name和age字段的值。

其他方法:包括parseFrom、writeTo、getSerializedSize等方法,用于序列化和反序列化消息,以及计算消息的大小。

综上所述,PeopleInfo.java文件定义了一个与protobuf消息对应的Java类,用于存储联系人的姓名和年龄,并提供了相应的方法用于获取和设置字段值,以及序列化和反序列化消息。

当我们使用 Protocol Buffers 编译器生成的 Java 类时,通常会包含一些用于操作消息对象的其他方法,包括以下方法:

parseFrom 方法:用于从字节序列中解析出消息对象的实例。这个方法通常用于反序列化操作,将二进制数据转换为消息对象的实例。

writeTo 方法:用于将消息对象序列化为字节序列并写入到输出流中。这个方法通常用于将消息对象转换为二进制数据以便进行传输或存储。

getSerializedSize 方法:用于获取消息对象序列化后的字节大小。这个方法通常用于在序列化之前确定消息对象的大小,以便为输出流分配足够的空间。

clear 方法:用于清除消息对象中的所有字段值,将其重置为默认值。例如,对于 PeopleInfo 类,可能会有 clearName() 和 clearAge() 方法来清除对应的姓名和年龄字段。

toBuilder 方法:用于创建当前消息对象的构建器实例,可以用于修改消息的字段值。通常情况下,当我们需要对消息进行修改时,可以使用 toBuilder 方法创建一个可修改的副本,而不直接修改原始消息对象。

mergeFrom 方法:用于将另一个消息对象的字段值合并到当前消息对象中。这个方法通常用于将两个消息对象的值进行合并,例如在处理更新操作时可能会用到。

isInitialized 方法:用于检查消息对象是否已经初始化,即是否所有必需字段都已经设置了值。通常情况下,会在对消息进行序列化之前调用此方法来确保消息的完整性。

getDefaultInstance 方法:用于获取消息类型的默认实例。默认实例是消息类型的一个静态实例,包含所有字段都设置为默认值的空消息对象。

hashCode 和 equals 方法:用于对消息对象进行哈希码计算和相等性比较。这些方法通常用于将消息对象存储在集合中或进行其他类型的比较操作。

这些方法可以帮助我们对消息对象进行序列化、反序列化、修改、合并和比较等操作,使得在使用 Protocol Buffers 进行数据通信时更加方便和高效。当然还可能会有其他特定于消息类型的方法,具体取决于我们在 .proto 文件中定义的消息结构。

序列化与反序列化的使用

我们将创建一个名为 FastStart.java 的测试文件,在其中实现以下功能:

我们将使用 Protocol Buffers(PB)对一个联系人的信息进行序列化,并将结果打印出来。我们将使用 Protocol Buffers 对序列化后的内容进行反序列化,解析出联系人信息,并将其打印出来。

<code>package com.example.start;

import com.google.protobuf.InvalidProtocolBufferException;

import java.util.Arrays;

public class FastStart {

public static void main(String[] args) throws InvalidProtocolBufferException {

PeopleInfo p1 = PeopleInfo.newBuilder()

.setName("小米")

.setAge(20)

.build();

// 序列化

byte[] bytes = p1.toByteArray();

System.out.println("序列化结果为:" + Arrays.toString(bytes));

// 反序列化

PeopleInfo p2 = PeopleInfo.parseFrom(bytes);

System.out.println("反序列化结果为:");

System.out.println("姓名:" + p2.getName());

System.out.println("年龄:" + p2.getAge());

}

}

当我们使用ProtoBuf对联系人对象进行序列化时,它将对象的各个字段值转换为二进制格式,这样就可以在不同的系统之间进行传输或存储。在Java中,我们通常使用byte[]数组来接收这些二进制数据,以便我们可以查看序列化后的结果或者将其传输到其他地方。

Protocol Buffers (ProtoBuf) 的序列化方法会将数据对象编码成二进制序列。这种二进制格式相比于文本格式(如 XML 和 JSON)更加紧凑,因此占用的存储空间更小,传输效率更高。二进制序列化的另一个优势是它们通常比文本格式更快速地进行序列化和反序列化操作,这对于高性能应用程序非常重要。

由于 ProtoBuf 序列化的结果是二进制数据,因此相比于文本格式,它们不太容易被人类读取和理解。这也提高了数据的安全性,因为破解二进制序列相对于破解文本格式更加困难。这种二进制格式还使得数据在网络上传输时更加高效,因为它们可以以原始字节的形式直接传输,而无需进行额外的文本编码和解码过程。

总之,Protocol Buffers 的二进制序列化提供了一种高效、紧凑和安全的数据交换方式,适用于各种不同的应用场景。

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。