您的位置:首页 > Web前端

Protocol Buffer介绍(Java实例)

2013-12-13 11:59 309 查看
本文译自:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN
ProtocolBuffer基础:Java
本指南提供了使用ProtocolBuffer工作的Java编程方法。全文通过一个简单的示例,向你介绍在Java中使用ProtocolBuffer的方法:
1.如何在.proto文件中定义消息格式;
2.如何使用ProtocolBuffer的creates编译器;
3.如何使用JavaProtocol Buffer的API来读写消息。
本文不是在Java中使用ProtocolBuffer的完整指南,更详细的信息请参照以下资料:
Protocol-buffers语言
JavaAPI参考
生成Java代码指南
编码参考

为什么使用ProtocolBuffer
我们使用了一个非常简单的“地址本”应用的例子,这个应用能够从一个文件中读写个人的联系方式信息。在地址本中每个人都有以下信息:姓名、ID、邮件地址、电话号码。
像这样的结构化数据应该如何系列化和恢复呢?以下几种方法能够解决这个问题:
1.使用Java系列化。因为它是内置在编程语言中的,所以是默认的方法,但是由于众所周知的主机问题,并且如果需要在使用不同编程语言(如C++或Python)编写应用程序之间共享数据,这种方式也不会很好的工作。
2.使用特殊的方式把数据项编码到一个单独的字符串中,如把4个整数编码成“12:3:-23:67”。尽管它需要编写一次性的编码和解码代码,但是这种方法简单而灵活,而且运行时解析成本很小。这种方法对于简单数据是最好的。
3.把数据系列化到XML。因为XML是可人类可读的,并且很多编程语言都有对应的功能类库,所以这种方法非常受欢迎。如果你想要跟其他应用程序/项目共享数据,那么这种方法是一个非常好的选择。但是,众所周知,XML是空间密集性的,并且编解码会严重影响应用程序的性能。此外,XML的DOM树导航也比一般的类中的字段导航要复杂的多。
ProtocolBuffer是完全解决这个问题的灵活、高效的自动化解决方案。使用ProtocolBuffer,要先编写一个.proto文件,用这个文件来描述你希望保存的数据结构。然后用ProtocolBuffer编译器创建一个类,这个类用高效的二进制的格式实现了ProtocolBuffer数据的自动编解码。生成的类提供了组成ProtocolBuffer字段的getter和setter方法,以及提供了负责读写一个ProtocolBuffer单位的方法。重要的是,ProtocolBuffer格式支持向后的兼容性,新的代码依然可以读取用旧格式编码的数据。

什么地方可以找到示例代码
示例代码的源代码包,可以直接从这儿下载。
定义协议格式
要创建你的地址本应用程序,需要从编写.proto文件开始。.proto文件的定义很简单:你要在每个想要系列化的数据结构前添加一个message关键字,然后指定消息中每个字段的名称和类型。以下就是你要定义的.proto文件,addressbook.proto:
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 phone = 4;
}

message AddressBook {
repeated Person person = 1;
}

