Google Protocol Buffer

举报
择城终老 发表于 2021/07/26 23:26:06 2021/07/26
【摘要】 Google Protocol Buffer(protobuf)是一种高效且格式可扩展的编码结构化数据的方法。和JSON不同,protobuf支持混合二进制数据,它还有先进的和可扩展的模式支持。protobuf已在大多数软件平台上实现,包括适用于Android的精简Java版。 http://developers.google.com/protocol-buffers/上有...

Google Protocol Buffer(protobuf)是一种高效且格式可扩展的编码结构化数据的方法。和JSON不同,protobuf支持混合二进制数据,它还有先进的和可扩展的模式支持。protobuf已在大多数软件平台上实现,包括适用于Android的精简Java版。

http://developers.google.com/protocol-buffers/上有protobuf文档,下载链接以及安装说明。需要注意的是,Android平台为构建精简版的protobuf,所以不能使用中央Maven仓库里的版本。在Java源码目录内执行mvn package -p lite可以生成精简版。检查是否有更多安装细节。

JSON允许对JSONObject对象进行任意数据的读写操作,但protobuf要求使用模式来定义要存储的数据。模式会定义一些消息,每个消息包含一些名-值对字段。字段可能是内置的原始数据类型,枚举或者其他消息。可以指定一个字段是必须的还是可选的,以及其他一些参数。一旦定义好模式,就可以使用protobuf工具生成Java代码。生成的Java类现在可以很方便地用来读写protobuf数据。

下面的代码使用protobuf模式定义了Task信息:

package com.aptl.code.task;

option optimize_for = LITE_RUNTIME;
option java_package = "com.aptl.protobuf";
option java_outer_classname = "TaskProtos";
message Task {
    enum Status {
        CREATED = 0;
        ONGOING = 1;
        CANCELLED = 2;
        COMPLETED = 3;
    }

message Owner {
        required string name = 1;
        optional string email = 2;
        optional string phone = 3;
    }

message Comment {
        required string author = 1;
        required uint32 timestamp = 2;
        required string content = 3;
    }

required string name = 1;
    required uint64 created = 2;
    required int32 priority = 3;
    required Status status = 4;
    optional Owner owner = 5;
    repeated Comment comments = 6;
}

这里将给出以上消息定义的关键性说明。
      1. message是消息定义的关键字,等同于C++中的struct/class,或是Java中的class。
      2. Task 为消息的名字,等同于结构体名或类名。
      3. required前缀表示该字段为必要字段,既在序列化和反序列化之前该字段必须已经被赋值。与此同时,在Protocol Buffer中还存在另外两个类似的关键字,optional和repeated,带有这两种限定符的消息字段则没有required字段这样的限制。相比于optional,repeated主要用于表示数组字段。具体的使用方式在后面的用例中均会一一列出。
      4. int64和string分别表示长整型和字符串型的消息字段,在Protocol Buffer中存在一张类型对照表,既Protocol Buffer中的数据类型与其他编程语言(C++/Java)中所用类型的对照。该对照表中还将给出在不同的数据场景下,哪种类型更为高效。该对照表将在后面给出。
      5. name,created ,priority ,status,owner comments分别表示消息字段名,等同于Java中的域变量名,或是C++中的成员变量名。
      6. 标签数字12则表示不同的字段在序列化后的二进制数据中的布局位置。在该例中,created字段编码后的数据一定位于name之后。需要注意的是该值在同一message中不能重复。另外,对于Protocol Buffer而言,标签值为1到15的字段在编码时可以得到优化,既标签值和类型信息仅占有一个byte,标签范围是16到2047的将占有两个bytes,而Protocol Buffer可以支持的字段数量则为2的29次方减一。有鉴于此,我们在设计消息结构时,可以尽可能考虑让repeated类型的字段标签位于1到15之间,这样便可以有效的节省编码后的字节数量。

