Protobuf -java基础教程(译文)

protobuf 基础教程

最近忽然对RPC序列化感兴趣,可是发现Protobuf的资料并很少,因而在官网找到了Java使用Protocol Buffer的入门指南,用蹩脚的英文翻译了下,以飨同道。原文地址html

示例开始:定义协议格式 Protocol Format

示例:一个简单的通信簿, .proto 文件见 addressbook.protojava

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_packagejava_outer_classnamejava_package 指定了生成的java类须要放在哪一个包下面,若是没有指定这个值,则会使用package指定的值。 java_outer_classname 选项定义了类名,包含这个.proto文件里面的全部类,若是没有明确指定这个值的话,将会把文件名经过驼峰大小写命名方式做为类名, 例如 my_proto.proto 则默认会生成 MyProto 类名。正则表达式

接下来,就有了消息(message)定义。编程

一个消息(message)是包含一系列类型字段的聚合。数组

不少标准的简单数据类型供字段可用,包含:maven

  • bool
  • int32
  • float
  • double
  • string

您还能够添加其余的结构类型为您的消息字段类型所用。前面的示例中,Person 消息包含了PhoneNumber消息,而AddressBook消息包含Person消息。 您还能够定义枚举类型enum,若是你想你的字段可能的值为一个事先预约好的列表中的值,就可使用enum类型。 这个电话簿示例中,phone number有三种类型: MOBILEHOMEWORK编程语言

=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弊大于利。他们更倾向于使用opyionalrepeated。无论怎样,这种观点并不广泛。

您还能够在Protocol Buffer 语言指南中了解到完整地教程。 不要尝试去寻找相似继承的工具,protocol buffer不支持这样作。

编译你的Protocol Buffers

如今您有了一个 .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 运行时安装

Protobuf 支持几种不一样的编程语言。针对于每一种语言,你能够参考源码中的各类语言的说明指导。

  • 如今运行编译器,须要指明源目录(应用源代码所在-若是您没有提供一个值的话默认使用当前目录)、目标目录(您想要代码生成的目的目录,一般相似于 $SRC_DIR)、还有 .proto 的路径。在这种状况下,您能够以下操做:

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

由于您想生成Java类,您看到 --java_out 选项,相似的选项也提供了其余编程语言。

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

Protocol Buffer API

让咱们来看看一些生成的代码,并查看一些由编译器为您建立的类与方法。若是您看一下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 字段由一些额外的方法:

  • getXXXCount 方法用于获取泪飙大小;
  • 增长了根据元素索引下标获取元素的get、set方法(public PhoneNumber getPhones(int index); 和 public Builder setPhones(int index, PhoneNumber value);)。
  • *add、addAll 方法追加新元素(列表)到列表中。

注意到全部这些访问方法都使用了驼峰命名方式,即便 .proto 文件使用了小写和下划线。这个转换是由protocol编译器自动完成的,因此生成的类符合Java风格标准规范。在 .proto 中您应该老是将字段名用小写和下划线来命名。 能够参考风格指南获取更多良好的 .proto 命名风格。 更多关于编译器为字段生成的特定的详细信息能够参考 Java生成代码参考指南

枚举和内部类

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

public static enum PhoneType {
  MOBILE(0, 0),
  HOME(1, 1),
  WORK(2, 2),
  ;
  ...
}
复制代码

内部类Person.PhoneNumber也生成了,正如您指望的同样,它是做为Person的一个内部类的。

Builders vs. Messages

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方法

每一个message和builder类也包含一系列其余的方法,可让您检查与操做message:

  • isInitialized() : 检查是否全部的required 字段都已经set过值了/
  • toString() : 返回一个可读性良好的message表示,一般对调试特别有用。
  • mergeFrom(Message other) : (只在builder有)将other合并到该message中
  • clear() : (只在builder有)清除全部的字段,时其恢复到最初的空状态

解析和序列化

最后,每一个 proto buffer 类都有一些用于读和写二进制的方法。

  • byte[] toByteArray() : 序列化message,返回一个byte数组。
  • static Person parseFrom(byte[] data) : 将给定的byte数组解析成一个message。
  • void writeTo(OutputStream output) : 序列化message,并将其写入一个输出流。
  • static Person parseFrom(InputStream input) : 读取一个输入流并从其解析处一个message。

这些只是为序列化和解析(反序列化)而提供的两组操做方法。更多完整列表能够参考 Message API帮助文档

Protocol Buffers 和 面向对象 Protocol buffer 类基本上是哑数据持有者(相似C中的struct);在对象模型中,它们不是一等公民。若是您想向生成类中添加更丰富的行为,最好的方式就是在一个特定于应用程序中的类中包装protocol buffer生成的类。若是您不能控制.proto文件的设计(例如,若是您正在重用来自另外一个项目的一个文件),那么包装协议缓冲区也是一个好主意。在这种状况下,您可使用包装器类来建立更适合您的应用程序的独特环境的接口:隐藏一些数据和方法,公开方便的函数,等等。这将破坏内部机制,并且不管如何都不是良好的面向对象实践。

写一个Message

如今尝试使用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();
  }
}
复制代码

读一个Message

固然,若是您不能从通信簿中就获取任何信息,那也就没什么用了。这个示例展现了读取前面一个示例建立的文件并输出所有信息:

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发布代码后,毋庸置疑,您会但愿改善protocol buffer的定义。若是您想您的新buffer能够向后兼容,而且旧的buffer能够向前兼容——您确定想要这个——这须要遵循一些规则。在新版本的protocol buffer中须要遵循:

  • 您不能改变已经存在的字段的tag的编号
  • 您不能添加或者删除任何required字段
  • 您能够删除optional或者repeated字段
  • 您能够添加新的optional或者repeated字段,但那时您必须使用新的tag编号(即该tag编号没有在这个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能够应用在更普遍的问题上,正如您所指望的那样。

相关文章
相关标签/搜索