Protocol buffers翻译

RPC:Remote Procedure Call,远程过程调用,很多RPC框架是夸语言的

  1. 定义一个接口说明文件:描述了对象(结构图体),对象成员,接口方法等一系列信息
  2. 通过RPC框架所提供的编码器,将接口说明文件编译成集体的语言文件
  3. 在客户端和服务端分别引入RPC编译器所生成的文件,即可像调用本地方法一样调用远程方法

1. Protocol buffers

Protocol buffers 是语言中立的,平台无关的,可扩展的用于序列化结构化数据

1.1. What are protocol buffers?

1
2
3
4
5
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
}

Protocol buffers是google提供的语言中立,平台中立,用于数据结构化,可扩展化的结构化数据–就像xml,但是它的提交更小,速度更快,更加简单。你只需要定义一次你的数据结构,然后使用特殊生成的源代码轻松地将结构化数据写入和读取各种数据流,并使用各种语言。

1.2. Pick your favorite language

1
2
3
4
5
6
7
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);

Protocol buffers当前支持生成的代码Java,Python,Objective-C和C++语言。使用我们新的proto3语言版本,您还可以使用Dart,Go,Ruby和C#,以及更多语言。

1.3 How do I start?

1
2
3
4
5
6
7
Person john;
fstream input(argv[1],
ios::in | ios::binary);
john.ParseFromIstream(&input);
id = john.id();
name = john.name();
email = john.email();

  1. 下载并安装protocol buffer 编译器
  2. 阅读指南
  3. 尝试使用您选择的语言的教程

2. Protocol Buffer Basics: Java

本教程提供了一个基本的Java程序员介绍如何使用Protocol buffers。它向您展示如何通过创建一个简单的示例应用程序,

  • 在.proto文件中定义消息格式。
  • 使用protocol buffer编译器
  • 使用Java protocol buffer API来编写和读取消息。

这不是在Java中使用protocol buffer的综合指南。有关更详细的参考信息,请参阅Protocol Buffer Language Guide,Java API Reference, Encoding Reference.

2.1 Why Use Protocol Buffers?

我们将要使用的示例是一个非常简单的“address book”应用程序,可以在文件中读取和写入人员的联系人详细信息。address book中的每个人都有姓名,ID,电子邮件地址和联系电话号码。

你如何序列化和检索这样的结构化数据?有几种方法可以解决这个问题:

  • 使用java的序列化:这是默认方法,因为它内置于语言中,但它有许多众所周知的问题(参见Effective Java,作者:Josh Bloch,第213页),并且如果您需要与使用C ++或Python编写的应用程序共享数据,也无法正常工作。
  • 您可以发明一种特殊的方法将数据项编码为单个字符串 - 例如将4个整数编码为“12:3:-23:67”,这是一种简单而灵活的方法,虽然它确实需要编写一次性编码和解析代码,并且解析会产生很小的运行时成本。这最适合编码非常简单的数据。
  • 将数据序列化为XML。这种方法非常有吸引力,因为XML是人类可读的,并且有许多语言的绑定库。如果您想与其他应用程序/项目共享数据,这可能是一个不错的选择。然而,XML是众所周知的空间密集型,并且编码/解码它会对应用程序造成巨大的性能损失。此外,导航XML DOM树比通常在类中导航简单字段要复杂得多。

Protocol Buffers是灵活,高效,自动化的解决方案,可以解决这个问题。使用Protocol Buffers,您可以编写要存储的数据结构的.proto描述。根据这个.proto文件,Protocol Buffers编译器创建一个class,该类使用有效的二进制格式实现Protocol Buffers数据的自动编码和解析。生成的类为构成Protocol Buffers针对字段提供getter和setter,并负责Protocol Buffers作为一个单元读取和写入的细节。重要的是,Protocol Buffers格式支持随着时间的推移扩展格式的想法,使得代码仍然可以读取用旧格式编码的数据。

2.2 Where to Find the Example Code

示例代码包含在源代码包中的“examples”目录下。Download it here.

2.3 Defining Your Protocol Format

要创建address book应用程序,您需要从.proto文件开始。.proto文件中的定义很简单:为要序列化的每个数据结构添加消息,然后为消息中的每个字段指定名称和类型。这是定义结构消息的.proto文件addressbook.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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文件以package声明开头,这有助于防止不同项目之间的命名冲突。在Java中,包名称用作Java包,除非您已经明确指定了java_package,就像我们在这里一样。即使你确实提供了java_package,你仍然应该定义一个普通的package,以避免在Protocol Buffers名称空间和非Java语言中发生名称冲突。

在声明package之后,您可以看到两个特定于Java的选项:java_packagejava_outer_classname.java_package指定生成的classes应该位于什么Java包中存在。如果没有明确指定它,它只是匹配package声明给出的包名中,但这些名称通常不是合适的Java包名(因为它们通常不以域名开头)。java_outer_classname选项定义应包含此文件中所有类的类名。如果未明确提供java_outer_classname,则将通过将文件名转换为驼峰形式来生成它。例如,默认情况下,“my_proto.proto”将使用“MyProto”作为外部类名。

