基于Java的ProtocolBuffer


这篇博客既是帮助一些初学者深入理解protocolBuffer,也是为了方便自己记忆和进一步学习。本文主要介绍了三个方面,包括:

· 在一个.proto文件里面如何定义消息格式

· 如何使用protocol buffer的编译器

· 如何使用java protocol bufferAPI来读写消息

首先,让我们来了解一下为什么要使用protocolBuffer?

假设我们现在要做一个叫做“address book”的应用程序来发送和接收人们的通讯录,这个通讯录包含了人们的姓名、身份证号码、电子邮箱、和一系列的联系方式(包括手机电话等等)。

那么你如何序列化并且检索这些结构化的数据呢?常规的有以下几种做法:

1.使用Java的序列化接口,这是我们在使用Java编程时候的常规做法,但是Java的序列化方式却有着许多缺点(这里暂不深究)。并且它在与用其他面向对象语言编写的程序(例如:C++,Python)进行数据共享的时候也存在着许许多多的问题。

2.你可以用一种特殊的方法把一些数据编码成一个字符串-例如把四个整数编码成这种形式“12:3:23:67”。这是一种非常简单并且灵活的方法,不过它需要按照这种特殊格式来进行解码,那么解码工作就需要一定的时间开销。但是这对于一些比较简单的消息来说无非是最好的方法。

3.把这些数据序列化然后写入XML。自从许多语言都为XML建立语言库之后,这种方法无疑是非常吸引人的。当你需要与其他的工程或者应用程序进行数据共享的时候,这无疑是一个很好的选择。然而,众所周知的是,XML的文字空间过于密集,编码和解码无疑成了应用程序的一个巨大工程。并且,要操作一系列相关的XML文档集,要比操作一些类集领域要复杂的多。

所以,Google公司就研发了灵活度非常高,并且能够高效的自动解决这些问题的protocolBuffer。使用protocol buffer的时候,你只需要写一个.proto文件来定义你需要结构化并且保存的数据,protocol buffer的编译器就会自动的为你解析protocol buffer数据(protocol buffer使用的是二进制的数据格式)并且完成编码工作。它生成的类为protocol buffer提供了getterssetters,并且特别的将定义readingwriting protocol buffer数据的方法作为一个单元。需要注意的是,protocol buffer格式支持超时的数据,并且仍然可以读取使用以前的格式编写的代码。

说了这么多,那就来真实的感受一下吧,就拿之前说到的“address book”的应用程序来举例,首先:

创建一个.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,让我们通过该文件的每一个部分来分析一下吧。

首先,这个文件以一个package的定义开头,以防在不同工程中的命名冲突,在Java里面,package就是用来当做Java package(除非你有明确的定义一个java package) 不过,在这里,即使你已经提供了一个java_package,你仍然需要定义一个package以防Protocol Buffer使用在其他没有Java语言的环境中。

package的定义之后,你可以看到两个option(选项):java_package以及java_outer_classnamejava_package是用来定义java类应该在哪个包里面生成,如果你没有写这一项的话,那么他默认的会以你的package的定义来生成。Java_outer_classname这一项是用来定义在这个文件中的哪一个类需要包含所有的这些类的信息,如果你不定义这一项的话,那么它会以你的文件名来作为刻板,比如说如果你写的是“my_proto.proto”的话,那么它就会默认的使用“MyProto”来作为类名。

接下来就是你消息的类型定义了。一个message就是一系列类型领域的集合,许多基础的数据类型在这里面都是可用的,包括bool,int32,float,double,以及string等。你同样可以添加更多的结构化的数据在里面,比如上面的Person message就包含了PhoneNumber message,AddressBook message包含了Person message。你同样在一中message类型里面定义另外一种类型,例如上面所举到的,Person里面所定义的enum(枚举)类型,以区分PhoneNumber的不同类型。

在每一个元素之后的“=1”,“=2”是用来区分它们的独特的“标签”。标签数字1-15编码所需的字节数比更高的数字所需的字节数要少一个,所以为了使程序达到最佳的状态,你可以使用这些标签进行反复标记(在同一个域中不能重复)。每一个元素在重复的领域都需要重新编码这些“标签”,所以重复的领域应该考虑到更多可能的方案来达到最佳状态。

每一个域的前面都必须使用下面这些修饰符来修饰:

·required(必需的)这说明这个域的值不能为空,否则这条message将会被当做“不知情的”。如果你尝试的创建一条“不知情的”message,那么系统将会抛出一个RuntimeException(运行时异常)。而分析一条“不知情的”message则会抛出IOException。除此之外,确切的来说一个被required修饰的域其行为则更接近optional修饰的域。 

·optional(可选择的)被这个修饰符修饰的域可以为空。如果一个被optional修饰的域没有设值的话,那么系统将会使用默认值。对于一些基本类型来说,你可以定义你自己的默认值(就像我前面在定义PhoneNumberPhoneType时一样) 否则,对于数值类型来说,系统的默认值是0string的默认值是empty string,bool的默认值是false。对于植入的message来说(比如AddressBook里面的Person),默认值则经常是该消息默认的实例或者标准。调用存取器去获取那些被optional(或者required)修饰的但还没有被初始化的域将会返回它的默认值。

·repeated(反复的)某一个域可能会被使用多次,而那些反复使用的值将会被保留在protocol buffer里面。 你可以把用repeated修饰的域想象成动态数组。

 

Required是“永久”的

当你使用required来修饰域的时候你必须非常的小心。如果某些时候你想要停止发送一个用required修饰的域并将它修改为optional修饰时,之前的readers会把你的message考虑为不完整的并且无意识的丢弃它。事实上,人们已经开始注意到了使用required的所带来的危害,所以我们应该更多的使用optionalrepeated

 

使用Protocol buffer的编译器

现在,你已经有一个.proto文件了,接下来就需要生成class来发送和读取AddressBook消息(包含Person以及PhoneNumber),为了完成这件工作,你需要调用protocol buffer的编译器的proto指令来编译你的.proto文件。语法如下:

proto -I=$SRC --java_out=$DIR File

$SRC表示资源文件夹,$DIR表示目标文件夹,File是你要编译的.proto文件,当然,你也可以定位到这个资源目录中然后只调用proto --java_out=$DIR File即可。(需要注意的是生成的class文件会存在于你所填的目标文件夹下的java_package所指向的目录中,如果你想把目标文件夹指向当前目录,你可以使用“.”来代替,例如对于该例,我们先定位到这个目录下,然后运行proto --java_out=. addressbook.proto)

 

使用protocol bufferAPI

首先,让我们看看编译器给我们生成了那些代码。首先你可以发现它的类名与我们定义的java_outer_classname的名字相同,同样里面还包含了你在addressbook.proto里面定义的各种message的类,每一个类都有它自身的Builder用来实例化这个类对象。所有的messagesbuilders都自动生成了存取器,但是messages只有getterbuilders既有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 phone = 4;
public List 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 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 value);
public Builder clearPhone();


正如你所看到的一样,这些都是一些简单的JavaBean-stylegetterssetters,但是用repeated修饰的域有一些特殊的方法,Count方法(用来统计这个消息list(列表)的长度)。通过add 方法可以在这个list中追加一个元素,而addAll 方法可以把一个Container(容器)里面的所有元素都添加在list当中。

注意到这些方法都是使用的驼峰式命名法,尽管在.proto文件里面我们都是写的小写,这也恰恰展示了protocol buffer的强大之处。

另外,还有一个需要注意的就是enum(枚举)类型所生成的类,它自动生成了如下代码:

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

Builders vs. Messages

Message类里面由protocol buffer编译器自动生成的代码都是不可变的,一旦一个message对象被实例化之后,它就不能再被修改了,就像Java中的String一样。而如果想要实例化一个message类,你必须首先实例化一个builder类,然后设置好所有你想要设置的属性,然后再调用builder类的build()方法。你或许已经注意到了builder的每一个用来改变message的属性的方法都返回了另外一个builder。不要怀疑,这个 builder就是为了让你更加方便的定义其他属性而存在的。下面就展示了一段用来创建一个新的Person类的代码:

Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("[email protected]") 
.addPhone( Person.PhoneNumber.newBuilder().setNumber("555-4321")       
.setType(Person.PhoneType.HOME))    
.build();


标准的Message方法

每一个message以及builder类都包含了一些其他的方法用来检测和操作所有的message,这些方法包括:

·isInitialized()检测所有用required修饰的域是否都设置了初始值。

·toString()返回一个可读的message,特别是用来测试的时候 

