什么是protocol buffer呢?
protobuf是Google开发的一种数据描述语言语言,能够将结构化的数据序列化,可用于数据存储,通信协议等方面,官方版本支持C++,Java,Python,社区版本支持更多语言。

比如说程序中生成了一个链表,但是程序退出重启后,还要重新生成链表,有时候,我们很需要上次程序中该链表中记录的数据。这些数据或许是经过很多大量运算生成的,每次都重新生成这些数据的话,需要消耗大量时间。这时候就可以考虑使用protobuf,将其序列化后保存在文件中,下次使用的时候,加载文件,反序列化后就可以直接使用了。

那我们为什么要用protocol buffer呢?

您可以创建一种特别的方法来将数据项编码为单个字符串,例如编码4 ints为“12:3:- 23:67”。这是一种简单而灵活的方法,尽管它确实需要编写一次性的编码和解析代码,而且解析带来了一个小的运行时成本。这对于编码非常简单的数据非常有效。
将数据序列化为XML。这种方法非常有吸引力,因为XML是人类可读的,而且有许多语言的绑定库。如果您想要与其他应用程序/项目共享数据,这将是一个很好的选择。然而,XML是出了名的空间密集型,编码/解码可以对应用程序施加巨大的性能惩罚。而且,导航XML DOM树要比在类中导航简单的字段要复杂得多。
protocol buffer 是解决这个问题的灵活、高效、自动化的解决方案。使用protocol buffer,您可以编写一个.proto文件来描述您希望存储的数据结构。这样,protocol buffer 编译器创建了一个类,该类使用高效的二进制格式实现对protocol buffer 数据的自动编码和解析。生成的类为组成protocol buffer 的字段提供了getter和setter,并负责将protocol buffer 的读取和写入的细节作为一个单元来处理。重要的是,protocol buffer 格式支持将格式扩展到时间的思想,这样代码仍然可以读取用旧格式编码的数据。

值得注意的是,protobuf是以二进制来存储数据的。相对于JSON和XML具有以下优点:

简洁

体积小:消息大小只需要XML的1/10 ~ 1/3

速度快:解析速度比XML快20 ~ 100倍

使用Protobuf的编译器,可以生成更容易在编程中使用的数据访问代码

更好的兼容性,Protobuf设计的一个原则就是要能够很好的支持向下或向上兼容

我们简单来看下用Java如何使用protocol buffer

  • 创建一个.proto的文件
  • 使用protocol buffer
  • 运用java的protocol buffer的api进行读写信息

举个例子
创建一个通讯录的应用,先创建一个.proto文件

package protobuf;

option java_package = "com.edianedi.myprotobuf.protobuf";
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;
}

让我们浏览一下文件的每个部分,看看它做了什么
.proto文件从一个包声明开始,这有助于防止不同项目之间的命名冲突。在Java中,包名被用作Java包,除非您明确指定了一个java_package,就像我们在这里所做的那样。即使您提供了一个java_package,您仍然应该定义一个正常包,以避免在protocol buffer名称空间和非java语言中发生名称冲突。

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

接下来,有您的消息定义。消息只是包含一组类型字段的聚合。许多标准的简单数据类型可用作字段类型,包括bool、int32、float、double和string。您还可以通过使用其他消息类型作为字段类型添加到消息的进一步结构——在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以定义在其他消息内嵌套的消息类型——正如您所看到的,PhoneNumber类型是在Person内部定义的。您还可以定义enum类型,如果您想要您的字段之一具有预定义的值列表——在这里您可以指定一个电话号码可以是移动、HOME或WORK。

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

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

需要:必须提供字段的值,否则该消息将被视为“未初始化”。尝试构建未初始化的消息将抛出RuntimeException。解析未初始化的消息将抛出IOException。除此之外,需要的字段与可选字段完全相同。
可选:字段可能设置或不设置。如果一个可选字段值没有设置,则使用默认值。对于简单类型,您可以指定您自己的默认值,就像我们在示例中为电话号码类型所做的那样。否则,将使用系统默认值:数字类型为零,字符串为空字符串,为bools为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,它没有设置字段。调用访问器以获取可选(或需要的)字段的值,该字段没有显式设置,总是返回该字段的默认值。
repeated:该字段可以重复多次(包括零)。重复值的顺序将保留在protocol buffer中。可以将重复字段看作是动态大小的数组。

编译你的protocol buffer

#运用以下命令来编译你的proto文件
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

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

消息和构建器都为消息的每个字段自动生成访问器方法;信息只有getter,而建设者有getter和setter。这里是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也有有相同的getter和setter:

// 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();

如您所见,每个字段都有简单的javabean风格的getter和setter。每个字段都有getter,如果该字段被设置,则返回true。最后,每个字段都有一个清除字段返回其空状态的清除方法。

Repeated字段有一些额外的方法——统计方法(也就是缩写列表的大小),getter和setter方法获取或设置一个特定的元素列表的索引,一个方法添加一个新元素添加到列表,以及一个addAll方法将整个容器的元素添加到列表中。

注意,这些访问器方法如何使用camel - case命名,尽管。使用lowercase-with-underscores原型文件。这个转换是由protocol buffer编译器自动完成的,因此生成的类符合标准Java风格的约定。您应该始终使用小写- with -下划线来表示字段名。原型文件;这确保了所有生成的语言都具有良好的命名实践。更多关于优秀的风格指南。典型的风格。

枚举和嵌套类
生成的代码是PhoneType的枚举类

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

嵌套类Person.PhoneNumber的生成,即嵌套类Person。

  • 生成器和信息类

被protocol buffer生成的信息类全都是不可变的。一旦创建了一个消息对象,它就不能被修改,就像java中的字符串一样。要构造消息,必须优先构造一个构造器,设置好之后,调用builder的build()方法。

你可能已经注意到,修改该消息的构造器的每个方法将返回另一个构造器。返回的对象实际上是调用方法的相同构造器。它是为方便而返回的,这样您就可以在一行代码中组合多个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();
  • 标准的消息方法

每一个消息和构造器类也包含了很多其他的方法供你检查或者操作整个信息,包括:

- isInitialized(): 检查是否已经设置了所有必需的字段
- toString(): 返回消息的可读表示,对调试特别有用。
- mergeFrom(Message other):(builder only) 在这个信息中合并other的内容。重写单数场(不好意思我没懂),合并复合字段,并将重复字段连接在一起
- clear():(builder only) 清除所有字段回到空状态。

以上方法实现了Message和Message.Builder的接口。所有java消息和构造器都可以共享以上方法。

  • 解析和序列化

最后,每个protocol buffer类都有使用protocol buffer的二进制格式编写和读取所选择类型的消息的方法。

- byte[] toByteArray(); 序列化消息返回字节数组
- static Person parseFrom(byte[] data);: 从给定的字节数组解析消息
- void writeTo(OutputStream output);: 序列化消息并将其写入OutputStream
- static Person parseFrom(InputStream input);:从InputStream读取解析消息

这些只是用于解析和序列化的两个选项。

  • 写一条消息

现在我们来用下protocol buffer类。你希望你的地址簿应用程序能够做的第一件事就是把个人信息写入你的地址簿文件。为此,您需要创建和填充protocol buffer类的实例,然后将它们写到输出流中。

这里有一个程序,它从一个文件中读取一个AddressBook,根据用户输入添加一个新用户,并将新的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);
  }
}