接下来,您有消息定义。 消息只是包含一组类型字段的聚合。许多标准的简单数据类型都可用作字段类型,包括bool,int32,float,double和string。您还可以使用其他消息类型作为字段类型向消息中添加更多结构 - 在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以定义嵌套在其他消息中的消息类型 -​​ 如您所见,PhoneNumber类型在Person中定义。如果您希望其中一个字段具有预定义的值列表之一,您还可以定义枚举类型 - 此处您要指定电话号码可以是MOBILE,HOME或WORK之一。

每个元素上的“= 1”,“= 2”标记标识该字段在二进制编码中使用的唯一“标记”。标签号1-15需要少于一个字节来编码而不是更高的数字,因此作为优化,您可以决定将这些标签用于常用或重复的元素,将标签16和更高版本留给不太常用的可选元素。重复字段中的每个元素都需要重新编码标记号,因此重复字段特别适合此优化。

必须使用以下修饰符之一注释每个字段:

  • required:必须提供该字段的值,否则该消息将被视为“未初始化”。尝试构建未初始化的消息将抛出RuntimeException。解析未初始化的消息将抛出IOException。除此之外,必填字段的行为与可选字段完全相同。

  • optional:可以设置也可以不设置字段。如果未设置可选字段值,则使用默认值。对于简单类型,您可以指定自己的默认值,就像我们在示例中为电话号码类型所做的那样。否则,使用系统默认值:数字类型为0,字符串为空字符串,bools为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,其中没有设置其字段。调用访问器以获取尚未显式设置的可选(或必需)字段的值始终返回该字段的默认值。

  • repeated:该字段可以重复任意次数(包括0次)。重复值的顺序将保留在protocol buffer中。将重复字段视为动态大小的数组。

必需永远您应该非常小心地将字段标记为required。如果您希望在某个时刻停止写入或不发送必填字段,则将字段更改为optional字段会有问题 - 旧readers会认为没有此字段,可能会无意中拒绝或丢弃它们。您应该考虑为protocol buffer编写特定于应用程序的自定义验证例程。谷歌的一些工程师得出的结论是,使用repeated的弊大于利;他们更喜欢只使用optionalrepeated。但是,这种观点并不普遍。

您将在Protocol Buffer Language Guide中找到编写.proto文件的完整指南 - 包括所有可能的字段类型。不要去寻找类继承类似方式,但protocol buffer不支持继承。

2. 4. Compiling Your Protocol Buffers

现在你已经有一个.proto,你接下来需要做的下一件事是生成你需要读取和写入AddressBook(以及Person和PhoneNumber)消息所需的类。为此,您需要在.proto上运行Protocol Buffers编译器protoc:

  1. 如果尚未安装Protocol Buffers编译器,请下载该软件包并按照自述文件中的说明进行操作。
  2. 现在运行编译器,指定source代码目录(应用程序的源代码所在的位置 - 如果不提​​供值,则使用当前目录),目标目录(您希望生成的代码在哪里;通常与$SRC_DIR),以及.proto的路径。在这种情况下,你…:
1
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

因为您需要Java class,所以使用–java_out选项 - 为其他受支持的语言提供了类似的选项。

这将在指定的目标目录中生成com/example/tutorial/AddressBookProtos.java。

2.5 The Protocol Buffer API

让我们看看一些生成的代码,看看编译器为您创建了哪些类和方法。如果你查看AddressBookProtos.java,你会发现它定义了一个名为AddressBookProtos的类,在addressbook.proto中指定的每个消息都嵌套在AddressBookProtos类中。每个类都有自己的Builder类,可用于创建该类的实例。您可以在下面的“Builders vs. Messages”部分中找到有关构建器的更多信息。

消息和构建器都为消息的每个字段都有自动生成的访问器方法;消息只有getter,而构建器有getter和setter。以下是Person类的一些访问器(为简洁起见省略了实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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。最后,每个字段都有一个clear方法,将字段取消设置回其空状态。

重复字段有一些额外的方法 - 一个Count方法(它只是列表大小的简写),getter和setter通过索引获取或设置列表的特定元素,add方法将一个新元素附加到列表,以及一个addAll方法,它将整个容器中的元素添加到列表中。

有关protocol编译器为任何特定字段定义生成的确切成员的更多信息,请参阅Java generated code reference

2.6 Enums and Nested Classes

生成的代码包含一个嵌套在Person中的PhoneType枚举:

1
2
3
4
5
6
7
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}

正如您所期望的那样,生成嵌套类型Person.PhoneNumber,作为Person中的嵌套类。

2.7 Builders vs. Messages

Protocol Buffer编译器生成的消息类都是不可变的。一旦构造了消息对象,就像Java String那样不能修改它。要构造消息,必须首先构造构建器,将要设置的任何字段设置为所选值,然后调用构建器的build()方法。

您可能已经注意到构建器的每个修改消息的方法都会返回另一个构建器。返回的对象实际上是您调用该方法的同一个构建器。为方便起见,它会返回,以便您可以在一行代码中将多个setter串在一起。

这是一个如何创建Person实例的示例:

