最近忽然对RPC序列化感兴趣,可是发现Protobuf的资料并很少,因而在官网找到了Java使用Protocol Buffer的入门指南,用蹩脚的英文翻译了下,以飨同道。原文地址html
示例:一个简单的通信簿, .proto 文件见 addressbook.proto。java
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;
}
复制代码
.proto 文件开始声明包,以避免出现命名冲突。 在 JAVA 中,该包名能够做为java中的package,除非您又专门指定了 java_package,在addressbook.proto 中咱们就指定了package。git
即便您指定了一个java_package,也应该须要定义一个常规的package,是为了在Protocol Buffers命名空间中产生冲突。github
在包定义声明以后,还看到了两个java规范中的可选项: java_package、java_outer_classname。 java_package 指定了生成的java类须要放在哪一个包下面,若是没有指定这个值,则会使用package指定的值。 java_outer_classname 选项定义了类名,包含这个.proto文件里面的全部类,若是没有明确指定这个值的话,将会把文件名经过驼峰大小写命名方式做为类名, 例如 my_proto.proto 则默认会生成 MyProto 类名。正则表达式
接下来,就有了消息(message)定义。编程
一个消息(message)是包含一系列类型字段的聚合。数组
不少标准的简单数据类型供字段可用,包含:maven
您还能够添加其余的结构类型为您的消息字段类型所用。前面的示例中,Person 消息包含了PhoneNumber消息,而AddressBook消息包含Person消息。 您还能够定义枚举类型enum,若是你想你的字段可能的值为一个事先预约好的列表中的值,就可使用enum类型。 这个电话簿示例中,phone number有三种类型: MOBILE、HOME、WORK。编程语言
=1、=2 表示每一个元素上的标识字段在二进制编码中使用的惟一“标记”。 Tag编号 1-15 编号所需的字节比编号高的要少,因此您能够对经常使用的或常常重复使用的标记使用这些编号,剩下的16到更高的标记在可选元素中比较少用。 重复字段中的每一个元素都须要从新编码标记号tag,因此重复的字段是这种优化的最佳选择。ide
每一个字段必须标注为如下几种修饰符:
required :该字段必须提供,不然认为消息是"未初始化"的。尝试去构建一个未初始化的消息会抛出一个RuntimeException。解析一个未初始化的消息则会抛出一个IOException。除此以外,required字段的行为和optional字段彻底同样。
optional :表示可选的字段,该字段能够设值亦能够不设值。若是一个可选字段没有set值,则会使用其默认值进行初始化。对于简单类型,您能够明确指定本身默认的值,就像咱们在示例中处理的同样(phoneNumber的type字段)。不然,系统默认值:数值类型默认为0,字符类型默认为空串,boo类型默认为false。对于嵌入的消息,默认值老是"默认实例"或者消息的"原型",没有设置任何字段。
repeated :该字段能够重复任意次数使用。重复值的顺序将会保留在protocol buffer中。能够将重复字段看做动态数组。
Required Is Forever 您须要很是谨慎地将字段标记为required修饰。若是在某些时候,您但愿中止写或发送一个required字段,在将该字段变动为optional时可能会发生问题——旧的reader认为消息没有这个值则会拒绝或者丢弃这个消息。您应该为考虑为缓冲区编写特定于应用程序地校验例程。有一些Google工程师推测使用required弊大于利。他们更倾向于使用opyional和repeated。无论怎样,这种观点并不广泛。
您还能够在Protocol Buffer 语言指南中了解到完整地教程。 不要尝试去寻找相似继承的工具,protocol buffer不支持这样作。
如今您有了一个 .proto 文件了,下一步要作的事,就是生成一个您将要读、写的AddressBook类。所以,您须要运行potocol buffer编译器 protoc 处理 .proto:
Protocol 编译器是C++编写的。若是您使用C++,请根据C++安装指导安装protoc。 对于非C++用户,最简单的安装protocol编译器的方式是从release页下载预构建的二进制:github.com/protocolbuf…
下载好的 protoc-$VERSION-$PLATFORM.zip
。包含了二进制protoc,还有一系列与protobuf发布的标准的.proto文件。 若是您还想找旧版本,能够在https://repo1.maven.org/maven2/com/google/protobuf/protoc/找到。
这些预构建的二进制文件只会在发行版本中提供。
Protobuf 支持几种不一样的编程语言。针对于每一种语言,你能够参考源码中的各类语言的说明指导。
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
由于您想生成Java类,您看到 --java_out 选项,相似的选项也提供了其余编程语言。
这将会在您指定的目标目录生成com/example/tutorial/AddressBookProtos.java。
让咱们来看看一些生成的代码,并查看一些由编译器为您建立的类与方法。若是您看一下AddressBookProtos.java 类,能够看到定义了一个叫作AddressBookProtos的类,内嵌了您在addressbooproto文件中为每一个消息指定的类。每一个类都拥有自身的Builder类,您可使用它来建立一个对应的类实例。您能够在下面的 Builders vs. Messages能够看到更多细节。
messages 和 builders 拥有针对消息每一个字段的自动生成的访问方法。message仅仅拥有getters方法、builders拥有getters、setters方法。这里有一些关于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 内部类则拥有getters、setters:
// 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();
复制代码
正如您所看到的同样,每一个字段都有简单的 Java Bean 风格的getter、setter 方法。 每一个字段也有其has方法,若是设置了字段值,则会返回true。 最后,每一个字段还有clear方法,能够将该字段回归到其空白状态。
repeated 字段由一些额外的方法:
注意到全部这些访问方法都使用了驼峰命名方式,即便 .proto 文件使用了小写和下划线。这个转换是由protocol编译器自动完成的,因此生成的类符合Java风格标准规范。在 .proto 中您应该老是将字段名用小写和下划线来命名。 能够参考风格指南获取更多良好的 .proto 命名风格。 更多关于编译器为字段生成的特定的详细信息能够参考 Java生成代码参考指南
生成的代码包含了一个枚举 PhoneType ,嵌套在 Person 类中:
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}
复制代码
内部类Person.PhoneNumber也生成了,正如您指望的同样,它是做为Person的一个内部类的。
protocol buffer编译器生成的类都是不可变的。一旦一个message对象被建立后,就不能再被更改,相似于Java的String类。要构建一个message,您必须构建一个builder,并设置任何您想设置的字段的值,再调用builders's 的builder 方法。(使用过lombok的朋友,这很相似其@Builder注解的用法)
您可能还注意到每一个builder的方法返回的是另外一个builder。返回的对象其实和您调用方法的builder是同一个。这种处理方式很方便,您能够将多个setter在一行代码中串写。
这里有一个示例,建立一个Person的实例:
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())
.build();
复制代码
每一个message和builder类也包含一系列其余的方法,可让您检查与操做message:
最后,每一个 proto buffer 类都有一些用于读和写二进制的方法。
这些只是为序列化和解析(反序列化)而提供的两组操做方法。更多完整列表能够参考 Message API帮助文档。
Protocol Buffers 和 面向对象 Protocol buffer 类基本上是哑数据持有者(相似C中的struct);在对象模型中,它们不是一等公民。若是您想向生成类中添加更丰富的行为,最好的方式就是在一个特定于应用程序中的类中包装protocol buffer生成的类。若是您不能控制.proto文件的设计(例如,若是您正在重用来自另外一个项目的一个文件),那么包装协议缓冲区也是一个好主意。在这种状况下,您可使用包装器类来建立更适合您的应用程序的独特环境的接口:隐藏一些数据和方法,公开方便的函数,等等。这将破坏内部机制,并且不管如何都不是良好的面向对象实践。
如今尝试使用protocol buffer 类吧。第一件事,但愿通信簿能够把我的的信息详情写道通信簿文件中。为了作到这一点,您须要建立和填入protocol buffer的类实例,并将它们写入一个输出流中。
这里有一个程序,从一个文件中读取了一个AddressBook,根据用户的输入还添加了一个新的Person到通信簿中,并将新的AddressBook从新写入文件中。
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();
}
}
复制代码
固然,若是您不能从通信簿中就获取任何信息,那也就没什么用了。这个示例展现了读取前面一个示例建立的文件并输出所有信息:
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);
}
}
复制代码
当您使用protocol buffer发布代码后,毋庸置疑,您会但愿改善protocol buffer的定义。若是您想您的新buffer能够向后兼容,而且旧的buffer能够向前兼容——您确定想要这个——这须要遵循一些规则。在新版本的protocol buffer中须要遵循:
若是您遵循这些规则,旧代码能够友好地读取新的message,而且忽略新的字段。对于旧代码,那些删除地optional字段会使用它们默认的值,而repeated字段则为空。 新代码显然会读取旧的message。不论如何,切记新的optional字段在旧的message中是不会出现的,因此您须要检查它们是否设置了值,可使用 has_,或者在您的 .proto文件中在该字段的tab编号后面使用 [default = value] 为其提供一个default值。 若是optional字段没有指定default值,则会根据该类型自动为其赋值:string类型则赋值空串,对于布尔类型则赋值为false,对于数值类型则赋值为0. 请注意,假如您添加了一个repeated字段,您的新代码将无从知晓该字段是空的(新代码),仍是历来没有设置过值(旧代码),由于它没有 has_ 方法。
Protocol buffers 不只仅只是提供了简单的访问和序列化功能。能够访问 Java API帮助文档.
protocol message类体哦概念股了一个关键的特性——反射。您能够遍历message的全部字段和操做它们的值而不须要编写指定的message类型的代码。 反射的一个有用的使用方式是在各类编码之间转换协议消息,例如XML或者JSON。 一个关于反射的更高级的用法是从两个相同类型的消息message中找出不一样之处,或者开发出一种协议消息的“正则表达式”,是的您能够编写这种表达式去匹配某些message内容。 若是您充分发挥您的想象,使用protocol Buffers能够应用在更普遍的问题上,正如您所指望的那样。