就像你看到的,语法与C++或Java非常类似,接下来让我们检查一下文件的每个部分,并看一下它们都做了些什么。
.proto文件开始是包声明,它有助于防止不同项目间的命名冲突。除非你明确的指定了java_package
关键字,否则,该包名会被用于生成的Java类文件的包名。即使提供了java_package,依然应该定义一个普通的package,以避免跟ProtocolBuffer命名空间以及非Java语言中的命名冲突。
在包声明之后,有两个可选的Java规范:java_package和java_outer_classname。java_package指定要生成的Java类的包名。如果没有明确的指定这个关键字,它会简单的用package关键字的声明来作为包名,但是这些名称通常不适合做Java的包名(因为它们通常不是用域名开头的)。java_outer_classname可选项定义了这个文件中所包含的所有类的类名。如果没有明确的给出java_outer_classname定义,它会把文件名转换成驼峰样式的类名。如,“my_proto.proto”文件,默认的情况下会使用MyProto作为外部的类名。
接下来是消息定义,一个消息包含了一组类型字段。很多标准的简单数据类型都可以作为有效的字段类型,包括:bool、int32、float、double和string。还可以是其他消息类型作为字段类型---在上面的示例中,Person消息包含了PhoneNumber消息,而AddressBook消息又包含了Person消息。甚至还可以定嵌套在其他消息内的消息类型---如,PhoneNumber类型就被定义在Person内。如果想要字段有一个预定义的值列表,也可以定enum类型---上例中电话号码能够指定MOBILE、HOMEWORK三种类型之一。
每个字段后标记的“=1”、“=2”,是在二进制编码时使用的每个字段的唯一标识。在编码时,数字1~15要比大于它们的数字少一个字节,因此,作为一个优化选项,可以把1~15的数字用于常用的或重复性的元素。大于等于16的数字尽可能的用于那些不常用的可选元素。在重复字段中的每个元素都需要预定义一个标记数字,因此在重复性字段中使用这种优化是良好的选择。
每个字段必须用以下修饰符之一来进行标注:
1.required:用这个修饰符来标注的字段必须给该字段提供一个值,否则该消息会被认为未被初始化。尝试构建一个未被初始化的消息会抛出一个RuntimeException异常。解析未被初始化的消息时,会抛出一个IOException异常。其他方面,该类型字段的行为与可选类型字段完全一样;
2.optional:用这个修饰符来标注的字段可以设定值,也可以不设定值。如果可选字段的。值没有设定,那么就会使用一个默认的值。对于简单类型,能够像上例中指定电话号码的type那样,指定一个默认值。否则,系统使用的默认值如下:数字类型是0、字符串类型是空字符串、布尔值是false。对于内嵌的消息,默认值始终是“默认的实例“或”消息的“原型”,其中没有字段设置。调用没有明确设置值的字段的获取值的访问器的时候,会始终返回字段的默认值。
3.repeated:用这个修饰符来标注的字段可以被重复指定的数字的次数(包括0)。重复值的顺序会被保留在ProtocolBuffer中。重复字段跟动态数组很像。
对于标记为required的字段要始终小心。如果在某些时候,你希望终止写入或发送一个required类型的字段,那么在把该字段改变成可选字段时,就会发生问题---旧的版本会认为没有这个字段的消息是不完整的,并且会因此而拒绝或删除它们。因此应该考虑使用编写应用程序规范来定制Buffer的验证规则来代替。Google的一些工程师认为使用required,弊大于利,他们建议只使用optional和repeqted。但实际上是行不通的。
在ProtocolBuffer语言指南中,你会找到完成.proto文件编写指南---包括所有可能的字段类型。不要寻求类的继承性,ProtocolBuffer是不支持的
156b4

编译ProtocolBuffer
现在有一个.proto文件了,接下来要做的就是生成一个读写AddressBook(包括Person和PhoneNumber)消息的类。运行ProtocolBuffer编译器protoc来生成与.proto文件相关的类。

首先,需要下载的关于Protobuf的文件:

1.到http://code.google.com/p/protobuf/downloads/list ,选择其中的win版本下载,我选择的是protoc-2.4.1-win32.zip,解压得到protoc.exe文件。

2.运行编译器。在cmd命令下,进入protoc.exe的路径,
然后输入以下命令:

