- Protocol Buffers 教程
- Protocol Buffers - 首页
- Protocol Buffers - 简介
- Protocol Buffers - 基本应用
- Protocol Buffers - 构造
- Protocol Buffers - 消息
- Protocol Buffers - 字符串
- Protocol Buffers - 数字
- Protocol Buffers - 布尔值
- Protocol Buffers - 枚举
- Protocol Buffers - 重复
- Protocol Buffers - 映射
- Protocol Buffers - 嵌套类
- Protocol Buffers - 可选性和默认值
- Protocol Buffers - 语言独立性
- Protocol Buffers - 复合数据类型
- Protocol Buffers - 命令行使用
- Protocol Buffers - 更新定义规则
- Protocol Buffers - 与 Kafka 集成
- Protocol Buffers - 其他语言
- Protocol Buffers 有用资源
- Protocol Buffers - 快速指南
- Protocol Buffers - 有用资源
- Protocol Buffers - 讨论
Protocol Buffers - 快速指南
Protocol Buffers - 简介
在深入了解 Protocol Buffer 之前,让我们先简单了解一下序列化,因为 Protocol Buffer 就是用来做序列化的。
什么是序列化和反序列化?
序列化是指将一个对象(任何语言的对象)转换为字节,并将其存储在持久性内存系统中的过程。这个内存系统可以是磁盘上的文件、消息队列或数据库。序列化对象的主要目的是为了能够重用数据,并在同一台或不同的机器上重新创建该对象。在反序列化中,我们将存储的字节转换回对象。
为什么我们需要序列化和反序列化?
虽然还有其他一些用例,但最基本和最重要的用例是它提供了一种方法,可以通过网络将对象数据传输到不同的服务/机器等,然后重新创建对象以供进一步使用。通过 API、数据库或消息队列传输对象数据需要将对象转换为字节,以便可以通过网络发送。这就是序列化变得重要的原因。
在微服务架构中,应用程序被分解成小的服务,这些服务通过消息队列和 API 相互通信。所有这些通信都发生在网络上,这需要频繁地将对象转换为字节,然后再转换回对象。因此,在分布式环境中,序列化和反序列化变得非常关键。
为什么选择 Google Protocol Buffers?
Google Protocol Buffers 执行将对象序列化为字节的过程,这些字节可以传输到网络上。但也有其他一些库和机制可以传输数据。
那么,是什么让 Google Protocol Buffers 如此特别呢?以下是一些重要的特性:-
语言独立 - 多种语言都有 Protocol Buffers 库,其中一些著名的库包括 Java、Python、Go 等。因此,Java 对象可以由 Java 程序序列化为字节,并且可以反序列化为 Python 对象。
高效的数据压缩 - 在微服务环境中,考虑到网络上会发生大量的通信,因此我们发送的数据尽可能简洁至关重要。我们需要避免任何多余的信息,以确保数据能够快速传输。Google Protocol Buffers 将此作为其重点关注领域之一。
高效的序列化和反序列化 - 在微服务环境中,考虑到网络上会发生大量的通信,因此序列化和反序列化的速度至关重要。Google Protocol Buffers 确保序列化和反序列化数据的速度尽可能快。
易于使用 - Protocol Buffers 库会自动生成序列化代码(我们将在接下来的章节中看到),并具有版本控制方案,以确保数据创建者和数据使用者可以拥有序列化定义的不同版本等。
Protocol Buffers 与其他方案比较 (XML/JSON/Java 序列化)
让我们看看其他网络数据传输方式与 Protocol Buffers 相比如何。
特性 | Protocol Buffers | JSON | XML |
---|---|---|---|
语言独立 | 是 | 是 | 是 |
序列化数据大小 | 三者中最小的 | 小于 XML | 三者中最大的 |
人类可读 | 否,因为它使用单独的编码方案 | 是,因为它使用基于文本的格式 | 是,因为它使用基于文本的格式 |
序列化速度 | 三者中最快的 | 比 XML 快 | 三者中最慢的 |
数据类型支持 | 比其他两者更丰富。支持复杂数据类型,如 Any、oneof 等。 | 支持基本数据类型 | 支持基本数据类型 |
支持演进模式 | 是 | 否 | 否 |
Protocol Buffers - 基本应用
概述
现在让我们使用 Google Protocol Buffer 并了解它在一个简单的问候应用中是如何工作的。在这个示例中,我们将创建一个简单的应用程序,该应用程序将执行以下操作:-
问候编写者 -
从用户处获取问候语和用户名
将上述信息存储在磁盘上的文件中
问候读取者 -
读取我们在上述文件中存储的同一文件
将该数据转换为对象并打印数据
Protocol Buffer 定义文件
协议缓冲区“定义文件”包含我们要序列化的数据的模式定义。数据存储在扩展名为".proto" 的人类可读文件中。
让我们将以下数据存储在greeting.proto 中,我们将在第一个应用程序中使用它。
syntax = "proto3"; package tutorialspoint; option java_package = "com.tutorialspoint.greeting"; message Greet { string greeting = 1; string username = 2; }
理解每个构造
现在,让我们仔细看看数据,并了解上面代码块中每一行代码的作用。
syntax = "proto3";
这里的syntax 表示我们使用的是哪个版本的 Protobuf。因此,我们使用的是最新的版本 3,因此模式可以使用对版本 3 有效的所有语法。
package tutorialspoint;
这里的package 用于冲突解决,例如,如果我们有多个具有相同名称的类/成员。
option java_package = "com.tutorialspoint.greeting";
此参数特定于 Java,即.proto 文件中代码将自动生成的包。
message Greet
将要创建/重新创建的对象的基本类的名称。
string greeting = 1; string username = 2;
这些是Greet 类的属性,以及数据类型和模式中标签的位置。如果要添加新的标签,它应该将“3”作为位置。请注意,此位置整数对于确保实际数据紧凑且有模式演进的余地非常重要。
Protocol Buffer 代码生成
现在我们已经定义了,让我们安装“proto”二进制文件,我们将使用它来自动生成上面Greet 类的代码。可以在"https://github.com/protocolbuffers/protobuf/releases/".找到二进制文件。
根据操作系统选择正确的二进制文件。我们将在 Windows 上安装 proto 二进制文件,但 Linux 的步骤差别不大。
我们已经下载了https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-win64.zip
验证 Proto 编译器设置
安装完成后,确保可以通过命令行访问它:-
protoc --version libprotoc 27.3
它确认 Protobuf 已正确安装。现在让我们继续为 Java创建上面描述的问候应用程序。
项目结构
以下是我们将拥有的整体项目结构:-
Java 中的问候应用程序
现在我们已经安装了protoc,我们可以使用protoc从 proto 文件自动生成代码。不过,让我们先创建一个 Java 项目。
以下是我们将用于 Java 项目的 Maven 配置。请注意,它还包含Protobuf-java所需的库。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.tutorials.point</groupId> <artifactId>protobuf-tutorial</artifactId> <version>1.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java --> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>4.27.3</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <configuration> <!--Put your configurations here--> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
我们所有的代码都将位于src/main/java下。
在项目结构完成后,让我们为Greet类生成代码:-
生成 Java 类
protoc --java_out=. greeting.proto
执行命令后,您将在当前目录内的com > tutorialspoint > greeting文件夹下看到一个自动生成的类。
Greeting.java
此文件包含一个类Greeting和一个接口GreetOrBuilder,它们将帮助我们序列化和反序列化Greet对象。
使用生成的 Java 类
现在,让我们编写数据的写入器,它将以用户名和问候语作为输入:-
GreetWriter.java
package com.tutorialspoint.greeting; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.greeting.Greeting.Greet; public class GreetWriter{ public static void main(String[] args) throws IOException { Greet greeting = Greet.newBuilder() .setGreeting(args[0]) .setUsername(args[1]) .build(); String filename = "greeting_protobuf_output"; System.out.println("Saving greeting to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ greeting.writeTo(output); } System.out.println("Saved greeting with following data to disk: \n" + greeting); } }
写入器只需获取 CLI 参数,创建Greet对象,将其序列化,然后将其转储到文件中。
现在让我们编写一个将读取文件的读取器:-
GreetReader.java
package com.tutorialspoint.greeting; import java.io.FileInputStream; import java.io.IOException; import com.tutorialspoint.greeting.Greeting.Greet; public class GreetReader{ public static void main(String[] args) throws IOException { Greet.Builder greetBuilder = Greet.newBuilder(); String filename = "greeting_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Greet greet = greetBuilder.mergeFrom(input).build(); System.out.println("Greeting: " + greet.getGreeting() + "\n" + "Username: " + greet.getUsername()); } } }
读取器只需从同一文件读取,将其反序列化,并打印有关问候语的数据。
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,让我们首先执行写入器以将对象序列化到文件系统中。
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.greeting.GreetWriter Hello John Saving greeting to file: greeting_protobuf_output Saved greeting with following data to disk: greeting: Hello username: John
反序列化已序列化的对象
然后,让我们执行读取器以从文件系统中反序列化对象。
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.greeting.GreetReader Reading from file greeting_protobuf_output Greeting: Hello Username: John
因此,正如我们看到的,写入器序列化并保存到文件中的数据,被读取器正确地反序列化并相应地打印出来。
Python 中的问候应用程序
现在让我们将同一个示例编写为 Python 项目:-
安装 protocol buffers 库
在继续之前,我们需要安装protobuf pip 包。
pip install protobuf
我们所有的代码都将位于python目录下。
从 proto 文件生成 Python 类
在项目结构完成后,让我们为 Greet 类生成代码:-
protoc --python_out=. greeting.proto
执行此命令后,您将在当前目录中看到一个自动生成的类greeting_pb2.py。此类将帮助我们序列化和反序列化Greet对象。
使用生成的 Python 类
现在,让我们编写数据的写入器,它将以用户名和问候语作为输入:-
greetWriter.py
import greeting_pb2 import sys greet = greeting_pb2.Greet() greet.username = sys.argv[1] greet.greeting = sys.argv[2] filename = "greeting_protobuf_output"; print("Saving to file: " + filename) f = open(filename, "wb") f.write(greet.SerializeToString()) f.close() print("Saved following greeting to disk: \n" + str(greet))
写入器只需获取 CLI 参数,创建Greet对象,将其序列化,然后将其转储到文件中。
现在让我们创建一个将读取文件的读取器:-
greetReader.py
import greeting_pb2 greet = greeting_pb2.Greet() filename = "greeting_protobuf_output"; print("Reading from file: " + filename) f = open(filename, "rb") greet.ParseFromString(f.read()) f.close() print("Read greeting from disk: \n" + str(greet))
读取器只需从同一文件读取,将其反序列化,并打印有关问候语的数据。
序列化 Python 对象
现在,让我们首先执行写入器。
py greetWriter.py Hola Jane Saving to file: greeting_protobuf_output Saved following greeting to disk: greeting: "Hola" username: "Jane"
反序列化 Python 对象
然后,让我们执行读取器。
python greetReader.py Reading from file: greeting_protobuf_output Read greeting from disk: greeting: "Hola" username: "Jane"
因此,正如我们所看到的,写入器序列化并保存到文件中的数据。接下来,读取器正确地反序列化了相同的数据并相应地打印出来。
Protocol Buffers - 构造
概述
现在让我们来看一下 Google Protocol Buffers 提供的一些基本数据结构和数据类型。我们将使用电影院的例子来了解这些数据结构。
请注意,虽然我们将使用 Java 代码来演示这个结构,但在 Python 代码中使用它们也同样简单且可行。
在接下来的几章中,我们将逐一讨论以下 Protocol Buffers 数据类型:
数据类型
message − “message” 是 Protocol Buffers 的一个非常基本的基础构建块。它在我们使用的语言(例如 Java、Python 等)中转换为类。
string − “string” 数据类型在我们使用的语言(例如 Java、Python 等)中转换为字符串。
Numbers − 数字包括 Protocol Buffers 类型,如 int32、int64、float、double,它们是 Protobuf 的基本构建块。它分别在我们在使用的语言(例如 Java、Python 等)中转换为 int、long、float、double。
bool − “bool” 数据类型是 Protocol Buffers 的基本构建块之一。它在我们使用的语言(例如 Java、Python 等)中转换为布尔值。
enum − “enum” 是 Protocol Buffers 的复合数据类型之一。它在我们使用的语言(例如 Java)中转换为枚举。
repeated − “repeated” 用于创建数组或列表,是 Protocol Buffers 的复合数据类型之一。Protocol Buffers 将其转换为 Java 中的 java.util.list 接口。
map − “Map” 是 Protocol Buffers 的复合数据类型之一。Protocol Buffers 将其转换为 Java 中的 java.util.Map 接口。
嵌套类 − 我们可以使用用“message”创建的类在另一个“message”中,从而创建嵌套类。Protocol Buffers 将其转换为嵌套的 Java 类。
Protocol Buffers - 消息
概述
Protocol Buffers 的最基本构建块是message属性。它在我们使用的语言(例如 Java、Python 等)中转换为类。
代码示例
以下是我们需要使用的语法,用于指示 Protocol Buffers 我们将创建给定类的实例:
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { }
我们将以上内容保存在“theater.proto”中,并在我们探索其他数据结构时使用它。
解释
这里的“syntax”表示我们使用的是哪个版本的 Protocol Buffers。因此,我们使用的是最新的版本 3,并且该模式可以使用所有对版本 3 有效的语法。
syntax = "proto3";
这里的包用于冲突解决,例如,如果我们有多个同名的类/消息。
package tutorial;
此参数特定于 Java,即“.proto”文件中的代码将自动生成的包。
option java_package = "com.tutorialspoint.greeting";
现在我们完成了先决条件,这里最后一项是:
message Theater
这仅仅是将要创建/重新创建的对象的基础类的类名。请注意,它在当前状态下毫无用处,因为它没有任何其他属性。但随着我们继续,我们将添加更多属性。
使用多个 message 属性
单个 proto 文件也可以有多个类/消息。例如,如果我们想,我们也可以在同一个文件中添加Visitors消息/类。Protocol Buffers 将确保为其创建两个独立的类。例如:
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { } message Visitor { }
从 proto 文件创建 Java 类
要使用 Protocol Buffers,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. proto_files\theater.proto
使用从 proto 文件创建的 Java 类
就是这样!以上命令应该在当前目录中创建所需的文件,现在我们可以在我们的 Java 代码中使用它们了:
Theater theater = Theater.newBuilder().build() Visitor visitor = Visitor.newBuilder().build()
在这个阶段,它没有太大用处,因为我们还没有向成员/类添加任何属性。当我们在Protocol Buffers - string章节中查看字符串时,我们将执行此操作。
Protocol Buffers - 字符串
概述
Protobuf 字符串在我们在使用的语言(例如 Java、Python 等)中转换为字符串。继续使用theater示例,以下是我们需要使用的语法,用于指示 Protobuf 我们将创建一个字符串:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; }
现在我们的类/消息包含两个字符串属性。它们每个都具有一个位置,这是 Protobuf 在序列化和反序列化时使用的。成员的每个属性都需要具有唯一的位置属性。
从 Proto 文件创建 Java 类
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
首先让我们创建一个writer来写入theater信息:
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" address: "212, Maple Street, LA, California"
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener" address: "212, Maple Street, LA, California"
因此,正如我们所看到的,我们能够通过将二进制数据反序列化到Theater对象来读取序列化的字符串。在下一章Protocol Buffers - Numbers中,我们将了解数字。
Protocol Buffers - 数字
概述
数字包括 protobuf 类型,如int32、int64、float、double,它们是 Protobuf 的基本构建块。它分别在我们在使用的语言(例如 Java、Python 等)中转换为int、long、float、double。
继续我们来自Protocol Buffers - String章节的theater示例,以下是我们需要使用的语法,用于指示 Protobuf 我们将创建数字:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { int32 total_capcity = 3; int64 mobile = 4; float base_ticket_price = 5; }
现在我们的类/消息包含数值属性。它们每个都具有一个位置,这是 Protobuf 在序列化和反序列化时使用的。成员的每个属性都需要分配一个唯一的数字。
从 Proto 文件创建 Java 类
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setTotalCapcity(320) .setMobile(98234567189L) .setBaseTicketPrice(22.45f) .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater.getBaseTicketPrice()); System.out.println(theater); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: total_capcity: 320 mobile: 98234567189 base_ticket_price: 22.45
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output 22.45 total_capcity: 320 mobile: 98234567189 base_ticket_price: 22.45
因此,正如我们所看到的,我们能够通过将二进制数据反序列化到Theater对象来读取序列化的int、float和long。在下一章Protocol Buffers - bool中,我们将了解布尔类型。
Protocol Buffers - 布尔值
概述
bool数据类型是 Protobuf 的基本构建块之一。它在我们使用的语言(例如Java、Python等)中转换为布尔值。
继续我们来自Protocol Buffers - String章节的theater示例,以下是我们需要使用的语法,用于指示 Protobuf 我们将创建bool:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { bool drive_in = 6; }
现在我们的消息类包含一个bool属性。它还有一个位置,这是 Protobuf 在序列化和反序列化时使用的。成员的每个属性都需要分配一个唯一的数字。
从 Proto 文件创建 Java 类
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setDriveIn(true) .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater.getDriveIn()); System.out.println(theater); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: drive_in: true
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output drive_in: true
因此,正如我们所看到的,我们能够通过将二进制数据反序列化到Theater对象来读取序列化的bool。在下一章Protocol Buffers - enum中,我们将了解枚举,一种复合类型。
Protocol Buffers - 枚举
概述
enum数据类型是 Protobuf 的复合数据类型之一。它在我们使用的语言(例如Java等)中转换为枚举。
继续我们来自Protocol Buffers - String章节的theater示例,以下是我们需要使用的语法,用于指示 Protobuf 我们将创建一个enum:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { enum PAYMENT_SYSTEM{ CASH = 0; CREDIT_CARD = 1; DEBIT_CARD = 2; APP = 3; } PAYMENT_SYSTEM payment = 7; }
现在我们的消息类包含一个enum属性。它还有一个位置,这是 Protobuf 在序列化和反序列化时使用的。成员的每个属性都需要分配一个唯一的数字。
我们定义了enum并在下面将其与“payment”属性一起用作数据类型。请注意,尽管我们在消息类内部定义了枚举,但它也可以驻留在其外部。
从 Proto 文件创建 Java 类
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.PAYMENT_SYSTEM; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setPayment(PAYMENT_SYSTEM.CREDIT_CARD) .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater.getPayment()); System.out.println(theater); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: payment: CREDIT_CARD
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output CREDIT_CARD payment: CREDIT_CARD
因此,正如我们所看到的,我们能够通过将二进制数据反序列化到Theater对象来读取序列化的enum。在下一章Protocol Buffers - repeated中,我们将了解repeated,一种复合类型。
Protocol Buffers - 重复
概述
repeated数据类型是 Protobuf 的复合数据类型之一。它在Java中转换为java.util.List接口。
继续我们来自Protocol Buffers - String章节的theater示例,以下是我们需要使用的语法,用于指示 Protobuf 我们将创建一个repeated:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { repeated string snacks = 8; }
现在我们的消息类包含一个repeated属性。请注意,尽管我们有一个字符串repeated,但我们也可以有数字、布尔值、自定义数据类型列表。它还有一个位置,这是 Protobuf 在序列化和反序列化时使用的。成员的每个属性都需要分配一个唯一的数字。
从 Proto 文件创建 Java 类
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
TheaterWriter.java
package com.tutorialspoint.theater; import java.util.List; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { List<String> snacks = new ArrayList<>(); snacks.add("Popcorn"); snacks.add("Coke"); snacks.add("Chips"); snacks.add("Soda"); Theater theater = Theater.newBuilder() .addAllSnacks(snacks) .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: snacks: "Popcorn" snacks: "Coke" snacks: "Chips" snacks: "Soda"
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output snacks: "Popcorn" snacks: "Coke" snacks: "Chips" snacks: "Soda"
因此,正如我们所看到的,我们能够通过将二进制数据反序列化到Theater对象来读取序列化的repeated。在下一章Protocol Buffers - map中,我们将了解map,一种复合类型。
Protocol Buffers - 映射
概述
map数据类型是 Protobuf 的复合数据类型之一。它在Java中转换为java.util.Map接口。
继续我们来自Protocol Buffers - String章节的theater示例,以下是我们需要使用的语法,用于指示 Protobuf 我们将创建一个repeated:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { map<string, int32> movieTicketPrice = 9; }
现在我们的消息类包含一个电影及其票价的map。请注意,尽管我们有“string -> int”map,但我们也可以有数字、布尔值和自定义数据类型。但是,请注意,我们不能有嵌套的map。它还有一个位置,这是 Protobuf 在序列化和反序列化时使用的。成员的每个属性都需要分配一个唯一的数字。
从 Proto 文件创建 Java 类
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Map<String, Integer> ticketPrice = new HashMap<>(); ticketPrice.put("Avengers Endgame", 700); ticketPrice.put("Captain America", 200); ticketPrice.put("Wonder Woman 1984", 400); Theater theater = Theater.newBuilder() .putAllMovieTicketPrice(ticketPrice) .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: movieTicketPrice { key: "Avengers Endgame" value: 700 } movieTicketPrice { key: "Captain America" value: 200 } movieTicketPrice { key: "Wonder Woman 1984" value: 400 }
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output movieTicketPrice { key: "Avengers Endgame" value: 700 } movieTicketPrice { key: "Captain America" value: 200 } movieTicketPrice { key: "Wonder Woman 1984" value: 400 }
因此,正如我们所看到的,我们能够通过将二进制数据反序列化到Theater对象来读取序列化的map。在下一章Protocol Buffers - Nested Class中,我们将了解嵌套类。
Protocol Buffers - 嵌套类
概述
在这里,我们将看到如何创建嵌套类。Protobuf 将其转换为嵌套的Java类。
继续我们来自Protocol Buffers - String章节的theater示例,以下是我们需要使用的语法,用于指示 Protobuf 我们将创建一个repeated:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { TheaterOwner owner = 10; } message TheaterOwner{ string name = 1; string address = 2; }
现在我们的消息类包含一个嵌套类,即影院所有者信息。
从 Proto 文件创建 Java 类
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.TheaterOwner; public class TheaterWriter{ public static void main(String[] args) throws IOException { TheaterOwner owner = TheaterOwner.newBuilder() .setName("Anthony Gonsalves") .setAddress("513, St Paul Street, West Coast, California") .build(); Theater theater = Theater.newBuilder() .setOwner(owner) .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: owner { name: "Anthony Gonsalves" address: "513, St Paul Street, West Coast, California" }
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output owner { name: "Anthony Gonsalves" address: "513, St Paul Street, West Coast, California" }
因此,正如我们所看到的,我们能够通过将二进制数据反序列化到Theater对象来读取序列化的嵌套类。
Protocol Buffers - 可选性和默认值
概述
虽然我们已经了解了各种数据类型以及如何使用它们,但如果在序列化时不指定值会发生什么?“proto2”版本支持“required”和“optional”标签,这有助于确定如果所需的解析逻辑不可用,序列化/反序列化是否应该失败。但“required”标签在“proto3”版本中被移除。失败部分需要由相应的代码处理。现在每个属性都是可选的,并且都有默认值。因此,从“proto3”版本开始,“optional”的使用就变得多余了。
Protocol Buffers根据下表支持其数据类型的默认值:
数据类型 | 默认值 |
---|---|
Int32 / Int64 | 0 |
Float/double | 0.0 |
String | 空字符串 |
Boolean | False |
Enum | 第一个枚举项,即“index=0”的项 |
重复类型 | 空列表 |
Map | 空Map |
嵌套类 | null |
因此,如果未为这些数据类型指定数据,则它们将采用上述默认值。现在,让我们继续使用我们的theater示例来演示其工作原理。
在此示例中,我们将让所有字段都使用默认值。唯一指定的字段将是剧院的名称。
继续我们从Protocol Buffers - String 章节中的theater示例,以下是我们需要使用的语法来指示Protobuf我们将创建不同的数据类型:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; int32 total_capcity = 3; int64 mobile = 4; float base_ticket_price = 5; bool drive_in = 6; enum PAYMENT_SYSTEM { CASH = 0; CREDIT_CARD = 1; DEBIT_CARD = 2; APP = 3; } PAYMENT_SYSTEM payment = 7; repeated string snacks = 8; map<string, int32> movieTicketPrice = 9; TheaterOwner owner = 10; } message TheaterOwner{ string name = 1; string address = 2; }
现在我们的message类包含多个属性。
从 Proto 文件创建 Java 类
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter { public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("SilverScreen") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println( "Name:" + theater.getName() + "\n" + "Address:" + theater.getAddress() + "\n" + "Drive_In:" + theater.getDriveIn() + "\n" + "Total Capacity:" + theater.getTotalCapcity() + "\n" + "Base Ticket Prices: " + theater.getBaseTicketPrice() + "\n" + "Owner: " + theater.getOwner() + "\n" + "Snacks: " + theater.getSnacksList() + "\n" + "Payment: " + theater.getPayment() ); //Map<FieldDescriptor, Object> f = theater.getAllFields(); System.out.println("List of fields explicitly specified: " + theater.getAllFields()); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "SilverScreen"
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output Name:SilverScreen Address: Drive_In:false Total Capacity:0 Base Ticket Prices: 0.0 Owner: Snacks: [] Payment: CASH List of fields explicitly specified: {theater.Theater.name=SilverScreen}
因此,正如我们所看到的,除了我们已明确指定为最底层行的name之外,所有值都相应地使用了默认值。
Protocol Buffers - 语言独立性
概述
到目前为止,我们一直在使用Java来序列化和反序列化电影院数据。但是,Google Protocol Buffers提供的关键功能之一是“语言独立性”。在本节中,我们将了解如何使用Java进行序列化,并使用Python进行反序列化。
继续我们从Protocol Buffers - String 章节中的theater示例,以下是我们需要使用的语法来指示Protobuf我们将创建不同的数据类型:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; int32 total_capcity = 3; int64 mobile = 4; float base_ticket_price = 5; bool drive_in = 6; enum PAYMENT_SYSTEM { CASH = 0; CREDIT_CARD = 1; DEBIT_CARD = 2; APP = 3; } PAYMENT_SYSTEM payment = 7; repeated string snacks = 8; map<string, int32> movieTicketPrice = 9; TheaterOwner owner = 10; } message TheaterOwner{ string name = 1; string address = 2; }
使用Java进行序列化
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
首先,我们将创建一个写入器来写入剧院信息:
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.TheaterOwner; import com.tutorialspoint.theater.TheaterOuterClass.Theater.PAYMENT_SYSTEM; public class TheaterWriter { public static void main(String[] args) throws IOException { TheaterOwner owner = TheaterOwner.newBuilder() .setName("Anthony Gonsalves") .setAddress("513, St Paul Street, West Coast, California") .build(); List<String> snacks = new ArrayList<>(); snacks.add("Popcorn"); snacks.add("Coke"); snacks.add("Chips"); snacks.add("Soda"); Map<String, Integer> ticketPrice = new HashMap<>(); ticketPrice.put("Avengers Endgame", 700); ticketPrice.put("Captain America", 200); ticketPrice.put("Wonder Woman 1984", 400); Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .setDriveIn(true) .setTotalCapcity(320) .setMobile(98234567189L) .setBaseTicketPrice(22.45f) .setPayment(PAYMENT_SYSTEM.CREDIT_CARD) .putAllMovieTicketPrice(ticketPrice) .addAllSnacks(snacks) .setOwner(owner) .build(); String filename = "E:/theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: E:/theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" address: "212, Maple Street, LA, California" total_capcity: 320 mobile: 98234567189 base_ticket_price: 22.45 drive_in: true payment: CREDIT_CARD snacks: "Popcorn" snacks: "Coke" snacks: "Chips" snacks: "Soda" movieTicketPrice { key: "Avengers Endgame" value: 700 } movieTicketPrice { key: "Captain America" value: 200 } movieTicketPrice { key: "Wonder Woman 1984" value: 400 } owner { name: "Anthony Gonsalves" address: "513, St Paul Street, West Coast, California" }
使用Python反序列化序列化对象
从 proto 文件生成 Python 类
让我们为Theater类生成Python代码:
protoc --python_out=. theater.proto
执行此命令后,您将在当前目录中注意到一个自动生成的类theater_pb2.py。此类将帮助我们反序列化Theater对象。
使用生成的 Python 类
现在,让我们编写数据的读取器,它将使用Java读取包含序列化对象的文件:
theaterReader.py
import theater_pb2 filename = "E:/theater_protobuf_output"; print("Reading from file: " + filename) theater = theater_pb2.Theater() f = open(filename, "rb") theater.ParseFromString(f.read()) f.close() print("Read theater from disk: \n" + str(theater))
然后,让我们执行读取器。
python theaterReader.py Reading from file: E:/greeting_protobuf_output Read theater from disk: name: "Silver Screener" address: "212, Maple Street, LA, California" total_capcity: 320 mobile: 98234567189 base_ticket_price: 22.45 drive_in: true payment: CREDIT_CARD snacks: "Popcorn" snacks: "Coke" snacks: "Chips" snacks: "Soda" movieTicketPrice { key: "Wonder Woman 1984" value: 400 } movieTicketPrice { key: "Captain America" value: 200 } movieTicketPrice { key: "Avengers Endgame" value: 700 } owner { name: "Anthony Gonsalves" address: "513, St Paul Street, West Coast, California" }
因此,正如我们所看到的,Java客户端写入的所有值都被正确地反序列化并由我们的Python客户端读取,这有效地意味着Protobuf是语言独立的。
Protocol Buffers - 复合数据类型
概述
还有两种复合数据类型可能对复杂的用例有用。它们是“OneOf”和“Any”。在本节中,我们将了解如何使用Protobuf的这两种数据类型。
OneOf
我们将一些参数传递给此OneOf数据类型,Protobuf确保其中只有一个被设置。如果我们设置了其中一个并尝试设置另一个,则第一个属性将被重置。让我们通过一个示例来了解这一点。
继续我们从Protocol Buffers - String 章节中的theater示例,假设我们有一个用于获取可用员工数量的API。从该API返回的值随后设置为以下文件中的“count”标签。但是,如果该API出错,我们实际上无法“count”,而是附加错误日志。
理想情况下,我们将始终设置其中之一,即调用成功并获得计数,或者计数计算失败并获得错误消息。
以下是我们需要使用的语法来指示Protobuf我们将创建一个OneOf属性:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { oneof availableEmployees { int32 count = 4; string errorLog = 5; } }
使用Java进行序列化
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
首先,我们将创建一个写入器来写入剧院信息:
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setCount(3) .setErrorLog("No employee found") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: errorLog: "No employee found"
反序列化已序列化的对象
现在,让我们执行reader从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output errorLog: "No employee found"
因此,正如我们所看到的,我们只能看到错误日志被设置为后面的内容。
Any
下一个可用于复杂用例的数据类型是Any。我们可以将任何类型/消息/类传递给此数据类型,Protobuf不会报错。让我们通过一个示例来了解这一点。
继续剧院示例,假设我们想要跟踪剧院内部的人员。其中一些可能是员工,另一些可能是观众。但最终它们都是人,因此我们将把它们放在一个包含这两种类型的列表中。
现在我们的class/message包含一个Any属性'peopleInside'列表以及Viewer和Employee类,即剧院内人员的信息。让我们看看它是如何工作的。
以下是我们需要使用的语法来指示Protobuf我们将创建一个Any属性:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; import "google/protobuf/any.proto"; message Theater { string name = 1; string address = 2; repeated google.protobuf.Any peopleInside = 3; } message Employee{ string name = 1; string address = 2; } message Viewer{ string name = 1; int32 age = 2; string sex = 3; }
使用Java进行序列化
要使用 Protobuf,我们现在必须使用protoc二进制文件从此“.proto”文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
这将在当前目录中的com > tutorialspoint > theater文件夹中创建一个 TheaterOuterClass.java 类。我们将在我们的应用程序中使用此类,类似于在Protocol Buffers - Basic App章节中所做的那样。
使用从 Proto 文件创建的 Java 类
首先,我们将创建一个写入器来写入剧院信息:
TheaterWriter.java
package com.tutorialspoint.theater; import java.util.List; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import com.google.protobuf.Any; import com.tutorialspoint.theater.TheaterOuterClass.Employee; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Viewer; public class TheaterWriter{ public static void main(String[] args) throws IOException { List<Any> people = new ArrayList<>(); people.add(Any.pack(Employee.newBuilder().setName("John").build())); people.add(Any.pack(Viewer.newBuilder().setName("Jane").setAge(30).build())); people.add(Any.pack(Employee.newBuilder().setName("Simon").build())); people.add(Any.pack(Viewer.newBuilder().setName("Janice").setAge(25).build())); Theater theater = Theater.newBuilder() .setName("SilverScreen") .addAllPeopleInside(people) .build(); String filename = "theater_protobuf_output_silver"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.IOException; import com.google.protobuf.Any; import com.tutorialspoint.theater.TheaterOuterClass.Employee; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; import com.tutorialspoint.theater.TheaterOuterClass.Viewer; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output_silver"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println("Name:" + theater.getName() + "\n"); for (Any anyPeople : theater.getPeopleInsideList()) { if(anyPeople.is(Employee.class)) { Employee employee = anyPeople.unpack(Employee.class); System.out.println("Employee:" + employee + "\n"); } if(anyPeople.is(Viewer.class)) { Viewer viewer = anyPeople.unpack(Viewer.class); System.out.println("Viewer:" + viewer + "\n"); } } } } }
编译项目
现在我们已经设置了读取器和写入器,让我们编译项目。
mvn clean install
序列化 Java 对象
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "SilverScreen" peopleInside { type_url: "type.googleapis.com/theater.Employee" value: "\n\004John" } peopleInside { type_url: "type.googleapis.com/theater.Viewer" value: "\n\004Jane\020\036" } peopleInside { type_url: "type.googleapis.com/theater.Employee" value: "\n\005Simon" } peopleInside { type_url: "type.googleapis.com/theater.Viewer" value: "\n\006Janice\020\031" }
注意 - 有两点需要注意:
对于Any,Protobuf将任何标签内的内容打包/序列化为字节,然后将其存储为'value'。基本上,这允许我们使用此'Any'标签发送任何消息类型。
我们还看到了"type.googleapis.com/theater.Viewer"和"type.googleapis.com/theater.Employee"。Protobuf使用它来保存对象类型以及数据,因为Any数据类型中的数据类型可能会有所不同。
反序列化已序列化的对象
现在让我们执行reader以从同一文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output_silver Name:SilverScreen Employee:name: "John" Viewer:name: "Jane" age: 30 Employee:name: "Simon" Viewer:name: "Janice" age: 25
因此,正如我们所看到的,即使我们的reader代码位于同一个数组中,也能够成功地区分Employee和Viewer。
Protocol Buffers - 命令行使用
概述
Protobuf序列化数据并将其存储在二进制格式中。如果我们只是处理字符串,这可能不是问题,因为最终Protobuf使用UTF-8。因此,如果使用支持UTF8的读取器,它存储的任何文本都将是人类可读的。但是,int32、Boolean、list、maps等使用特定的技术进行编码以减少空间消耗。
这就是为什么有时通过简单的命令行实用程序编码/解码消息对于测试目的很有用。让我们看看它是如何工作的:
假设我们使用以下简单的"greeting_cli.proto":
syntax = "proto3"; package tutorial; option java_package = "com.tutorialspoint.greeting"; message Greet { string greeting = 1; string username = 2; int32 age = 3; }
并且我们在cli_greeting_message中创建一条消息:
greeting: "Yo" username : "John" age : 50
现在,让我们使用Protobuf CLI工具对该消息进行编码:
cat .\cli_greeting_msg.proto | protoc --encode=tutorial.Greet .\greeting_cli.proto > encoded_greeting
如果我们查看此文件内部的内容或cat此文件:
cat .\encoded_greeting ☻Yo↕♦John↑2
除了"Yo"和"John"之外,您还会注意到一些奇怪的字符。这是因为这些编码可能不是有效的Unicode/UTF-8编码。UTF-8通常是在大多数地方使用的。在Protobuf的情况下,它用于string,但ints、maps、Boolean、list有单独的格式。此外,此文件还包含数据的元数据。
这就是为什么我们需要解码器/反序列化器来读取这些数据。让我们使用它。
cat .\encoded_greeting | protoc --decode=tutorial.Greet .\greeting_cli.proto greeting: "Yo" username : "John" age : 50
因此,正如我们所看到的,我们能够获取序列化后并在文件中看起来很奇怪的数据。
Protocol Buffers - 更新定义规则
概述
假设您已经提出了将在生产环境中使用的proto文件的定义。将来肯定会有需要更改此定义的时候。在这种情况下,必须使我们进行的更改符合某些规则,以便更改向后兼容。让我们通过一些注意事项来了解这一点。
在writer中添加一个新字段,而reader保留旧版本的代码。
假设您决定添加一个新字段。理想情况下,要添加新字段,我们将不得不同时更新writer和reader。但是,在大规模部署中,这是不可能的。在某些情况下,writer已更新,但reader尚未更新新字段。这就是上述情况发生的地方。让我们看看它是如何工作的。
继续我们的theater示例,假设我们的proto文件中只有一个标签,即'name'。以下是我们需要使用的语法来指示Protobuf:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; }
要使用Protobuf,我们现在将不得不使用protoc二进制文件从这个".proto"文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
上述命令应该创建所需的文件,现在我们可以在Java代码中使用它。首先,我们将创建一个writer来写入theater信息:
TheaterWriter.java
package com.tutorialspoint.theater; package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取剧院信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.google.protobuf.ProtocolStringList; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); System.out.println("Unknwon fields: " + theater.getUnknownFields()); } } }
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener"
现在让我们执行reader以从同一文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener"
未知字段
我们根据Protobuf定义简单地写入了一个字符串,并且reader能够读取该字符串。我们还看到读取器没有意识到任何未知字段。
但是现在,假设我们想在Protobuf定义中添加一个新的字符串'address'。现在,它将如下所示:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; }
我们还将更新我们的writer并添加一个address字段:
Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .build();
在编译之前,将上一次编译生成的JAR重命名为protobuf-tutorial-old-1.0.jar。然后编译。
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" address: "212, Maple Street, LA, California"
现在让我们执行reader以从同一文件但从旧JAR中读取:
java -cp .\target\protobuf-tutorial-old-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output Reading from file theater_protobuf_output name: "Silver Screener" 2: "212, Maple Street, LA, California" Unknown fields: 2: "212, Maple Street, LA, California"
从输出的最后一行可以看出,旧的reader不知道新writer添加的address字段。它只是展示了“新writer - 旧reader”组合的功能。
删除字段
假设您决定删除现有字段。理想情况下,要使已删除字段立即生效,我们将不得不同时更新writer和reader。但是,在大规模部署中,这是不可能的。在某些情况下,writer已更新,但reader尚未更新。在这种情况下,reader仍将尝试读取已删除的字段。让我们看看它是如何工作的。
继续我们的theater示例,假设我们的proto文件中只有两个标签。以下是我们需要使用的语法来指示Protobuf:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; }
要使用Protobuf,我们现在将不得不使用protoc二进制文件从这个".proto"文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=java/src/main/java proto_files\theater.proto
上述命令应该创建所需的文件,现在我们可以在Java代码中使用它。首先,我们将创建一个writer来写入theater信息:
TheaterWriter
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.google.protobuf.ProtocolStringList; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); System.out.println("Unknwon fields: " + theater.getUnknownFields()); } } }
现在,编译后,让我们首先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" address: "212, Maple Street, LA, California"
现在让我们执行reader以从同一文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener" address: "212, Maple Street, LA, California"
所以,这里没有什么新东西,我们只是根据Protobuf定义简单地写了一个string,并且reader能够读取string。
但是现在,假设我们想从Protobuf定义中删除字符串'address'。因此,定义将如下所示:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; }
我们还将更新我们的writer,如下所示:
Theater theater = Theater.newBuilder() .setName("Silver Screener") .build();
在编译之前,将上一次编译生成的JAR重命名为protobuf-tutorial-old-1.0.jar。然后编译。
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener"
现在让我们执行reader以从同一文件但从旧JAR中读取:
java -cp .\target\protobuf-tutorial-old-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output Reading from file theater_protobuf_output name: "Silver Screener" address:
从输出的最后一行可以看出,旧的reader默认为“address”的值。它展示了“新writer - 旧reader”组合的功能。
避免重复使用字段的序列号
在某些情况下,我们可能会错误地更新字段的“序列号”。这可能存在问题,因为序列号对于Protobuf理解和反序列化数据至关重要。一些旧的reader可能依赖此序列号来反序列化数据。因此,建议您:
不要更改字段的序列号
不要重复使用已删除字段的序列号。
让我们通过交换字段标签来了解这一点。
继续使用剧院示例,假设我们只有两个标签在我们的proto文件中。以下是我们需要用来指示 Protobuf 的语法:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; }
要使用Protobuf,我们现在将不得不使用protoc二进制文件从这个".proto"文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=java/src/main/java proto_files\theater.proto
上述命令应该创建所需的文件,现在我们可以在Java代码中使用它。首先,我们将创建一个writer来写入theater信息:
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.google.protobuf.ProtocolStringList; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); System.out.println("Unknwon fields: " + theater.getUnknownFields()); } } }
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" address: "212, Maple Street, LA, California"
接下来,让我们执行读取器从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener" address: "212, Maple Street, LA, California"
这里,我们只是根据我们的 Protobuf 定义写了简单的字符串,并且读取器能够读取字符串。但是现在,让我们在 Protobuf 定义中交换序列号,使其如下所示:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 2; string address = 1; }
在编译之前,将 JAR 从之前的编译重命名为protobuf-tutorial-old-1.0.jar。然后编译。
现在,编译后,让我们首先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: address: "212, Maple Street, LA, California" name: "Silver Screener"
现在让我们执行reader以从同一文件但从旧JAR中读取:
java -cp .\target\protobuf-tutorial-old-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "212, Maple Street, LA, California" address: "Silver Screener"
从输出中可以看到,旧的读取器交换了地址和名称。这表明更新序列号以及结合“新的写入器-旧的读取器”不会按预期工作。
更重要的是,这里我们有两个字符串,这就是我们能够看到数据的原因。如果我们使用了不同的数据类型,例如int32、布尔值、映射等,Protobuf 会放弃并将此视为未知字段。
因此,务必不要更改字段的序列号或重复使用已删除字段的序列号。
更改字段类型
在某些情况下,我们需要更新属性/字段的类型。Protobuf 对此有一些兼容性规则。并非所有类型都可以转换为其他类型。需要注意的一些基本类型:
如果字节是 UTF-8,则字符串和字节是兼容的。这是因为 Protobuf 始终将字符串编码/解码为 UTF-8。
就值而言,枚举与int32和int64兼容,但是客户端可能无法按预期反序列化它。
int32、int64(无符号也)以及布尔值是兼容的,因此可以互换。类似于语言中的强制转换工作方式,可能会截断多余的字符。
但是更改类型时需要非常小心。让我们通过将int64转换为int32的错误示例来查看其操作。
继续使用剧院示例,假设我们只有两个标签在我们的proto文件中。以下是我们需要用来指示 Protobuf 的语法:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; int64 total_capacity = 2; }
要使用Protobuf,我们现在将不得不使用protoc二进制文件从这个".proto"文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=java/src/main/java proto_files\theater.proto
上述命令应该创建所需的文件,现在我们可以在Java代码中使用它。首先,我们将创建一个writer来写入theater信息:
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .setTotalCapacity(2300000000L) .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" total_capacity: 2300000000
假设我们对读取器使用不同版本的proto文件:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; int64 total_capacity = 2; }
接下来,我们将有一个reader来读取theater信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.google.protobuf.ProtocolStringList; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); System.out.println("Unknwon fields: " + theater.getUnknownFields()); } } }
现在让我们执行reader以从同一文件读取:
java -cp .\target\protobuf-tutorial-old-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener" address: "212, Maple Street, LA, California"
所以,这里没有什么新内容,我们只是根据 Protobuf 定义写了简单的字符串,并且读取器能够读取字符串。但是现在,让我们在 Protobuf 定义中交换序列号,使其如下所示:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 2; int32 total_capacity = 2; }
在编译之前,将 JAR 从之前的编译重命名为protobuf-tutorial-old-1.0.jar。然后编译。
现在,编译后,让我们先执行writer:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Reading from file theater_protobuf_output address: "Silver Screener" total_capcity: -1994967296
从输出中可以看到,旧的读取器将数字从int64转换,但是给定的int32没有足够的空间来容纳数据,它环绕到负数。此环绕是 Java 特定的,与 Protobuf 无关。
因此,我们需要从int32升级到int64,而不是反过来。如果我们仍然希望从int64转换为int32,我们需要确保值实际上可以保存在 31 位中(1 位用于符号位)。
Protocol Buffers - Kafka 集成
我们已经介绍了很多关于 Protocol Buffers 及其数据类型的示例。在本章中,让我们再举一个例子,看看 Protocol Buffers 如何与 Kafka 使用的 Schema Registry 集成。让我们首先了解什么是“schema registry”。
Schema Registry
Kafka 是最广泛使用的消息队列之一。它用于大规模应用发布者-订阅者模型。有关 Kafka 的更多信息,请访问此处:https://tutorialspoint.com/apache_kafka/index.htm
但是,在基本层面上,Kafka生产者应该发送消息,即 Kafka消费者可以读取的信息片段。而这种消息的发送和消费是我们需要 schema 的地方。它在大型组织中尤其需要,在大型组织中,多个团队读取/写入 Kafka 主题。Kafka 提供了一种将此 schema 存储在schema registry中的方法,这些 schema 随后在生产者/消费者创建/消费消息时创建/使用。
维护 schema 有两个主要好处:
兼容性:在大型组织中,必须确保生成消息的团队不会破坏使用这些消息的下游工具。Schema registry 确保更改向后兼容。
高效编码:在每条消息中发送字段名称及其类型效率低下且计算量大。使用 schema,我们不需要在每条消息中发送此信息。
schema registry 支持Avro、Google Protocol Buffers和JSON Schema 作为 schema 语言。这些语言中的 schema 可以存储在 schema registry 中。在本教程中,我们需要 Kafka 设置和 Schema registry 设置。
有关 Kafka 的安装,您可以查看以下链接:
安装 Kafka 后,您可以通过更新/etc/schema-registry/schema-registry.properties文件来设置 Schema Registry。
# where should schema registry listen on listeners=http://0.0.0.0:8081 # Schema registry uses Kafka beneath it, so we need to tell where are the Kafka brokers available kafkastore.bootstrap.servers=PLAINTEXT://hostname:9092,SSL://hostname2:9092 Once done, you can then run: sudo systemctl start confluent-schema-registry
设置完成后,让我们开始将 Google Protocol Buffers 与 Schema Registry 一起使用。
使用 Protocol Buffers Schema 的 Kafka 生产者
让我们继续使用我们的剧院示例。我们将使用以下 Protocol Buffers schema:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; int32 total_capcity = 3; int64 mobile = 4; float base_ticket_price = 5; bool drive_in = 6; enum PAYMENT_SYSTEM{ CASH = 0; CREDIT_CARD = 1; DEBIT_CARD = 2; APP = 3; } PAYMENT_SYSTEM payment = 7; repeated string snacks = 8; map<string, int32> movieTicketPrice = 9; }
现在,让我们创建一个简单的 Kafka写入器,它会将以这种格式编码的消息写入 Kafka 主题。但要做到这一点,首先我们需要向我们的 Maven POM 添加一些依赖项:
Kafka 客户端用于 Kafka 生产者和消费者
Kafka Protocol Buffers 序列化程序用于序列化和反序列化消息
Slf4j simple 确保我们从 Kafka 获取日志
<dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>2.5.0</version> </dependency> <!-- https://mvnrepository.com/artifact/io.confluent/kafka-protobuf-serializer --> <dependency> <groupId>io.confluent</groupId> <artifactId>kafka-protobuf-serializer</artifactId> <version>5.5.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.30</version> </dependency>
完成此操作后,让我们现在创建一个 Kafka生产者。此生产者将创建并发送一条消息,其中包含剧院对象。
KafkaProtbufProducer.java
package com.tutorialspoint.kafka; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.PAYMENT_SYSTEM; public class KafkaProtbufProducer { public static void main(String[] args) throws Exception{ String topicName = "testy1"; Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); props.put("clientid", "foo"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "io.confluent.kafka.serializers.protobuf.KafkaProtocol BuffersSerializer"); props.put("schema.registry.url", "https://127.0.0.1:8081"); props.put("auto.register.schemas", "true"); Producer<String, Theater> producer = new KafkaProducer<>(props); producer.send(new ProducerRecord<String, Theater>(topicName, "SilverScreen", getTheater())).get(); System.out.println("Sent to Kafka: \n" + getTheater()); producer.flush(); producer.close(); } public static Theater getTheater() { List<String> snacks = new ArrayList<>(); snacks.add("Popcorn"); snacks.add("Coke"); snacks.add("Chips"); snacks.add("Soda"); Map<String, Integer> ticketPrice = new HashMap<>(); ticketPrice.put("Avengers Endgame", 700); ticketPrice.put("Captain America", 200); ticketPrice.put("Wonder Woman 1984", 400); Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .setDriveIn(true) .setTotalCapacity(320) .setMobile(98234567189L) .setBaseTicketPrice(22.45f) .setPayment(PAYMENT_SYSTEM.CREDIT_CARD) .putAllMovieTicketPrice(ticketPrice) .addAllSnacks(snacks) .build(); return theater; } }
以下是我们需要了解的一些要点:
我们需要将 Schema Registry URL 传递给生产者。
我们还需要传递正确的 Protocol Buffers 序列化程序,该序列化程序特定于 Schema Registry。
当我们完成发送后,schema registry 会自动存储剧院对象的 schema。
最后,我们从自动生成的 Java 代码创建了一个剧院对象,这就是我们要发送的内容。
输出
现在,让我们编译并执行代码:
mvn clean install ; java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.kafka.KafkaProtbufProducer
我们将看到以下输出:
[main] INFO org.apache.kafka.common.utils.AppInfoParser - Kafka version: 2.5.0 [main] INFO org.apache.kafka.common.utils.AppInfoParser - Kafka commitId: 66563e712b0b9f84 [main] INFO org.apache.kafka.common.utils.AppInfoParser - Kafka startTimeMs: 1621692205607 [kafka-producer-network-thread | producer-1] INFO org.apache.kafka.clients.Metadata - [Producer clientId=producer-1] Cluster ID: 7kwQVXjYSz--bE47MiXmjw
发送到 Kafka
name: "Silver Screener" address: "212, Maple Street, LA, California" total_capacity: 320 mobile: 98234567189 base_ticket_price: 22.45 drive_in: true payment: CREDIT_CARD snacks: "Popcorn" snacks: "Coke" snacks: "Chips" snacks: "Soda" movieTicketPrice { key: "Avengers Endgame" value: 700 } movieTicketPrice { key: "Captain America" value: 200 } movieTicketPrice { key: "Wonder Woman 1984" value: 400 } [main] INFO org.apache.kafka.clients.producer.KafkaProducer - [Producer clientId=producer-1] Closing the Kafka producer with timeoutMillis = 9223372036854775807 ms.
这意味着我们的消息已发送。
现在,让我们确认 schema 已存储在 Schema Registry 中。
curl -X GET https://127.0.0.1:8081/subjects | jq
显示的输出是“topicName” + “key/value”
[ "testy1-value" ]
我们还可以看到 registry 存储的 schema:
curl -X GET https://127.0.0.1:8081/schemas/ids/1 | jq { "schemaType": "PROTOBUF", "schema": "syntax = \"proto3\";\npackage theater;\n\noption java_package = \"com.tutorialspoint.theater\";\n\nmessage Theater { \n string name = 1;\n string address = 2;\n int64 total_capacity = 3;\n int64 mobile = 4;\n float base_ticket_price = 5;\n bool drive_in = 6;\n .theater.Theater.PAYMENT_SYSTEM payment = 7;\n repeated string snacks = 8;\n repeated .theater.Theater.MovieTicketPriceEntry movieTicketPrice = 9;\n\n message MovieTicketPriceEntry {\n option map_entry = true;\n \n string key = 1;\n int32 value = 2;\n }\n enum PAYMENT_SYSTEM { \n CASH = 0;\n CREDIT_CARD = 1;\n DEBIT_CARD = 2;\n APP = 3;\n }\n }\n" }
使用 Protocol Buffers Schema 的 Kafka 消费者
现在让我们创建一个 Kafka消费者。此消费者将使用包含剧院对象的消息。
KafkaProtbufConsumer.java
package com.tutorialspoint.kafka; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.PAYMENT_SYSTEM; public class KafkaProtbufConsumer { public static void main(String[] args) throws Exception{ String topicName = "testy1"; Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); props.put("clientid", "foo"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "io.confluent.kafka.serializers.protobuf.KafkaProtocol BuffersSerializer"); props.put("schema.registry.url", "https://127.0.0.1:8081"); props.put("auto.register.schemas", "true"); Producer<String, Theater> producer = new KafkaProducer<>(props); producer.send(new ProducerRecord<String, Theater>(topicName, "SilverScreen", getTheater())).get(); System.out.println("Sent to Kafka: \n" + getTheater()); producer.flush(); producer.close(); } public static Theater getTheater() { List<String> snacks = new ArrayList<>(); snacks.add("Popcorn"); snacks.add("Coke"); snacks.add("Chips"); snacks.add("Soda"); Map<String, Integer> ticketPrice = new HashMap<>(); ticketPrice.put("Avengers Endgame", 700); ticketPrice.put("Captain America", 200); ticketPrice.put("Wonder Woman 1984", 400); Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .setDriveIn(true) .setTotalCapacity(320) .setMobile(98234567189L) .setBaseTicketPrice(22.45f) .setPayment(PAYMENT_SYSTEM.CREDIT_CARD) .putAllMovieTicketPrice(ticketPrice) .addAllSnacks(snacks) .build(); return theater; } }
以下是我们需要了解的一些要点:
我们需要将 Schema Registry URL 传递给消费者。
我们还需要传递正确的 Protocol Buffers 反序列化程序,该反序列化程序特定于 Schema Registry。
当我们完成消费后,Schema Registry 会自动读取存储的剧院对象的 schema。
最后,我们从自动生成的 Java 代码创建了一个剧院对象,这就是我们要发送的内容。
输出
现在,让我们编译并执行代码:
mvn clean install ; java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.kafka.KafkaProtbufConsumer offset = 0, key = SilverScreen, value = May 22, 2021 7:50:15 PM com.google.protobuf.TextFormat$Printer$MapEntryAdapter compareTo May 22, 2021 7:50:15 PM com.google.protobuf.TextFormat$Printer$MapEntryAdapter compareTo name: "Silver Screener" address: "212, Maple Street, LA, California" total_capacity: 320 mobile: 98234567189 base_ticket_price: 22.45 drive_in: true payment: CREDIT_CARD snacks: "Popcorn" snacks: "Coke" snacks: "Chips" snacks: "Soda" movieTicketPrice { key: "Captain America" value: 200 } movieTicketPrice { key: "Wonder Woman 1984" value: 400 } movieTicketPrice { key: "Avengers Endgame" value: 700 }
因此,正如我们所看到的,写入 Kafka 的消息被消费者正确地使用了。此外,Registry 存储了可以通过 REST API 访问的 schema。
Protocol Buffers - 其他语言
我们一直在 Java 和 Python 中使用 Protocol Buffers。但它支持多种语言,包括 C++、C#、Kotlin、Dart、Go 等。基本内容大多保持不变,即编写proto schema,通过protoc二进制文件生成源代码,我们的代码可以使用该源代码。让我们在本节中为Go和Dart编写一个基本示例。
我们将使用以下proto文件:
greeting.proto
syntax = "proto3"; package tutorial; message Greet { string greeting = 1; string username = 2; }
在 Go Lang 中使用 Google Protocol Buffers
要使用上述 Protocol Buffers 文件,我们首先必须为Go语言中的Greet类生成代码。为此,我们需要执行以下操作:
安装Go Protocol Buffers 插件 (protoc-gen-go),这是我们一直在使用的protoc文件的先决条件:
go install google.golang.org/protobuf/cmd/protoc-gen-go
然后,使用提供的“.proto”文件运行protoc,我们将指示它在“go”目录下生成代码。
protoc --go_out=go proto_files/greeting.proto
执行上述命令后,您会注意到一个自动生成的类:“greeting.pb.go”。此类将帮助我们序列化和反序列化 Greet 对象。
现在,让我们创建数据的写入器,它将以用户名和问候语作为输入:
greeting_writer.go
import "fmt" import "io/ioutil" func main() { greet := Greeting{} greet.username = "John" greet.greeting = "Hello" out, err := proto.Marshal(greet) ioutil.WriteFile("greeting_go_out", out , 0644) fmt.Println("Saved greeting with following data to disk:") fmt.Println(p) }
现在让我们创建将读取文件的读取器:
greeting_reader.go
import "fmt" import "io/ioutil" func main() { in, err := ioutil.ReadFile("greeting_go_out") greet := &pb.Greet{} proto.Unmarshal(in, greet) fmt.Println("Reading from file greeting_protobuf_output:") fmt.Println(greet) }
输出
读取器只需从同一文件读取,将其反序列化,并打印有关问候语的数据。
现在我们已经设置了读取器和写入器,让我们编译项目。
接下来,让我们首先执行写入器:
go run greeting_writer.go Saved greeting with following data to disk: {greeting: Hello, username: John}
然后,让我们执行读取器:
go run greeting_reader.go Reading from file greeting_protobuf_output {greeting: Hello, username: John}
因此,正如我们所看到的,写入器序列化并保存到文件中的数据,这些数据被读取器正确地反序列化并相应地打印出来。
在 Dart 中使用 Google Protocol Buffers
要使用上述 Protocol Buffers 文件,我们首先必须安装并为 Dart 语言中的Greet类生成代码。为此,我们需要执行以下操作:
安装Dart Protocol Buffers 插件 (protoc-gen-go),这是我们一直在使用的protoc文件的先决条件。https://github.com/dart-lang/protobuf/tree/master/protoc_plugin#how-to-build-and-use
然后,使用提供的“.proto”文件运行protoc,我们将指示它在“dart”目录下生成代码。
protoc --go_out=dart proto_files/greeting.proto
执行上述命令后,您会注意到一个自动生成的类:“greeting.pb.dart”。此类将帮助我们序列化和反序列化Greet对象。
现在,让我们创建数据的写入器,它将以用户名和问候语作为输入:
greeting_writer.dart
import 'dart:io'; import 'dart/greeting.pb.dart'; main(List arguments) { Greeting greet = Greeting(); greet.greeting = "Hello"; greet.username = "John"; File file = File("greeting_go_out"); print("Saved greeting with following data to disk:") file.writeAsBytes(greet.writeToBuffer()); print(greet) }
接下来,让我们创建一个将读取文件的读取器:
greeting_reader.dart
import 'dart:io'; import 'dart/greeting.pb.dart'; main(List arguments) { File file = File("greeting_go_out"); print("Reading from file greeting_protobuf_output:") Greeting greet = Greeting.fromBuffer(file.readAsBytesSync()); print(greet) }
输出
读取器只是从同一个文件读取,反序列化它,并打印有关问候语的数据。
现在我们已经设置了读取器和写入器,让我们编译项目。
接下来,让我们首先执行写入器:
dart run greeting_writer.dart Saved greeting with following data to disk: greeting: Hello username: John
然后,让我们执行读取器。
dart run greeting_reader.dart Reading from file greeting_protobuf_output greeting: Hello username: John
所以,正如我们所看到的,由写入器序列化并保存到文件中的数据,这些数据被读取器正确地反序列化并相应地打印出来。