·mergeFrom(Message other): (builder独有的把另一个message合并到这个message当中,重写单独域并且连接反复的域。

·clear(): (builder独有的)清空所有的域并且返回空的状态

这些方法都实现了MessageMessage.Builder 接口并且接口被所有的Java messages以及builders共享。

 

解析和序列化

最后,所有的protocol buffer类都有writingreading你所选择的protocol buffer (二进制)格式数据的方法。他们包括:

·byte[] toByteArray();序列化这个 message 并且返回一个字节数组。

·static Person parseFrom(byte[] data);从给定的字节数组中解析一条message

·void writeTo(OutputStream output);序列化这个message并且将它写入 OutputStream.

·static Person parseFrom(InputStream input);读取并且解析一条InputStream中的message

这里只是其中的几个解析和序列化的方法而已,如果想要知道其中所有的方法可以将该类生成doc文档然后查看。

 

Protocol BuffersO-O Design

 Protocol buffer的类基本上是无声的数据持有者(就像 C++里面的结构体一样);他们最开始在一个对象模型中表现的并不友好,如果你想要为生成的类添加一个更加友好的方法的话,最好的方式是将protocol buffer生成的类包装在一个特定的应用程序里面。对于不太懂.proto文件设计的人来说包装protocol buffer也是一个不错的主意。你也可以使用包装类生成接口以便更好地适应特定的程序环境:隐藏一些数据和方法,并且显示一些便捷的功能,等等。特别需要注意的是,你最好不要写一些行为去继承这些生成的类。那会打破它内部的机制况且它对于你来说也不是一次很好的面向对象的练习机会。

 

Writing A Message

OK,说了这么多,那就来使用一下protocol buffer生成的类吧。首先,你肯定希望你的这个“addressbook”的应用程序可以write一个你定义的message。为了完成这项工作,你需要创建一个新的类来调用protocol buffer类里面的方法并将message写入OutputStream.

下面就是一个将用户在控制台输入的AddressBook 的相关信息写入文件的一个类,当然,你首先得创建一个文件(当然你也可以在文件不存在的情况下使用File类的createNewFile()方法来创建一个新的文件),为了个性化你的程序,不妨以.book作为你的后缀名,具体代码如下:

import java.io.BufferedReader;  
import java.io.FileInputStream;  
import java.io.FileNotFoundException;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.InputStreamReader;  
import java.io.PrintStream;  
  
import com.example.tutorial.AddressBookProtos.AddressBook;  
import com.example.tutorial.AddressBookProtos.Person;  
class AddPerson {  
    /** 
     * 将用户输入的Person message写入输出流中   
     * @param stdin 输入流 
     * @param stdout 打印输出流 
     * @return Person类 
     * @throws IOException 
     */  
    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) {  
            //按下Enter键结束输入  
            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();  
          
        // 检验是否存在这个文件  
        try {  
            addressBook.mergeFrom(new FileInputStream("src/Book/TestPerson.book"));  
            } catch (FileNotFoundException e) {  
                System.out.println("src/Book/TestPerson.book" + ": File not found.Creating a new file.");  
            }      
          
        //将这条Person message添加到AddressBook中  
        addressBook.addPerson(PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),System.out));  
          
        //将新建的AddressBook写入文件当中  
        FileOutputStream output = new FileOutputStream("src/Book/TestPerson.book");  
        addressBook.build().writeTo(output);  
        output.close();    
    }  
}  

Reading A Message

当然,这个程序肯定不止一个写入消息的类,还要能把存在文件中的数据读出来。如下:

import com.example.tutorial.AddressBookProtos.AddressBook;  
import com.example.tutorial.AddressBookProtos.Person;  
import java.io.FileInputStream;  
class ListPeople {  
    /** 
     * 迭代遍历并且打印文件中所包含的信息 
     * @param addressBook AddressBook对象 
     */  
    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());  
                }  
            }  
        }  
      
    public static void main(String[] args) throws Exception {  
//      if (args.length != 1) {  
//          System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");   
//          System.exit(-1);  
//      }   
        // 读取已经存在.book文件  
        AddressBook addressBook = AddressBook.parseFrom(new FileInputStream("src/Book/TestPerson.book"));  
        Print(addressBook);  
    }  
 }  

 

扩展一个Protocol Buffer

当你发布了一段由你的protocol buffer编写的代码之后,你或许迫不及待的想要扩展它的功能。如果你想要使你的新buffers是反向兼容的或者你的旧buffers是正向兼容的话,那么下面有几条规则是你需要遵守的,在新的protocol buffer的版本中:· 你最好不要改变已经存在的域的标签(Tag)

   ·你最好不要添加或者删除任何用required修饰的域

      ·你可以删除optional或者repeated修饰的域

   ·你可以添加新的用optional或者repeated修饰的域但你必须使用Tag数字(从未被这个protocol所使用过的tag,即使是被删除了的也不行).

如果你遵循这些规则,旧的代码也会非常“高兴”的读取新的消息。对于旧的代码来说,那些被删除了的用optional修饰的域会有他们的默认值并且被删除了用repeated修饰的域会为空,新的代码读取旧的消息也会很轻松。然而,请记住,新的optional域不会存在于旧的message当中,所以你应该明确的知道它们是否被设置为has_或者在你的.proto 文件提供了一个合理的默认值[default = value]。如果默认值没有明确是一个optional元素,而是按默认类型定义的话对于string来说默认值就是empty string,其他类型也类型,在本文的上面已经提到过了,这里就不在累赘。特别声明,如果你添加了一个新的用repeated修饰的域而没有has_标志的话,你的新代码将无法识别它是否为空,而你的旧代码则完全无法设置它。

 

高级用法

Protocol buffers还有一些用法是一般的存取器和序列化所无法办到的,如果你需要了解更多的信息可以上Google的官方文档上面去查看。

Protocolmessage类提供的一个关键特征就是映射 在一个message里面你可以反复声明不同的域并且操作他们的值而不需要在任何类型的message之前写代码。使用映射来从其他的编码中转换一条message无疑是一个非常有效的方法,即使面对XML 或者JSON也一样。关于映射的一个更加高级的用法恐怕就是找出两条相同类型的message类之间的不同点了,或者是为Protocol buffer生成一系列的“规则映像”,使用这些映像你可以匹配确切的消息内容。发挥你的想象,Protocol Buffers可以应用到更多的领域当中去。

                                                  (材料取自:Google官方文档)

PS:由于可视化编辑器的问题,本来想用颜色着重标记一些重点的,不过对这个感兴趣的人也会好好的看一看吧。

你可能感兴趣的:(Java,java,protocolbuffer)