美文网首页
Android 解析 Protocol Buffers 格式数据

Android 解析 Protocol Buffers 格式数据

作者: Little丶Jerry | 来源:发表于2018-11-30 17:15 被阅读0次

    protocolbuffers/protobuf GitHub 地址

    protocolbuffers/protobuf 官网

    一、前言

    Protocol Buffers 简称为 protobuf,是 Google 公司开发的一种数据描述语言,类似于 XML 能够将结构化数据序列化,可用于数据存储、通信协议等方面。相比于现在流行的 XML 以及 JSON 格式存储数据,通过 Protocol Buffers 来定义的文件体积更小,解析速度更快。

    假设你要为具有姓名和电子邮件的人建模,使用 XML 格式:

      <person>
        <name>John Doe</name>
        <email>jdoe@example.com</email>
      </person>
    

    而对应的 protocol buffers 格式:

    # Textual representation of a protocol buffer.
    # This is *not* the binary format used on the wire.
    person {
      name: "John Doe"
      email: "jdoe@example.com"
    }
    

    1.1 编写 protobuf 文件

    .proto 文件中的定义很简单:为要序列化的每个数据结构添加 message,然后为 message 中的每个字段指定名称和类型。通俗地说,message 类似 Java 中的类,里面可以定义我们需要的属性。下面是官方的一个示例,定义一个 addressbook.proto 文件。地址簿中的每个人都有姓名,ID,邮件地址和联系电话。

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

    如上所示,语法类似于 C++ 或 Java。

    .proto 文件以包声明为开头,这有助于防止不同项目之间的命名冲突。在 Java 中,包的名称被当作 Java 包,除非你明确指定了 java_package,就像例子中的一样。即使你提供了 java_package,你仍然应该定义一个普通的包名,以避免在 Protocol Buffers 命名空间和非 Java 语言中发生命名冲突。

    在包声明之后,你可以看到两个对于 Java 的特定选项:java_packagejava_outer_classnamejava_package 指定生成的类应该放在哪个 Java 包下。如果没有明确指定它,它只是匹配包声明给出的包名,但这些名称通常不是合适的 Java 包名(因为它们通常不以域名开头)。java_outer_classname 选项用来定义包含此文件中所有类的类名,即最外层类的类名。如果你没有提供 java_outer_classname,则会通过骆驼命名法转换文件名来生成它。例如,my_proto.proto 将使用 MyProto 作为外部类名。

    接下来,你需要定义 message (信息)message 只是包含一组类型字段的集合。许多标准的简单数据类型都可用作字段类型,包括 boolint32floatdoublestring。你还可以使用其他信息类型作为字段类型。在上面的示例中,Person 信息包含 PhoneNumber 信息,而 AddressBook 信息包含 Person 信息。你甚至可以定义嵌套在其他信息中的信息类型,例如,PhoneNumber 类型在 Person 中定义。如果你希望某个字段具有预定义的值列表中的一个值,你可以定义枚举类型。例如你要指定电话号码可以是 MOBILEHOMEWORK 之一。

    每个元素上的 = 1= 2 标记标识该字段在二进制编码中使用的唯一标签。标签号 1-15 比更大的数字要少一个字节进行编码,因此作为优化,你可以将这些标签用于常用或重复的元素,将标签 16 或更高的标签留给不太常用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段特别适合此优化。

    protocol buffers 不支持继承。

    1.2 修饰符类型

    必须使用以下修饰符之一修饰每个字段:

    • required:必须提供该字段的值,否则信息将被视为 “未初始化”。构建未初始化的信息将抛出 RuntimeException。解析未初始化的信息将抛出 IOException。除此之外,required 字段的行为与 optional 字段完全相同。

    • optional:该字段可以设置也可以不设置。如果未设置 optional 字段值,则会使用默认值。对于简单类型,你可以指定自己的默认值,就像我们在示例中为电话号码类型所做的那样。否则,将使用系统默认值:数字类型为 0,字符串为空字符串,boolsfalse。对于嵌入式信息,默认值始终是信息的 默认实例原型,其中没有设置任何字段。通过访问器获取尚未显式设置的 optional(或 required)字段的值始终返回该字段的默认值。

    • repeated:该字段可以被重复任意次数(包括 0 次),但是它们的顺序会被保留。可以将重复字段视为动态大小的数组。

    二、解析 protobuf

    2.1 下载最新的 proctoc.exe

    这里可以下载最新发布的 protobuf。这是 protoc-3.6.1-win32.zip 的下载地址,适用于 windows 系统,也有适用于 linux 系统的,大家可以按需下载。下载之后里面有 protoc.exe,我们把它配置到环境变量,然后使用 cmd 运行即可。

    2.2 生成 Java 文件

    运行 protoc.exe 编译 .proto 文件生成 Java 文件,指定源目录(应用程序的源代码所在的位置,如果不提​​供值,则使用当前目录),目标目录(生成文件的输出路径,通常跟源目录相同),以及 .proto 文件的所在目录。执行命令如下:

    protoc -I=$SRC_DIR(源目录) --java_out=$DST_DIR(目标目录) $SRC_DIR(.proto 文件所在目录)/addressbook.proto
    

    如果使用上述官方示例的 .proto 文件,则会在指定的目标目录中生成 com/example/tutorial/AddressBookProtos.java

    笔者为了适配自己的应用,修改了下 java_package 的值,如下:

    syntax = "proto2";
    
    package tutorial;
    
    option java_package = "com.example.jerry.myapplication.tutorial";
    
    option java_outer_classname = "AddressBookProtos";
    

    然后执行编译命令:

    protoc -I=D:/AndroidProjects/MyApplication 
    --java_out=D:/AndroidProjects/MyApplication/app/src/main/java/ 
    D:/AndroidProjects/MyApplication/addressbook.proto
    

    于是我们想要的 Java 文件就顺利生成了。

    其实笔者在执行命令的时候还是遇到不少问题的:

    • 如果命令中没有指定源目录,则 .proto 文件必须放在 cmd 的当前目录下

    • 如果指定了源目录,则 .proto 文件必须放在源目录下

    • 还有很重要的一点,在 windows 系统下路径分隔符不能用 \,而要改为 /

    有关生成的 Java 文件的内部代码解析,请看官方说明,这里就不一一阐述了。关键部分就是 MessageMessage.Builder 的联系和区别。还有几个标准的 Message 方法:

    • isInitialized():检查是否设置了所有 required 字段。

    • toString():返回一个具有可读性的 message 表示,对调试特别有用。

    • mergeFrom(Message other):仅限 Builder 使用,将其他 message 的内容合并到此 message 中,覆盖标量字段,合并复合字段以及连接重复字段。

    • clear():仅限 Builder 使用,将所有字段清除回空状态。

    2.3 添加 Maven 依赖

    因为 protobuf 是 Google 提供的,所以使用 Android Studio 很容易引入最新的依赖库。我们在 Project Structure 中选择 Dependencies 选项卡,从网络添加依赖库,输入关键字 com.google.protobuf 就可以搜索到最新的 protocol buffer 依赖库。例如:

    dependencies {
        ...
        implementation 'com.google.protobuf:protobuf-java:3.6.1'
    }
    

    2.4 序列化和反序列化

    每个 protocol buffer 类都有使用 protocol buffer 二进制格式编写和读取所选类型 message 的方法。比如上述地址簿的例子:

    • byte[] toByteArray():序列化 message 对象并返回包含其原始字节的字节数组。

    • static Person parseFrom(byte[] data):解析来自给定字节数组的 message 并返回 Java 类对象。

    • void writeTo(OutputStream output):序列化 message 并将其写入 OutputStream

    • static Person parseFrom(InputStream input):读取并解析来自 InputStreammessage

    2.5 写入信息 Writing A Message

    笔者在这里就直接展示官网的示例代码了,还是地址簿的例子,主要就是掌握上述所说的序列化和反序列化的那几个方法。

    package com.example.jerry.myapplication.tutorial;
    
    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;
    
    public class AddPerson {
    
        // This function fills in a Person message based on user input.
        static AddressBookProtos.Person PromptForAddress(BufferedReader stdin,
                                                         PrintStream stdout) throws IOException {
            AddressBookProtos.Person.Builder person = AddressBookProtos.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;
                }
    
                AddressBookProtos.Person.PhoneNumber.Builder phoneNumber =
                        AddressBookProtos.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(AddressBookProtos.Person.PhoneType.MOBILE);
                } else if (type.equals("home")) {
                    phoneNumber.setType(AddressBookProtos.Person.PhoneType.HOME);
                } else if (type.equals("work")) {
                    phoneNumber.setType(AddressBookProtos.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);
            }
    
            AddressBookProtos.AddressBook.Builder addressBook = AddressBookProtos.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();
        }
    }
    

    2.6 读取信息 Reading A Message

    同上。

    package com.example.jerry.myapplication.tutorial;
    
    import java.io.FileInputStream;
    
    public class ListPeople {
    
        // Iterates though all people in the AddressBook and prints info about them.
        static void Print(AddressBookProtos.AddressBook addressBook) {
            for (AddressBookProtos.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 (AddressBookProtos.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.
            AddressBookProtos.AddressBook addressBook =
                    AddressBookProtos.AddressBook.parseFrom(new FileInputStream(args[0]));
    
            Print(addressBook);
        }
    }
    

    2.7 小结

    当把依赖库和实体类全部导入到项目中后,就可以根据服务端提供的接口获取数据,然后开始解析。

    解析其实不难,如果实体类叫做 Person,那么只需一句代码:

    Person person = Person.parseFrom(byte[] data);
    

    相关文章

      网友评论

          本文标题:Android 解析 Protocol Buffers 格式数据

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