一 、为什么要使用Protocol Buffer
假设我们要创建一款非常简单的“地址簿”应用程序,它可以从文件中读取和写入人们的联系方式。地址簿中的每个人都有一个姓名、ID、电子邮件地址和联系电话号码。
如何序列化和检索这样的结构化数据?
- 使用Java序列化。有许多众所周知的问题
- 自己发明一种特别的方法将数据项编码为单个字符串。例如将4个int编码为“12:3:-23:67”。这是一种简单而灵活的方法,尽管它确实需要编写一次性编码和解析代码,而且解析会带来较小的运行时成本。这对于编码非常简单的数据最有效
- 将数据序列化为XML:这种方法非常有吸引力,因为XML(某种程度上)是可读的,而且有许多语言的绑定库。如果您想与其他应用程序/项目共享数据,这可能是一个很好的选择。然而,XML是出了名的空间密集,对它进行编码/解码会给应用程序带来巨大的性能损失。而且,在XML DOM树中导航要比在类中导航简单字段复杂得多。
协议缓冲区正是解决这一问题的灵活、高效、自动化的解决方案。使用协议缓冲区,您可以为希望存储的数据结构编写一个.proto描述。在此基础上,协议缓冲区编译器创建一个类,以有效的二进制格式实现协议缓冲区数据的自动编码和解析。生成的类为组成协议缓冲区的字段提供了getter和setter,并负责将协议缓冲区作为一个单元进行读写的细节。重要的是,协议缓冲区格式支持扩展的思想
二、一个简易地地址簿程序
2.1 创建一个.proto文件
要创建地址簿应用程序,需要从创建.proto文件开始。proto文件中的定义很简单,为想要序列化的每个数据结构添加一条消息,然后为消息中的每个字段指定名称和类型。下面是定义你的信息的.proto文件,addressbook.proto。
syntax = "proto2";
package tutorial;
option java_multiple_files = true;
option java_package = "com.example.tutorial.protos";
option java_outer_classname = "AddressBookProtos";
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
proto文件以包声明开始,这有助于防止不同项目之间的命名冲突。在Java中,包名被用作Java包,除非您显式地指定了java_package,就像我们在这里所做的那样。即使您提供了一个java_package,您仍然应该定义一个普通的包,以避免在Protocol Buffers名称空间中以及在非java语言中发生名称冲突。
在包声明之后,您可以看到三个特定于java的选项:java_multiple_files、java_package和java_outer_classname。
- java_package: 指定生成的类应该在哪个Java包中存在。如果不显式指定,它只匹配包声明给出的包名,但这些名称通常不适合Java包名(因为它们通常不以域名开头)。
- java_outer_classname:选项定义了将代表此文件的包装器类的类名。如果您没有显式地给出java_outer_classname,那么它将通过将文件名转换为大驼峰大小写来生成类名。
接下来,是消息定义。消息只是包含一组类型化字段的聚合。许多标准的简单数据类型可以作为字段类型使用,包括bool、int32、float、double和string。
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
您还可以通过使用其他消息类型作为字段类型来为消息添加进一步的结构—在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。你甚至可以定义嵌套在其他消息中的消息类型,如您所见,PhoneNumber类型定义在Person中。
message Person {
...
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
...
如果您希望某个字段具有预定义的值列表之一,您还可以定义枚举类型。这里您希望指定一个电话号码可以是MOBILE、HOME或WORK中的一个。
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
每个元素上的" = 1"," = 2"标记标识字段在二进制编码中使用的唯一"标记"。标签号1-15比更高的数字需要少一个字节来编码,因此作为优化,您可以决定对常用或重复的元素使用这些标签,而对不常用的可选元素留下标签16和更高。重复字段中的每个元素都需要重新编码标签号,因此重复字段是这种优化的特别好的候选者。如这里的name id 与 email字段都给定了序号。
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
每个字段必须用以下修饰符之一进行注释:
- optional: 默认为optional,如果没有设置则默认为optional。
- repeated:该字段可以重复任意次(包括0次)。重复值的顺序将在协议缓冲区中保留。可以将重复字段看作动态大小的数组。
- required:必须为字段提供一个值,否则该消息将被认为是“未初始化的”。试图构建未初始化的消息将抛出RuntimeException。解析未初始化的消息将抛出IOException。除此之外,必选字段的行为与可选字段完全相同。
2.2 编译你的protocol Buffer
现在已经有了.proto,接下来需要做的事情是生成需要读取和写入AddressBook(以及Person和PhoneNumber)消息的类。要做到这一点,你需要在.proto上运行协议缓冲编译器协议:
- 如果您还没有安装编译器,请下载该包并按照README中的说明操作。 这里如果你是使用的Springboo集成的Protobuf可以去搜一下,怎么在SpringBoot中集成Protobuf,就不用自己编译了。
- 现在运行编译器,指定源目录(应用程序源代码所在的目录——如果不提供值,则使用当前目录)、目标目录(希望生成的代码所在的目录;通常与$SRC_DIR相同),以及到.proto的路径。然后执行
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
这将在指定的目标目录下生成一个com/example/tutorial/protos/子目录,其中包含一些生成的.java文件。
2.3 Protocol Buffer API
让我们看看生成的java代码,看看编译器为您创建了哪些类和方法。如果你看一下com/example/tutorial/protos/,你可以看到它包含了.java文件,为你在addressbook.proto中指定的每个消息定义了一个类。 每个类都有自己的Builder类(构造器),用于创建该类的实例。
消息和构建器都为消息的每个字段自动生成访问方法;消息只有getter,而构建器同时有getter和setter。下面是Person类的一些访问器(为简洁起见省略了实现):
// required string name = 1;
public boolean hasName();
public String getName();
// required int32 id = 2;
public boolean hasId();
public int getId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
同样,Person.Builder中有相同的getter和setter:
// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();
// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();
// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
public Builder setPhones(int index, PhoneNumber value);
public Builder addPhones(PhoneNumber value);
public Builder addAllPhones(Iterable<PhoneNumber> value);
public Builder clearPhones();
如您所见,每个字段都有简单的javabeans风格的getter和setter。对于每个单独的字段也有getter,如果该字段被设置,则返回true。最后,每个字段都有一个明确的方法来取消将字段设置为空状态。
repeated 字段有一些额外的方法——Count方法(返回List的size),getter和setter方法获取或设置一个特定的元素列表的索引,一个方法添加一个新元素添加到列表,以及一个addAll方法将整个容器的元素添加到列表中。
2.4 生成一个消息
协议缓冲编译器生成的消息类都是不可变的。一旦构造了消息对象,就不能像Java字符串那样修改它。要构造消息,必须首先构造一个构建器,将想要设置的任何字段设置为所选值,然后调用构建器的build()方法。 您可能已经注意到,构建器的每个set方法都会返回另一个构建器。返回的对象实际上与调用方法的构建器相同。它的返回是为了方便,这样您就可以将多个setter串在一个上。如下面的例子:
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhones(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.HOME))
.build()
2.5 标准的消息方法
每个消息和构建器类还包含许多其他方法,这些方法允许您检查或操作整个消息,包括:
- isInitialized():检查是否设置了所有必需的字段。
- toString():返回一个字符串,对调试很有用。
- mergeFrom(Message other):(only builder)将other的内容合并到此消息中,覆盖单数标量字段,合并复合字段,并连接重复字段。
- Clear():(仅限builder)将所有字段清除回空状态。
2.6 解析和序列化
最后,每个协议缓冲区类都有使用协议缓冲区二进制格式写入和读取所选类型的消息的方法。这些包括:
- byte[] toByteArray(); 序列化消息并返回包含其原始字节的字节数组。
- static Person parseFrom(byte[] data);解析来自给定字节数组的消息。
- void writeTo(OutputStream output);:序列化消息并将其写入OutputStream
- static Person parseFrom(InputStream input);从InputStream读取并解析消息
2.7 发送一个Protocol Buffer类型的消息
现在让我们尝试使用您的协议缓冲区类。您希望地址簿应用程序能够做的第一件事是将个人详细信息写入地址簿文件。为此,您需要创建并填充协议缓冲区类的实例,然后将它们写入输出流。
下面是一个程序,它从文件中读取AddressBook,根据用户输入向其添加一个新的Person,并将新的AddressBook再次写入文件。突出显示了直接调用或引用协议编译器生成的代码的部分。
import com.example.tutorial.protos.AddressBook;
import com.example.tutorial.protos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;
class AddPerson {
// This function fills in a Person message based on user input.
static Person PromptForAddress(BufferedReader stdin,
PrintStream stdout) throws IOException {
Person.Builder person = Person.newBuilder();
stdout.print("Enter person ID: ");
person.setId(Integer.valueOf(stdin.readLine()));
stdout.print("Enter name: ");
person.setName(stdin.readLine());
stdout.print("Enter email address (blank for none): ");
String email = stdin.readLine();
if (email.length() > 0) {
person.setEmail(email);
}
while (true) {
stdout.print("Enter a phone number (or leave blank to finish): ");
String number = stdin.readLine();
if (number.length() == 0) {
break;
}
Person.PhoneNumber.Builder phoneNumber =
Person.PhoneNumber.newBuilder().setNumber(number);
stdout.print("Is this a mobile, home, or work phone? ");
String type = stdin.readLine();
if (type.equals("mobile")) {
phoneNumber.setType(Person.PhoneType.MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(Person.PhoneType.HOME);
} else if (type.equals("work")) {
phoneNumber.setType(Person.PhoneType.WORK);
} else {
stdout.println("Unknown phone type. Using default.");
}
person.addPhones(phoneNumber);
}
return person.build();
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE");
System.exit(-1);
}
AddressBook.Builder addressBook = AddressBook.newBuilder();
// Read the existing address book.
try {
addressBook.mergeFrom(new FileInputStream(args[0]));
} catch (FileNotFoundException e) {
System.out.println(args[0] + ": File not found. Creating a new file.");
}
// Add an address.
addressBook.addPerson(
PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
System.out));
// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream(args[0]);
addressBook.build().writeTo(output);
output.close();
}
}
2.8 读一个Protocol Buffer消息
import com.example.tutorial.protos.AddressBook;
import com.example.tutorial.protos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
static void Print(AddressBook addressBook) {
for (Person person: addressBook.getPeopleList()) {
System.out.println("Person ID: " + person.getId());
System.out.println(" Name: " + person.getName());
if (person.hasEmail()) {
System.out.println(" E-mail address: " + person.getEmail());
}
for (Person.PhoneNumber phoneNumber : person.getPhonesList()) {
switch (phoneNumber.getType()) {
case MOBILE:
System.out.print(" Mobile phone #: ");
break;
case HOME:
System.out.print(" Home phone #: ");
break;
case WORK:
System.out.print(" Work phone #: ");
break;
}
System.out.println(phoneNumber.getNumber());
}
}
}
// Main function: Reads the entire address book from a file and prints all
// the information inside.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
System.exit(-1);
}
// Read the existing address book.
AddressBook addressBook =
AddressBook.parseFrom(new FileInputStream(args[0]));
Print(addressBook);
}
}