protoc.exe --proto_path=protocolfile --java_out=javafile protocolfile/*.proto

protocolfile表示当前目录下的一个文件夹,里面放着你要编译的.proto的文件。
javafile 表示该当前目录下的一个文件夹,里面放着编译后的java文件。
[/code]
ProtocolBuffer API

让我们来看一下生成的代码,并看一下编译器都为你创建了那些类和方法。如果你在看
AddressBookProtos.java
文件,你能够看到它定义了一个叫做
AddressBookProtos
的类,在
addressbook.proto
文件中指定的每个消息都嵌套在这个类中。每个类都有它们自己的
Builder
类,你能够使用这个类来创建对应的类的实例。在下文的
Buildersvs.
Messages
章节中,你会找到更多的有关
Builder
的信息。

Message
Builder
会给消息的每个字段都生成访问方法。
Message
仅有
get
方法,而
Builder
同时拥有
get
set
方法。以下是
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 phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
同时,
Person.Builder
get
set
方法:

// 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 phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<PhoneNumber> value);
public Builder clearPhone();
正如你看到的,每个字段都有简单的
JavaBean
样式的的
get
set
方法。对于每个有
get
方法的字段,如果该字段被设置,那么对应的
has
方法会返回
ture
。最后,每个字段还有一个
clear
方法,它会清除对应字段的设置,让它们回退到空的状态。

重复性字段会有一些额外的方法
---Count
方法(它会返回列表的尺寸)、通过索引指定获取或设定列表元素的
get
set
方法、往列表中添加新元素的
add
方法、以及把装有完整元素的容器放入列表中。

注意,这些访问方法都使用驼峰式命名,即使是使用小写字母和下划线的
.proto
文件名。这些变换都是由
Protocol Buffer
编译器自动完成的,因此生成的类也符合标准的
Java
样式协议。在你的
.proto
文件中,应该始终使用小写字母和下划线的字段命名,这样就会在所有的生成的编程语言中具有良好的命名实践。更多的良好的
.proto
样式,请看样式指南

对于那些特殊的字段定义,
Protocol
编译器生成的成员相关的更多更准确的信息,请看“
Java
生成代码参照
”。

枚举和嵌套类

在嵌套的
Person
类的生成代码中包含了
Java5
中的枚举类型
PhoneType

public static enum PhoneType {

  MOBILE(0, 0),
  HOME(1, 1),
  WORK(2, 2),
  ;
  ...
}
正如你所期待的,作为
Person
的嵌套类,生成了
Person.PhoneNumber
类型。

Builders vs. Messages

这些有
Protocol Buffer
编译器生成的消息类都是不可变的。一旦消息对象被构建了,它就不能被编辑了,就像
Java
String
。要构建一个消息对象,首先必须构建一个
Builder
,把你选择的值设置给对应的字段,然后调用
build()
方法。

你可能已经注意到,每个编辑消息的
builder
方法都会返回另外一个
Builder
对象,返回的
Builder
对象实际上与你调用的那个方法的
Builder
对象相同。这主要是为了能够在一行中编写
set
方法提供方便。

以下是创建
Person
实例的例子:

Person john =

  Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .addPhone(
      Person.PhoneNumber.newBuilder()
        .setNumber("555-4321")
        .setType(Person.PhoneType.HOME))
    .build();
标准的消息方法

每个消息和构建器类还包含了一些其他的方法,这些方法会帮助你检查或维护整个消息,这些方法包括:

1.isInitialized():
检查所有的
required
字段是否都被设置了。

2.toString():
返回一个可读的消息描述,对于调试特别有用。

3.mergeFrom(Message other):(
只有构建器有这个方法
)
,它会把
other
参数中的内容,用重写和串联
的方式合并到本消息中。

Clear():(
只有构建器才有这个方法
)
,清除所有字段的值,让它们返回到空的状态。

这些方法实现的
Message
Message.Builder
接口,会被所有的
Java
消息和构建器共享。更多信息,请看
Message
的完成
API
文档

解析和系列化

最后,每个
Protocol Buffer
类都有一些使用二进制来读写你选择的类型的方法,包括:

1.byte[] toByteArray()
:系列化消息,并返回包含原始字节的字节数组。

2.static Person parseFrom(byte[] data):
从给定的字节数组中解析消息。

3.void writeTo(OutputStream output):
系列化一个消息,并把该消息写入一个
OutputStream
对象中。

4.static Person parseFrom(InputStream input):
InputStream
对象中读取和解析一个消息。

对于解析和系列化,这些方法是成对使用的。完整的
API
列表请看“
Message API
参考

Protocol Buffer
和面向对象的设计:
Protocol Buffer
类是基本的数据持有者(有点类似
C++
中的结构体);在对象模型中,它们不是良好的一等类公民。如果你想要给生成的类添加丰富的行为,最好的做法是在特定的应用程序类中封装生成的
Protocol Buffer
类。如果在
.proto
文件的设计上没有控制,那么封装
Protocol Buffer
类也是个不错的主意(比方说,你要重用另一个项目中一个
Protocol Buffer
类)。在这种情况下,你能够包装类来构建一个适应你的应用程序环境的更好的接口:如隐藏一些数据和方法、暴露一些方便的功能,等等。你不应该通过继承给这些生成的类添加行为方法,这样做会终端内部机制,而且也不是良好的面向对象的实践。

编写一个消息

现在,让我们来尝试使用这些
Protocol Buffer
类。首先,你希望你的地址本应用程序能够把个人详细信息写入地址本文件。要完成这件事情,你需要创建并初始化
Protocol Buffer
类的实例,然后把它们写入一个输出流中。

以下是一段从文件中读取
AddressBook
的程序,它会基于用户的输入把一个新的
Person
对象添加到
AddressBook
对象中,并这个新的
AddressBook
对象在写回该文件中。

import
com.example.tutorial.AddressBookProtos.AddressBook
;

importcom.example.tutorial.AddressBookProtos.Person;
importjava.io.BufferedReader;
importjava.io.FileInputStream;
importjava.io.FileNotFoundException;
importjava.io.FileOutputStream;
importjava.io.InputStreamReader;
importjava.io.IOException;
importjava.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.addPhone(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();
  }
}
读取一个消息
当然,如果不能够从输出的文件中获取任何信息,那么这个地址本就毫无用处。下面的例子演示了如何从上例创建的文件中读取信息,并把所有的信息都打印出来:
importcom.example.tutorial.AddressBookProtos.AddressBook;
importcom.example.tutorial.AddressBookProtos.Person;
importjava.io.FileInputStream;
importjava.io.IOException;
importjava.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.getPersonList()) {
      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.getPhoneList()) {
        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的代码发布以后,不可避免的,你希望要改善Protocol Buffer的定义。如果想要新的Buffer类保持向后的兼容性,旧的Buffer保持向前的兼容性---几乎可以确定你是希望这样的。以下是你的新的Protocol Buffer版本要遵循的一些规则:
1.一定不要改变既存的标记数字;
2.不要添加或删除任何required类型的字段;
3.可以删除可选的或重复类型的字段;
4.可以添加新的可选的或重复类型的字段,必须使用新的标记数字(即,在该Protocol Buffer中没有被使用过的(即使是被删除的字段也不曾使用过)标记数字)。
(除了这些规则之外,还有一些其他的规则,但是它们很少使用)
如果你遵循了这些规则,旧的代码将会很好的读取新的消息,并且只是简单忽略了新的字段。对于旧代码,被删除的可选字段会简单的使用它们的默认值,被删除的重复性字段会被设置为空。新的代码也会透明的读取旧的消息。但是,要记住,新的可选字段不会出现在旧的消息里,因此你既可以明确的使用has_方法来检查它们是否被设置,也可以在.proto文件中在标记数字之后,用[default = value]来提供一个合理的默认值。对于没有指定默认值的可选元素,以下是特定类型使用的默认值:字符串类型,默认值是空字符串;布尔类型,默认值是false;数字类型,默认值是0。还要注意的是,如果你添加了一个新的重复性字段,因为没有给它has_标记,所以你的新代码不能被告知该字段是否是空的还是没有被设置。
高级用法
Protocol Buffer消息提供的一个关键特征就是反射。你能够迭代消息的字段,不用编写任何代码就可以维护任何指定的消息类型的值。使用反射的一个非常有用的方法就是把其他的编码格式转换成Protocol Buffer消息,如XML消息或JSON消息。反射的更高级的用途是查找两个相同类型消息直接的差异,或者是开发一种针对Protocol Buffer消息的正则表达式,在这个表达式中,你能够编写跟确定消息内容匹配的表达式。如果发挥你的想象力,Protocol Buffer的应用范围会比你的初始期望值还要高。
反射是作为Message和Message.Builder的接口部分来提供的。[/code](另外一个例子:http://www.cnblogs.com/stephen-liu74/archive/2013/01/06/2842972.html 很详细。点击打开链接
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: