一、前言
Protocol Buffers
简称为 protobuf
,是 Google 公司开发的一种数据描述语言,类似于 XML 能够将结构化数据序列化,可用于数据存储、通信协议等方面。相比于现在流行的 XML 以及 JSON 格式存储数据,通过 Protocol Buffers
来定义的文件体积更小,解析速度更快。
假设你要为具有姓名和电子邮件的人建模,使用 XML 格式:
<person>
<name>John Doe</name>
<email>jdoe@example.com</email>
</person>
而对应的 protocol buffers 格式:
# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
name: "John Doe"
email: "jdoe@example.com"
}
1.1 编写 protobuf 文件
.proto
文件中的定义很简单:为要序列化的每个数据结构添加 message
,然后为 message
中的每个字段指定名称和类型。通俗地说,message
类似 Java 中的类,里面可以定义我们需要的属性。下面是官方的一个示例,定义一个 addressbook.proto
文件。地址簿中的每个人都有姓名,ID,邮件地址和联系电话。
syntax = "proto2";
package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
如上所示,语法类似于 C++ 或 Java。
.proto
文件以包声明为开头,这有助于防止不同项目之间的命名冲突。在 Java 中,包的名称被当作 Java 包,除非你明确指定了 java_package
,就像例子中的一样。即使你提供了 java_package
,你仍然应该定义一个普通的包名,以避免在 Protocol Buffers
命名空间和非 Java 语言中发生命名冲突。
在包声明之后,你可以看到两个对于 Java 的特定选项:java_package
和 java_outer_classname
。java_package
指定生成的类应该放在哪个 Java 包下。如果没有明确指定它,它只是匹配包声明给出的包名,但这些名称通常不是合适的 Java 包名(因为它们通常不以域名开头)。java_outer_classname
选项用来定义包含此文件中所有类的类名,即最外层类的类名。如果你没有提供 java_outer_classname
,则会通过骆驼命名法转换文件名来生成它。例如,my_proto.proto
将使用 MyProto
作为外部类名。
接下来,你需要定义 message (信息)
。message
只是包含一组类型字段的集合。许多标准的简单数据类型都可用作字段类型,包括 bool
,int32
,float
,double
和 string
。你还可以使用其他信息类型作为字段类型。在上面的示例中,Person
信息包含 PhoneNumber
信息,而 AddressBook
信息包含 Person
信息。你甚至可以定义嵌套在其他信息中的信息类型,例如,PhoneNumber
类型在 Person
中定义。如果你希望某个字段具有预定义的值列表中的一个值,你可以定义枚举类型。例如你要指定电话号码可以是 MOBILE
,HOME
或 WORK
之一。
每个元素上的 = 1
,= 2
标记标识该字段在二进制编码中使用的唯一标签。标签号 1-15
比更大的数字要少一个字节进行编码,因此作为优化,你可以将这些标签用于常用或重复的元素,将标签 16
或更高的标签留给不太常用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段特别适合此优化。
protocol buffers
不支持继承。
1.2 修饰符类型
必须使用以下修饰符之一修饰每个字段:
-
required
:必须提供该字段的值,否则信息将被视为 “未初始化”。构建未初始化的信息将抛出RuntimeException
。解析未初始化的信息将抛出IOException
。除此之外,required
字段的行为与optional
字段完全相同。 -
optional
:该字段可以设置也可以不设置。如果未设置optional
字段值,则会使用默认值。对于简单类型,你可以指定自己的默认值,就像我们在示例中为电话号码类型所做的那样。否则,将使用系统默认值:数字类型为 0,字符串为空字符串,bools
为false
。对于嵌入式信息,默认值始终是信息的默认实例
或原型
,其中没有设置任何字段。通过访问器获取尚未显式设置的optional
(或required
)字段的值始终返回该字段的默认值。 -
repeated
:该字段可以被重复任意次数(包括 0 次),但是它们的顺序会被保留。可以将重复字段视为动态大小的数组。
二、解析 protobuf
2.1 下载最新的 proctoc.exe
在这里可以下载最新发布的 protobuf
。这是 protoc-3.6.1-win32.zip 的下载地址,适用于 windows 系统,也有适用于 linux 系统的,大家可以按需下载。下载之后里面有 protoc.exe
,我们把它配置到环境变量,然后使用 cmd
运行即可。
2.2 生成 Java 文件
运行 protoc.exe
编译 .proto
文件生成 Java 文件,指定源目录(应用程序的源代码所在的位置,如果不提供值,则使用当前目录),目标目录(生成文件的输出路径,通常跟源目录相同),以及 .proto
文件的所在目录。执行命令如下:
protoc -I=$SRC_DIR(源目录) --java_out=$DST_DIR(目标目录) $SRC_DIR(.proto 文件所在目录)/addressbook.proto
如果使用上述官方示例的 .proto
文件,则会在指定的目标目录中生成 com/example/tutorial/AddressBookProtos.java
。
笔者为了适配自己的应用,修改了下 java_package
的值,如下:
syntax = "proto2";
package tutorial;
option java_package = "com.example.jerry.myapplication.tutorial";
option java_outer_classname = "AddressBookProtos";
然后执行编译命令:
protoc -I=D:/AndroidProjects/MyApplication
--java_out=D:/AndroidProjects/MyApplication/app/src/main/java/
D:/AndroidProjects/MyApplication/addressbook.proto
于是我们想要的 Java 文件就顺利生成了。
其实笔者在执行命令的时候还是遇到不少问题的:
-
如果命令中没有指定源目录,则
.proto
文件必须放在cmd
的当前目录下 -
如果指定了源目录,则
.proto
文件必须放在源目录下 -
还有很重要的一点,在 windows 系统下路径分隔符不能用
\
,而要改为/
。
有关生成的 Java 文件的内部代码解析,请看官方说明,这里就不一一阐述了。关键部分就是 Message
和 Message.Builder
的联系和区别。还有几个标准的 Message
方法:
-
isInitialized()
:检查是否设置了所有required
字段。 -
toString()
:返回一个具有可读性的message
表示,对调试特别有用。 -
mergeFrom(Message other)
:仅限Builder
使用,将其他message
的内容合并到此message
中,覆盖标量字段,合并复合字段以及连接重复字段。 -
clear()
:仅限Builder
使用,将所有字段清除回空状态。
2.3 添加 Maven 依赖
因为 protobuf
是 Google 提供的,所以使用 Android Studio 很容易引入最新的依赖库。我们在 Project Structure
中选择 Dependencies
选项卡,从网络添加依赖库,输入关键字 com.google.protobuf
就可以搜索到最新的 protocol buffer
依赖库。例如:
dependencies {
...
implementation 'com.google.protobuf:protobuf-java:3.6.1'
}
2.4 序列化和反序列化
每个 protocol buffer
类都有使用 protocol buffer
二进制格式编写和读取所选类型 message
的方法。比如上述地址簿的例子:
-
byte[] toByteArray()
:序列化message
对象并返回包含其原始字节的字节数组。 -
static Person parseFrom(byte[] data)
:解析来自给定字节数组的message
并返回 Java 类对象。 -
void writeTo(OutputStream output)
:序列化message
并将其写入OutputStream
。 -
static Person parseFrom(InputStream input)
:读取并解析来自InputStream
的message
。
2.5 写入信息 Writing A Message
笔者在这里就直接展示官网的示例代码了,还是地址簿的例子,主要就是掌握上述所说的序列化和反序列化的那几个方法。
package com.example.jerry.myapplication.tutorial;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
public class AddPerson {
// This function fills in a Person message based on user input.
static AddressBookProtos.Person PromptForAddress(BufferedReader stdin,
PrintStream stdout) throws IOException {
AddressBookProtos.Person.Builder person = AddressBookProtos.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;
}
AddressBookProtos.Person.PhoneNumber.Builder phoneNumber =
AddressBookProtos.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(AddressBookProtos.Person.PhoneType.MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(AddressBookProtos.Person.PhoneType.HOME);
} else if (type.equals("work")) {
phoneNumber.setType(AddressBookProtos.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);
}
AddressBookProtos.AddressBook.Builder addressBook = AddressBookProtos.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.addPeople(
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.6 读取信息 Reading A Message
同上。
package com.example.jerry.myapplication.tutorial;
import java.io.FileInputStream;
public class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
static void Print(AddressBookProtos.AddressBook addressBook) {
for (AddressBookProtos.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 (AddressBookProtos.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.
AddressBookProtos.AddressBook addressBook =
AddressBookProtos.AddressBook.parseFrom(new FileInputStream(args[0]));
Print(addressBook);
}
}
2.7 小结
当把依赖库和实体类全部导入到项目中后,就可以根据服务端提供的接口获取数据,然后开始解析。
解析其实不难,如果实体类叫做 Person
,那么只需一句代码:
Person person = Person.parseFrom(byte[] data);
网友评论