1
2
3
4
5
6
7
8
9
10
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.8. Standard Message Methods

每个消息和构建器类还包含许多其他方法,可用于检查或操作整个消息,包括:

  • isInitialized(): 检查是否已设置所有必填字段。
  • toString():返回人类可读消息,对调试特别有用
  • mergeFrom(Message other):(仅限构建器)将其他内容合并到此消息中,覆盖单个标量字段,合并复合字段以及连接重复字段。
  • clear():(仅限构建器)将所有字段清除回空状态。

这些方法实现了所有Java消息和构建器共享的Message和Message.Builder接口。有关更多信息,请参阅complete API documentation for Message

2.9 Parsing and Serialization

最后,每个protocol buffer类都有相应的方法使用protocol buffer二进制格式去编写和读取所选类型的消息的方法。这些包括:

  • byte[] toByteArray();: 序列化消息并返回包含其原始字节的字节数组。
  • static Person parseFrom(byte[] data);: 根据字节数组来解析消息。
  • void writeTo(OutputStream output);: 序列化消息并将其写入OutputStream
  • static Person parseFrom(InputStream input);: 从InputStream读取和解析消息。

这些只是解析和序列化提供的几个选项。再次,请参阅Message API reference以获取完整列表。

Protocol Buffers 和O-O(面向对象)设计Protocol Buffers 类基本上是哑数据持有者(如C中的struct);他们没有在对象模型中成为优秀的一等公民。如果要为生成的类添加更丰富的行为,最好的方法是将生成的Protocol Buffers 类包装在特定于应用程序的类中。如果您没有对.proto文件进行更好的设计(例如,如果您正在重用另一个项目中的一个消息),那么包装Protocol Buffers 也是一个好主意。在这种情况下,您可以使用包装器类来创建更适合应用程序的独特环境的接口:隐藏一些数据和方法,公开便利功能等。您永远不应该通过继承它们来向生成的类添加行为。这将打破内部机制,无论如何都不是良好的面向对象的实践。

简单理解就是Protocol Buffers 不支持继承

2.10 Writing A Message

这个demo是将一个对象通过Protocol Buffer写入到一个文件中

现在让我们尝试使用您的Protocol Buffer类。首先第一件事使用address book应用程序将个人详细信息写入您的address book文件。为此,您需要创建并填充Protocol Buffer类的实例属性,然后将它们写入输出流。

这是一个从键盘输入中读取到AddressBook的程序,根据用户输入向其添加一个新Person,并将新的AddressBook再次写回文件。直接调用或引用Protocol protocol编译器生成的代码的部分将突出显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.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.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.11 Reading A Message

将2.10 生成的文件反序列化到程序中

当然,如果您无法从中获取任何信息,那么address book就不会有多大用处!此示例读取上面示例创建的文件并打印其中的所有信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.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);
}
}

2.12 Extending a Protocol Buffer

在使用protocol buffer的代码之后,您无疑会想要“改进”protocol buffer的定义。
如果你希望你的新buffers向后兼容,并且你的旧buffers是向前兼容的 - 而且你几乎肯定想要这个 - 那么你需要遵循一些规则。
在新版本的protocol buffer中:

  • 您不得更改任何现有字段的标记号。
  • 您不得添加或删除任何required字段。
  • 您可以删除optional或repeated的字段。
  • 您可以添加新的optional或repeated字段,但必须使用新的标记号(即从未在此protocol buffer中使用的标记号,甚至不包括已删除的字段)。
    (这些规则有一些Exceptions,但它们很少使用)

如果您遵循这些规则,旧代码将很乐意阅读新消息并简单地忽略任何新字段。对于旧代码,已删除的可选字段将只具有其默认值,删除的重复字段将为空。新代码也将透明地读取旧消息。但是,请记住旧的消息中不会出现新的可选字段,因此您需要明确检查它们是否设置为has_,或者使用[default = value]在.proto文件中提供合理的默认值。标签号后面。如果未为可选元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值为空字符串。对于布尔值,默认值为false。对于数字类型,默认值为零。另请注意,如果添加了新的重复字段,则新代码将无法判断它是否为空(通过新代码)或从未设置(通过旧代码),因为它没有has_标志。

2.13. Advanced Usage

protocol buffer的用途不仅仅是简单的访问器和序列化。请务必浏览Java API reference,以了解您可以使用它们做些什么。

protocol消息类提供的一个关键特性是反射。您可以迭代消息的字段并在你写的代码中操纵它们的值,而无需针对任何特定的消息类型编写代码。使用反射的一种非常有用的方法是将protocol消息转换为与其他编码(例如XML或JSON)之间的转换。更高级的反射使用可能是找到两个相同类型的消息之间的差异,或者开发一种“protocol消息的正则表达式”,您可以在其中编写与某些消息内容匹配的表达式。如果您运用自己的想象力,可以将protocol buffer应用于比您最初预期更广泛的问题!

MessageMessage.Builder接口提供一部分反射信息

3. Developer Guide

欢迎开发者来到Protocol buffers文档 – 一种与语言无关,平台无关,可扩展的序列化结构化数据的方法,用于通信协议,数据存储等