Protocol Buffer允许我们在.proto文件中定义一些常用的选项,这样可以指示Protocol Buffer编译器帮助我们生成更为匹配的目标语言代码。Protocol Buffer内置的选项被分为以下三个级别:
      1. 文件级别,这样的选项将影响当前文件中定义的所有消息和枚举。
      2. 消息级别,这样的选项仅影响某个消息及其包含的所有字段。
      3. 字段级别,这样的选项仅仅响应与其相关的字段。
      下面将给出一些常用的Protocol Buffer选项。
      1. option java_package = "com.aptl.protobuf";
      java_package是文件级别的选项,通过指定该选项可以让生成Java代码的包名为该选项值,如上例中的Java代码包名为com.aptl.protobuf。与此同时,生成的Java文件也将会自动存放到指定输出目录下的com/aptl/protobuf子目录中。如果没有指定该选项,Java的包名则为package关键字指定的名称。该选项对于生成C++代码毫无影响。
      2. option java_outer_classname = "TaskProtos";
      java_outer_classname是文件级别的选项,主要功能是显示的指定生成Java代码的外部类名称。如果没有指定该选项,Java代码的外部类名称为当前文件的文件名部分,同时还要将文件名转换为驼峰格式,如:my_project.proto,那么该文件的默认外部类名称将为MyProject。该选项对于生成C++代码毫无影响。
      注:主要是因为Java中要求同一个.java文件中只能包含一个Java外部类或外部接口,而C++则不存在此限制。因此在.proto文件中定义的消息均为指定外部类的内部类,这样才能将这些消息生成到同一个Java文件中。在实际的使用中,为了避免总是输入该外部类限定符,可以将该外部类静态引入到当前Java文件中,如:import static com.aptl.protobuf.TaskProtos.*
      3. option optimize_for = LITE_RUNTIME;
      optimize_for是文件级别的选项,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。
      SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。
      CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。
      LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。因此我们在C++中链接Protocol Buffer库时仅需链接libprotobuf-lite,而非libprotobuf。在Java中仅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。
      注:对于LITE_MESSAGE选项而言,其生成的代码均将继承自MessageLite,而非Message。    
      4. [pack = true]: 因为历史原因,对于数值型的repeated字段,如int32、int64等,在编码时并没有得到很好的优化,然而在新近版本的Protocol Buffer中,可通过添加[pack=true]的字段选项,以通知Protocol Buffer在为该类型的消息对象编码时更加高效。如:
      repeated int32 samples = 4 [packed=true]。
      注:该选项仅适用于2.3.0以上的Protocol Buffer。
      5. [default = default_value]: optional类型的字段,如果在序列化时没有被设置,或者是老版本的消息中根本不存在该字段,那么在反序列化该类型的消息是,optional的字段将被赋予类型相关的缺省值,如bool被设置为false,int32被设置为0。Protocol Buffer也支持自定义的缺省值,如:
      optional int32 result_per_page = 3 [default = 10]。

从InputStream反序列化protobuf对象非常容易,如下例所示。生成的Java代码提供一些用于合并字节数组,byteBuffer和InputStream对象的函数。

public static TaskProtos.Task readBrotoBufFromStream(InputStream inputStream)
            throws IOException {
        TaskProtos.Task task = TaskProtos.Task.newBuilder()
                .mergeFrom(inputStream).build();
        Log.d("ProtobufDemo", "Read Task from stream: "
                + task.getName() + ", "
                + new Date(task.getCreated()) + ", "
                + (task.hasOwner() ?
                task.getOwner().getName() : "no owner") + ", "
                + task.getStatus().name() + ", "
                + task.getPriority()
                + task.getCommentsCount() + " comments.");
        return task;
    }

本例显示了如何检索protobuf对象的值。注意:protobuf对象是不可变的。修改它们唯一的方法是从现有对象创建一个新的构建器,设置新的值,并生成一个取代原有对象的Task。这使得protobuf有点不好用,但它强制开发者在持久化复杂对象时使用更好的设计。

下面的方法显示了如何构建一个新的protobuf对象。首先为构造的对象创建一个新的Builder,然后设置所需要的值并调用Builder.build()方法来创建不可变的protobuf对象。

public static TaskProtos.Task buildTask(String name, Date created,
                                     String ownerName, String ownerEmail,
                                     String ownerPhone,
                                     TaskProtos.Task.Status status,
                                     int priority,
                                     List<TaskProtos.Task.Comment> comments) {
        TaskProtos.Task.Builder builder = TaskProtos.Task.newBuilder();
        builder.setName(name);
        builder.setCreated(created.getTime());
        builder.setPriority(priority);
        builder.setStatus(status);
        if(ownerName != null) {
            TaskProtos.Task.Owner.Builder ownerBuilder
                    = TaskProtos.Task.Owner.newBuilder();
            ownerBuilder.setName(ownerName);
            if(ownerEmail != null) {
                ownerBuilder.setEmail(ownerEmail);
            }
            if(ownerPhone != null) {
                ownerBuilder.setPhone(ownerPhone);
            }
            builder.setOwner(ownerBuilder);
        }
        if (comments != null) {
            builder.addAllComments(comments);
        }
        return builder.build();
    }

API提供了一系列方法用来把protobuf对象写到文件或者网络流中。下面的代码演示了如何把Task对象序列化到OutputStream中。

public static void writeTaskToStream(TaskProtos.Task task,
                                  OutputStream outputStream)
            throws IOException {
        task.writeTo(outputStream);
    }

protobuf主要的优点是它比JSON消耗的内存少,而且读写速度更快。protobuf对象还是不可变的,如果要确保对象的值在整个生命周期中保持不变,该特性会非常有用。

文章来源: liyuanjinglyj.blog.csdn.net,作者:李元静,版权归原作者所有,如需转载,请联系作者。

原文链接:liyuanjinglyj.blog.csdn.net/article/details/45915671

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。