美文网首页
Protocol Buffer V3.3.0 在Andoroi

Protocol Buffer V3.3.0 在Andoroi

作者: 黄海佳 | 来源:发表于2017-07-13 17:03 被阅读658次

    前面翻译了谷歌的Proto3 语言指南

    这一篇讲实战应用
    先总结一下 Protocol Buffer

    一、什么是Protocol Buffer

    Google 出品 的一种结构化数据 的数据存储格式,类似于 XML、Json。但更小,更快,更简单。
    它可以通过将 结构化的数据 进行 串行化(序列化),从而实现 数据存储 / RPC 数据交换的功能。

    • 序列化: 将 数据结构或对象 转换成 二进制串 的过程
    • 反序列化:将在序列化过程中所生成的二进制串 转换成 数据结构或者对象 的过程
    1 使用场景

    传输数据量大 & 网络环境不稳定 的数据存储、RPC 数据交换 的需求场景(如即时IM )

    2 相对于其他数据存储格式 Protocol Buffer的优势与缺点
    二、Window下 Protocol Buffer的安装环境

    要使用Protocol Buffer,先要配好环境

    1 下载Protocol Buffers v3.3.0

    官方下载Protocol Buffers v3.3.0
    需翻墙(翻墙都不会?!!还好我有百度云)
    百度云下载

    2 下载protoc-3.3.0-win32.zip

    protoc-3.3.0-win32.zip

    3 将protoc-3.3.0-win32.zip解压后的bin/protoc.exe文件拷贝到解压后的protobuf-3.3目录中,并配置好环境变量。

    执行命令检查是否安装成功

    protoc --version
    

    这样环境算是搞好了

    三、Protocol Buffer在JAVA中的应用

    如果你对Proto的基本语法以及结构还不熟悉,那么请你先快速看一下这篇 Proto3 语言指南

    1、创建 .proto 文件

    我们先通过官方的Demo来感知一下 .proto 文件是怎么样的,然后根据它的格式创建你想要的结构文件。

    addressbook.proto

    // [START declaration]
    syntax = "proto3";
    package tutorial;
    // [END declaration]
    
    // [START java_declaration]
    option java_package = "com.example.tutorial";
    option java_outer_classname = "AddressBookProtos";
    // [END java_declaration]
    
    // [START csharp_declaration]
    option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
    // [END csharp_declaration]
    
    // [START messages]
    message Person {
      string name = 1;
      int32 id = 2;  // Unique ID number for this person.
      string email = 3;
    
      enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
      }
    
      message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
      }
    
      repeated PhoneNumber phones = 4;
    }
    
    // Our address book file is just one of these.
    message AddressBook {
      repeated Person people = 1;
    }
    // [END messages]
    

    解释一下上面部分关键字的定义

    • required 字段必须提供,否则消息将被认为是 "未初始化的 (uninitialized)"。尝试构建一个未初始化的消息将抛出一个 RuntimeException。解析一个未初始化的消息将抛出一个 IOException。
    • optional 字段可以设置也可以不设置。如果可选的字段值没有设置,则将使用默认值。默认值你可以自己定义,也可以用系统默认值:数字类型为0,字符串类型为空字符串,bools值为false。
    • repeated 字段可以重复任意多次 (包括0)(相当于JAVA中的list)
    • default 默认值
    2、编译 .proto 文件

    通过以下命令来编译 .proto 文件:

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

    -I--java_out 分别用于指定源目录 (放置应用程序源代码的地方 —— 如果没有提供则使用当前目录),目的目录 (希望放置生成的代码的位置),最后的参数为 .proto 文件的路径。
    protoc会按照标准Java风格,生成Java类及目录结构。如对于上面的例子,会生成 com/example/tutorial/ 目录结构,及 AddressBookProtos.java 文件。

    如我的addressbook.proto文件放在E盘下,执行:

    E:\pro>protoc -I=E:/pro --java_out=E:/pro E:/pro/addressbook.proto
    
    //或者
    E:\pro>protoc -I=./ --java_out=./ ./addressbook.proto
    

    最后生成此文件

    3、但是当你把生成的文件放进项目中的时候,你会发现报错了
    ...
    Error:(9, 26) 错误: 程序包com.google.protobuf不存在
    ...
    

    解决方法一:利用eclipse程序进行编译 前面下载protoc-3.3.0-win32.zip
    右键点击java文件夹下面的pom.xml文件


    生成protobuf-java-3.3.0.jar

    解决方法二:网上下一个protobuf-java-3.3.0.jar,如果找不到留言联系我

    关于 Protocol Buffers 编译文件后的细节
    1、简单分析通过protoc编译后的文件到底是有什么东西

    这么简单的一个结构文件居然生成了快2700行的代码,看着就头痛

    抽取一些对象以及它们相关的代码发现共同点:

    • 每个类都有它自己的 Builder 类,你可以用来创建那个类的实例。
    • 消息只有getters,而builders则同时具有getters和setters。
    • 每个字段都会有四个常用方法(hasX,getX,setX,clearX)
    • 定义了 repeated 之后会多( getXList()、getXCount、addXPhone)这几个方法。
    • accessor方法是如何以驼峰形式命名的,所以在命名.proto 文件字段时,尽量使用小写字母加下划线。
    // 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();
    
    
    2 每个消息和builder类还包含大量的其它方法,来让你检查或管理整个消息
    • isInitialized() : 检查是否所有的required字段都已经被设置了。
    • toString() : 返回一个人类可读的消息表示,对调试特别有用。
    • mergeFrom(Message other): (只有builder可用) 将 other 的内容合并到这个消息中,覆写单数的字段,附接重复的。
    • clear(): (只有builder可用) 清空所有的元素为空状态。

    更多信息,请参考 Message的完整API文档

    3 解析和序列化

    每个protocol buffer类都有使用protocol buffer 二进制格式写和读你所选择类型的消息的方法。这些方法包括:

    • byte[] toByteArray();: 序列化消息并返回一个包含它的原始字节的字节数组。
    • static Person parseFrom(byte[] data);: 从给定的字节数组解析一个消息。
    • void writeTo(OutputStream output);: 序列化消息并将消息写入 OutputStream。
    • static Person parseFrom(InputStream input);: 从一个 InputStream 读取并解析消息。
    4 写消息

    你要想把你个人信息写进AddressBook,流程如下:

    • 程序通过文件读取一个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.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();
      }
    }
    
    
    四、 Protocol Buffers 在Android中的使用
    1 添加依赖
    dependencies {
        ...
        compile 'com.google.protobuf:protobuf-java:3.3.0'
    }
    
    
    2、将刚编译好的文件对应包名放在你的项目目录下

    放好之后,你可以通过如下方式去Builder 你的对象:

    Person haiJia =
      Person.newBuilder()
        .setId(888)
        .setName("黄海佳")
        .setEmail("haijia@qq.com")
        .addPhone(
          Person.PhoneNumber.newBuilder()
            .setNumber("110-119")
            .setType(Person.PhoneType.HOME))
        .build();
    
    3、写消息
    public class AddPerson {
    
    
        // This function fills in a Person message based on user input.
        static Person PromptForAddress(BufferedReader stdin) throws IOException {
            Person.Builder person = Person.newBuilder();
    
            LogUtils.log("Enter person ID: ");
            person.setId(Integer.valueOf(stdin.readLine()));
    
            LogUtils.log("Enter name: ");
            person.setName(stdin.readLine());
    
            LogUtils.log("Enter email address (blank for none): ");
            String email = stdin.readLine();
            if (email.length() > 0) {
                person.setEmail(email);
            }
    
            while (true) {
                LogUtils.log("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);
    
                LogUtils.log("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 {
                    LogUtils.log("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))));
    
            // Write the new address book back to disk.
            FileOutputStream output = new FileOutputStream(args[0]);
            addressBook.build().writeTo(output);
            output.close();
        }
    
    
    }
    
    4 读消息
    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);
      }
    }
    
    
    
    五、使用protobuf-gradle-plugin

    每次单独执行protoc编译 .proto 文件总是太麻烦,通过protobuf-gradle-plugin可以在编译我们的app时自动地编译 .proto 文件,这样就大大降低了我们在Android项目中使用 Protocol Buffers 的难度。

    1 首先我们需要将 .proto 文件添加进我们的项目中,如:
    2 然后修改 app/build.gradle 对protobuf gradle插件做配置:
    buildscript {
     repositories {
         jcenter()
         mavenCentral()
     }
     dependencies {
         classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0'
     }
    }
    
    
    
    3 添加protobuf块,对protobuf-gradle-plugin的执行做配置:
    protobuf {
     protoc {
         path = '/usr/local/bin/protoc'
     }
    
     generateProtoTasks {
         all().each { task ->
             task.builtins {
                 remove java
             }
             task.builtins {
                 java { }
                 // Add cpp output without any option.
                 // DO NOT omit the braces if you want this builtin to be added.
                 cpp { }
             }
         }
     }
    }
    
    

    protoc块用于配置Protocol Buffers编译器,这里我们指定用我们之前手动编译的编译器。
    task.builtins的块必不可少,这个块用于指定我们要为那些编程语言生成代码,这里我们为C++和Java生成代码。缺少这个块的话,在编译时会报出如下的错误:

    Information:Gradle tasks [:app:generateDebugSources, :app:mockableAndroidJar, :app:prepareDebugUnitTestDependencies, :app:generateDebugAndroidTestSources, :netlib:generateDebugSources, :netlib:mockableAndroidJar, :netlib:prepareDebugUnitTestDependencies, :netlib:generateDebugAndroidTestSources]
    Error:Execution failed for task ':app:generateDebugProto'.
    > protoc: stdout: . stderr: /media/data/CorpProjects/netlibdemo/app/build/extracted-protos/main: warning: directory does not exist.
    /media/data/CorpProjects/netlibdemo/app/src/debug/proto: warning: directory does not exist.
    /media/data/CorpProjects/netlibdemo/app/build/extracted-protos/debug: warning: directory does not exist.
    /media/data/CorpProjects/netlibdemo/app/build/extracted-include-protos/debug: warning: directory does not exist.
    /media/data/CorpProjects/netlibdemo/app/src/debug/proto: warning: directory does not exist.
    /media/data/CorpProjects/netlibdemo/app/build/extracted-protos/debug: warning: directory does not exist.
    /media/data/CorpProjects/netlibdemo/app/build/extracted-include-protos/debug: warning: directory does not exist.
    Missing output directives.
    
    

    提示说没有指定输出目录的路径。
    这是由于 protobuf-gradle-plugin 执行的protobuf编译器命令的参数是在protobuf-gradle-plugin/src/main/groovy/com/google/protobuf/gradle/GenerateProtoTask.groovy中构造的:

    def cmd = [ tools.protoc.path ]
     cmd.addAll(dirs)
    
     // Handle code generation built-ins
     builtins.each { builtin ->
       String outPrefix = makeOptionsPrefix(builtin.options)
       cmd += "--${builtin.name}_out=${outPrefix}${getOutputDir(builtin)}"
     }
    
     // Handle code generation plugins
     plugins.each { plugin ->
       String name = plugin.name
       ExecutableLocator locator = tools.plugins.findByName(name)
       if (locator == null) {
         throw new GradleException("Codegen plugin ${name} not defined")
       }
       String pluginOutPrefix = makeOptionsPrefix(plugin.options)
       cmd += "--${name}_out=${pluginOutPrefix}${getOutputDir(plugin)}"
       cmd += "--plugin=protoc-gen-${name}=${locator.path}"
     }
    
     if (generateDescriptorSet) {
       def path = getDescriptorPath()
       // Ensure that the folder for the descriptor exists;
       // the user may have set it to point outside an existing tree
       def folder = new File(path).parentFile
       if (!folder.exists()) {
         folder.mkdirs()
       }
       cmd += "--descriptor_set_out=${path}"
       if (descriptorSetOptions.includeImports) {
         cmd += "--include_imports"
       }
       if (descriptorSetOptions.includeSourceInfo) {
         cmd += "--include_source_info"
       }
     }
    
     cmd.addAll protoFiles
     logger.log(LogLevel.INFO, cmd.toString())
     def stdout = new StringBuffer()
     def stderr = new StringBuffer()
     Process result = cmd.execute()
     result.waitForProcessOutput(stdout, stderr)
     def output = "protoc: stdout: ${stdout}. stderr: ${stderr}"
     logger.log(LogLevel.INFO, cmd)
     if (result.exitValue() == 0) {
       logger.log(LogLevel.INFO, output)
     } else {
       throw new GradleException(output)
     }
    
    

    可以看到,输出目录是由builtins构造的。

    4 指定 .proto 文件的路径
    sourceSets {
         main {
             java {
                 srcDir 'src/main/java'
             }
             proto {
                 srcDir 'src/main/proto'
             }
         }
     }
    
    

    这样我们就不用那么麻烦每次手动执行protoc了。
    对前面的protobuf块做一点点修改,我们甚至来编译protobuf编译器都不需要了。修改如下:

    protobuf {
        protoc {
            artifact = 'com.google.protobuf:protoc:3.0.0'
        }
    
        generateProtoTasks {
            all().each { task ->
                task.builtins {
                    remove java
                }
                task.builtins {
                    java { }
                    cpp { }
                }
            }
        }
    }
    
    

    相关文章

      网友评论

          本文标题: Protocol Buffer V3.3.0 在Andoroi

          本文链接:https://www.haomeiwen.com/subject/zcawhxtx.html