以下内容主要整理自官方文档

为什么使用 Protocol Buffers

通常序列化和解析结构化数据的几种方式?

  • 使用Java默认的序列化机制。这种方式缺点很明显:性能差、跨语言性差。
  • 将数据编码成自己定义的字符串格式。简单高效,但是仅适合比较简单的数据格式。
  • 使用XML序列化。比较普遍的做法,优点很明显,人类可读,扩展性强,自描述。但是相对来说XML结构比较冗余,解析起来比较复杂性能不高。

Protocol Buffers是一个更灵活、高效、自动化的解决方案。它通过一个.proto文件描述你想要的数据结构,它能够自动生成解析 这个数据结构的Java类,这个类提供高效的读写二进制格式数据的API。最重要的是Protocol Buffers的扩展性和兼容性很强,只要遵很少的规则 就可以保证向前和向后兼容。

.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;
}

Protocol Buffers 语法

.proto文件的语法跟Java的很相似,message相当于class,enum即枚举类型, 基本的数据类型有boolint32floatdouble, 和 string,类型前的修饰符有:

  • required 必需的字段
  • optional 可选的字段
  • repeated 重复的字段

NOTE 1: 由于历史原因,数值型的repeated字段后面最好加上[packed=true],这样能达到更好的编码效果。 repeated int32 samples = 4 [packed=true];

NOTE 2: Protocol Buffers不支持map,如果需要的话只能用两个repeated代替:keys和values。

字段后面的1,2,3…是它的字段编号(tag number),注意这个编号在后期协议扩展的时候不能改动。[default = HOME]即默认值。 为了避免命名冲突,每个.proto文件最好都定义一个package,package用法和Java的基本类似,也支持import

import "myproject/other_protos.proto";

扩展

PB语法虽然跟Java类似,但是它并没有继承机制,它有所谓的Extensions,这很不同于我们原来基于面向对象的JavaBeans式的协议设计。

Extensions就是我们定义message的时候保留一些field number 让第三方去扩展。

message Foo {
  required int32 a = 1;
  extensions 100 to 199;
}
message Bar {

    optional string name =1;
    optional Foo foo = 2;
} 

extend Foo {
    optional int32 bar = 102;
}

也可以嵌套:

message Bar {

    extend Foo {
    optional int32 bar = 102;
    }

    optional string name =1;
    optional Foo foo = 2;
}

Java中设置扩展的字段:

BarProto.Bar.Builder bar = BarProto.Bar.newBuilder();
bar.setName("zjd");
        
FooProto.Foo.Builder foo = FooProto.Foo.newBuilder();
foo.setA(1);
foo.setExtension(BarProto.Bar.bar,12);
        
bar.setFoo(foo.build());
System.out.println(bar.getFoo().getExtension(BarProto.Bar.bar));

个人觉得使用起来非常不方便。

有关PB的语法的详细说明,建议看官方文档。PB的语法相对比较简单,一旦能嵌套就能定义出非常复杂的数据结构,基本可以满足我们所有的需求。

编译.proto文件

可以用Google提供的一个proto程序来编译,Windows版本下载protoc.exe。基本使用如下:

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

.proto文件中的java_packagejava_outer_classname定义了生成的Java类的包名和类名。

Protocol Buffers API

AddressBookProtos.java中对应.proto文件中的每个message都会生成一个内部类:AddressBookPerson。 每个类都有自己的一个内部类Builder用来创建实例。messages只有getter只读方法,builders既有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 phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);

Person.Builder

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

除了JavaBeans风格的getter-setter方法之外,还会生成一些其他getter-setter方法:

  • has_ 非repeated的字段都有一个这样的方法来判断字段值是否设置了还是取的默认值。
  • clear_ 每个字段都有1个clear方法用来清理字段的值为空。
  • _Count 返回repeated字段的个数。
  • addAll_ 给repeated字段赋值集合。
  • repeated字段还有根据index设置和读取的方法。

枚举和嵌套类

message嵌套message会生成嵌套类,enum会生成未Java 5的枚举类型。

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

Builders vs. Messages

所有的messages生成的类像Java的string一样都是不可变的。要实例化一个message必须先创建一个builder, 修改message类只能通过builder类的setter方法修改。每个setter方法会返回builder自身,这样就能在一行代码内完成所有字段的设置:

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

每个message和builder提供了以下几个方法:

  • isInitialized(): 检查是否所有的required字段都已经设置;
  • toString(): 返回一个人类可读的字符串,这在debug的时候很有用;
  • mergeFrom(Message other): 只有builder有该方法,合并另外一个message对象,非repeated字段会覆盖,repeated字段则合并两个集合。
  • clear(): 只有builder有该方法,清除所有字段回到空值状态。

解析和序列化

每个message都有以下几个方法用来读写二进制格式的protocol buffer。关于二进制格式,看这里(可能需要FQ)。

  • byte[] toByteArray(); 将message序列化为byte[]。
  • static Person parseFrom(byte[] data); 从byte[]解析出message。
  • void writeTo(OutputStream output); 序列化message并写到OutputStream。
  • static Person parseFrom(InputStream input); 从InputStream读取并解析出message。

每个Protocol buffer类提供了对于二进制数据的一些基本操作,在面向对象上面做的并不是很好,如果需要更丰富操作或者无法修改.proto文件 的情况下,建议在生成的类的基础上封装一层。

Writing A Message

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.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();
  }
}
View Code

相关文章:

  • 2022-02-26
  • 2022-12-23
  • 2022-12-23
  • 2022-01-30
  • 2021-09-27
  • 2022-12-23
  • 2021-11-28
  • 2021-07-09
猜你喜欢
  • 2022-12-23
  • 2022-01-03
  • 2021-11-15
  • 2021-07-04
  • 2022-01-27
相关资源
相似解决方案