Protobuf 入门

  1. 云栖社区>
  2. 博客>
  3. 正文

Protobuf 入门

期待l 2019-01-06 20:22:35 浏览2140
展开阅读全文

IDEA中的Protobuf的插件的使用

  • 首先File->setting中找到plugins
  • 然后安装插件

markdown_img_paste_2019010520224182

  • 安装完成后重启IDEA就可以了
  • 测试: 在src/main/下建立proto文件夹,并在其中建立以proto为后缀文件

markdown_img_paste_20190105202548355

  • 然后将如下的proto协议写进去

    syntax = "proto3";
    message SearchRequest {
        string query = 1;
        int32 page_number = 2;
        int32 result_per_page = 3;
    }
  • 然后maven中添加

    <properties>
        <grpc.version>1.6.1</grpc.version>
        <protobuf.version>3.3.0</protobuf.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>${protobuf.version}</version>
        </dependency>
    </dependencies>
    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.5.0.Final</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.5.0</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>  
  • 等待下载完成后,然后点击maven中的compile插件,运行完,如果配置正确,在项目的target的目录下就生成了我们需要的类文件了

markdown_img_paste_20190105203226910

  • 好了介绍到这里,IDEA中使用protobuf插件就完成了,下面是关于protobuf的简介和一些详细内容

简介

  • Google Protocol Buffer 简称Protobuf是Google公司内部的开源项目,可用于RPC系统和持续数据存储系统
  • 是一种轻便高效的结构化数据存储格式,可用于结构化数据串行化,或者序列化,非常适合数据存储或RPC数据交换格式,可用于通讯协议,数据存储等领域,并且是平台无关的

定义数据类型

  • 如上在IDEA中运行的protobuf文件,我们拿这个来说

    syntax = "proto3";
    message SearchRequest {
      string query = 1;
      int32 page_number = 2;
      int32 result_per_page = 3;
    }
    • syntax 指定protobuf的版本,必须是文件的第一行
    • message 代表这是一个消息,SearchRequest消息中指定了三个字段,每一个字段对应于要包含在这种类型的消息中的数据,每一个字段都有一个名称和类型,比如上面的int32 page_number = 2;,名称就是page_number,而类型就是int32,注意等号后面的不是默认值,而可以认为他是此page_number字段的身份证,不可重复,这些数字用于以二进制格式标识您的字段,一旦消息类型被使用就不应该被更改,需要注意的是1到15范围内的字段编号需要一个字节来编码,而16到2047范围内的字段编号需要两个字节,所以经常使用的应该为其编码为1到15之间,这些数字的范围是[1, 536870911],其中19000~19999是为协议缓冲区实现而保留的,不可以使用,否则报错

标量值类型

  • int32 page_number = 2;中的int32叫做标量值类型
  • 下面是标量值类型以及生成的java中的对应类型
类型 说明 对应类型
double null double
float null float
int32 使用可变长度编码,编码负数效率低下,如果你的字段可能有负值,请改用sint32 int
int64 使用可变长度编码,编码负数效率低下,如果你的字段可能有负值,请改用sint64 long
uint32 使用可变长度编码 int
uint64 使用可变长度编码 long
sint32 使用可变长度编码,符号整型值,这些比常规int32编码负数更有效 int
sint64 使用可变长度编码,有符号的int值,这些比常规int64编码负数更有效 long
fixed32 总是四字节,如果值经常大于2^28,则比uint32更有效 int
fixed64 总是八字节,如果值经常大于2^56,则比uint64更有效 long
sfixed32 总是四字节 int
sfixed64 总是八字节 long
bool null boolean
string 字符串必须始终包含UTF8编码或7位ASCII文本 string
bytes 可以包含任意字节序列 byteString
  • 如果你使用的是别的语言,那么还有一张对照图,在下面

markdown_img_paste_20190105210453593

修饰符

  • 如果一个字段被repeated修饰,则表示它是一个列表类型的字段
  • 如果你现在不确定是否某个值或者变量是否以后会使用的到为了不误写,那么可以保留

    message Foo {
      reserved 2, 15, 9 to 11;
      reserved "foo", "bar";
      string foo = 3 // 编译报错,因为‘foo’已经被标为保留字段
    }

缺省值:默认值

  • string类型的默认值是空字符串
  • bytes类型的默认值是空字节
  • bool类型的默认值是false
  • 数字类型的默认值是0
  • enum类型的默认值是第一个定义的枚举值
  • message类型(对象,如上文的SearchRequest就是message类型)的默认值与 语言 相关
  • repeated修饰的字段默认值是空列表,比如repeated string query = 1; = java.util.List<java.lang.String>
  • 如果一个字段的值等于默认值(如bool类型的字段设为false),那么它将不会被序列化,这样的设计是为了节省流量

枚举

  • 之前的都是简单类型,比如int32之类的,那么在message中还可以定义复杂类型
  • 每个枚举值有对应的数值,数值不一定是连续的.第一个枚举值的 数值必须是0且至少有一个枚举值 ,否则编译报错.编译后编译器会为你生成对应语言的枚举类,零值必须为第一个元素,以便与proto 2语义兼容,其中第一个枚举值总是默认的
  • 由于编码原因,出于效率考虑,不推荐使用负数作为枚举值的数值
  • 如下就在之前的基础上添加到了春夏秋冬四个枚举常量值,当然里面的数字标识不用按顺序来

    syntax = "proto3";
    message SearchRequest {
        string query = 1;
        int32 page_number = 2;
        int32 result_per_page = 3;
        Groups groups = 4;
        enum Groups{
            SPRING = 0;
            SUMMER = 1;
            Autumn = 2;
            WINTER = 3;
        }
    }
  • 如果个别的枚举值的标识是相同的,需要用option allow_alias = true;来声明一下,比如

    enum Groups{
        option allow_alias = true;
        SPRING = 0;
        SUMMER = 2;
        Autumn = 2;
        WINTER = 3;
    }
  • 如果去掉option allow_alias = true;,那么编译会出现错误, 并且注意枚举数常量必须在32位整数的范围内
  • 可以使用messageName.enum的形式,在其他message中引用,比如

    message Others {
        SearchRequest.Groups groups = 1;
    }
  • 其生成的类中方法定义的返回值就是Mes.SearchRequest.Groups getGroups();

使用其它的message类型

  • 第一种使用方法如下

    syntax = "proto3";
    message First {
        repeated string query = 1;
        Second second = 2;    //引用其他message类型
    }
    message Second {
        int32 num = 1;
    }
  • 第二种即import导入使用

    syntax = "proto3";
    import "second.proto";   //首先导入
    message First {
        repeated string query = 1;
        Second second = 2;    //引用其他message类型
    }
    • 上面这种导入如果测试,需要将second.proto移到相同目录下,并且First和Second消息是两个proto文件

嵌套

  • 可以在一个message类型中定义另一个message类型,并且可以一直嵌套下去,类似Java的内部类

    message SearchResponse {
      message Result {
        string url = 1;
        string title = 2;
        repeated string snippets = 3;
      }
      repeated Result results = 1;
    }
  • 可以使用Parent.Type的形式引用嵌套的message

    message SomeOtherMessage {
      SearchResponse.Result result = 1;
    }

更新message

  • 当proto不能满足要求的情况下,那么我们就需要按照要求更新message

    • 不要更改任何已有的字段的数值标识
    • 如果增加新的字段,使用旧格式的字段仍然可以被新产生的代码所解析.相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉.注意:未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的
    • 非required的字段可以移除,只要它们的标识号在新的消息类型中不再使用,但是更好的做法是将此字段重命名,保证其标识号不被其他变量误用
    • nt32, uint32, int64, uint64,和bool是全部兼容的,如果转换的时候发生不符合的情况,那么会强制类型转换
    • sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容
    • string和bytes是兼容的,只要bytes是有效的UTF-8编码
    • 嵌套消息与bytes是兼容的,只要bytes包含该消息的一个编码过的版本
    • fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的
    • 枚举类型与int32,uint32,int64和uint64相兼容,,如果转换的时候发生不符合的情况,那么会强制类型转换

Any

  • Any允许包装任意的message类型 ,一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型.为了使用Any类型,你需要导入import google/protobuf/any.proto

    import "google/protobuf/any.proto";
    
    message Response {
        google.protobuf.Any data = 1;
    }
  • 如上会被解析成private java.util.List<com.google.protobuf.Any> details_; 类型
  • 对于给定的消息类型的默认类型URL是type.googleapis.com/packagename.messagename
  • 不同语言的实现会支持动态库以线程安全的方式去帮助封装或者解封装Any值,例如在java中,Any类型会有特殊的pack()unpack()访问器
  • 实例

    //People.proto
    syntax = "proto3";
    message Get{
        string name = 1;
    }
    //First.proto
    syntax = "proto3";
    import "google/protobuf/any.proto";
    
    message Response {
        google.protobuf.Any data = 1;
    }
    public class Tests {
        public static void main(String[] args) throws InvalidProtocolBufferException {
            People.Get wangziqiang = People.Get.newBuilder().setName("wangziqiang").build();
            First.Response build = First.Response.newBuilder().setData(Any.pack(wangziqiang)).build();
            System.out.println(build.getData());
            System.out.println(build.getData().unpack(People.Get.class).getName());
    
        }
        /**
         * 输出结果
         * type_url: "type.googleapis.com/Get"
         * value: "\n\vwangziqiang"
         * wangziqiang
         */
    }
  • 如上我们就看到了,Any可以包含任何的message,也就是进行pack动作,而unpack是进行解包得到内容,这是我自己的理解

Oneof

  • 用于几个参数只能初始化其中一个的情况,设置其中一个字段会清除其它字段,repeated不可以在其中被使用

    syntax = "proto3";  
    message Name{
        oneof set_name{
            string first_name = 1;
            string last_name = 2;
        }
    }
    public class Tests {
        public static void main(String[] args) {
            People.Name wangziqiang = People.Name.newBuilder().setFirstName("nn").setLastName("wangziqiang").build();
            System.out.println("getFirstName -> " + wangziqiang.getFirstName());
            System.out.println("getLastName -> " + wangziqiang.getLastName());
            System.out.println(wangziqiang.getFirstName().isEmpty());
            System.out.println(wangziqiang.getLastName().isEmpty());
        }
        /**
         * getFirstName -> wangziqiang
         * getLastName ->
         * true
         * false
         * 也就可以看出来,只有最后一次设置的字段有值.
         */
    }

Map

  • 其实是一种语法糖,如下

    message xx{
      int32 i = 1;
      string j = 2;
    }
    message ss {
      xx x = 1;  //map<int32,string> = 1
    }
  • 需要注意的是key除了floating和bytes的任意标量类型都是可以的,value可以是任意类型

    • map的顺序不一定
    • map类型字段不支持repeated
    • 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序
    • 如果有重复的key则后一个key不会被使用
  • 实例

    syntax = "proto3";
    message Name{
         map<int32,string> names = 1;
    }
    public class Tests {
        public static void main(String[] args) throws InvalidProtocolBufferException {
            People.Name.Builder builder = People.Name.newBuilder();
            builder.putNames(1,"one");
            builder.putNames(3,"three");
            builder.putNames(2,"two");
            People.Name name = builder.build();
            System.out.println(name);
        }
    
        /**
         * names {
         *   key: 1
         *   value: "one"
         * }
         * names {
         *   key: 3
         *   value: "three"
         * }
         * names {
         *   key: 2
         *   value: "two"
         * }
         */
    }

  • 使用方法

    syntax = "proto3";
    package qidai;
    message Name{
         map<int32,string> names = 1;
    }

定义服务

  • 如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码

    syntax = "proto3";
    package qidai;
    message NameRequest{
         string name = 1;
    }
    message NameResponse{
         int32 name_hash = 1;
    }
    service rpc_name {
         rpc getName(NameRequest) returns(NameResponse);
    }
  • 上面的表示,rpc的参数为NameRequest,而返回值为NameResponse,对于java应用protobuf实现简单的rpc,之后再说

JSON映射

  • 在protobuf中支持使用JSON,那么对于protobuf中的类型和JSON中的类型的对应表如下
proto3 JSON JSON示例 注意
message object {"fBar": v, "g": null, …} 产生JSON对象,消息字段名可以被映射成lowerCamelCase形式,并且成为JSON对象键,null被接受并成为对应字段的默认值
enum string "FOO_BAR" 枚举值的名字在proto文件中被指定
map object {"k": v, …} 所有的键都被转换成string
repeated V array [v, …] null被视为空列表
bool true, false true, false
string string "Hello World!"
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+"
int32,fixed32,uint32 number 1, -10, 0 JSON值会是一个十进制数,数值型或者string类型都会接受
int64,fixed64,uint64 string "1", "-10" JSON值会是一个十进制数,数值型或者string类型都会接受
float,double number 1.1, -10.0, 0, "NaN", "Infinity" JSON值会是一个数字或者一个指定的字符串如”NaN”,”infinity”或者”-Infinity”,数值型或者字符串都是可接受的,指数符号也可以接受
Any object {"@type": "url", "f": v, … } 如果一个Any保留一个特上述的JSON映射,则它会转换成一个如下形式:{"@type": xxx, "value": yyy}否则,该值会被转换成一个JSON对象,@type字段会被插入所指定的确定的值
Timestamp string "1972-01-01T10:00:20.021Z" 使用RFC 339,其中生成的输出将始终是Z-归一化啊的,并且使用0,3,6或者9位小数
Duration string "1.000340012s", "1s" 生成的输出总是0,3,6或者9位小数,具体依赖于所需要的精度,接受所有可以转换为纳秒级的精度
Struct object { … } 任意的JSON对象,见struct.proto
Wrapper types various types 2, "2", "foo", true, "true", null, 0, ... 包装器在JSON中的表示方式类似于基本类型,但是允许nulll,并且在转换的过程中保留null
FieldMask string "f.fooBar,h" 见fieldmask.proto
ListValue array [foo, bar, …]
Value value 任意JSON值
NullValue null JSON null
  • 下面也是对照表

markdown_img_paste_20190106172122957

选项

  • 在定义.proto文件时能够标注一系列的options.Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式.完整的可用选项可以在google/protobuf/descriptor.proto找到,在IDEA中直接搜索descriptor.proto就行
  • 一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中.一些选项是消息级别的,意味着它可以用在消息定义的内部.当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中
  • 常用的选择

    • java_package:文件选项,即指定包名,如下,但是自己感觉好像与package com.qidai;一样的作用

      syntax = "proto3";
      option java_package = "com.qidai";
      message NameRequest{
           string name = 1;
      }
    • java_outer_classname:文件选项,即指定外部类类名,如果不指定,那么默认的类名是以proto文件的名字作为类名的

      syntax = "proto3";
      option java_outer_classname = "Peoples";  //然后编译完成后就是生成的Peoples的类名,而不是以proto文件名作为类名了
      message NameRequest{
           string name = 1;
      }
    • optimize_for:文件选项,可以被设置为 SPEED, CODE_SIZE,或者LITE_RUNTIME,这些值将影响代码的生成

      • SPEED:默认protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作.这种代码是最优的.
      • CODE_SIZE:protocol buffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作.采用该方式产生的代码将比SPEED要少得多, 但是操作要相对慢些,但是生成的代码的访问方式和SPEED是一样的
      • LITE_RUNTIMEprotocol buffer编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite 替代libprotobuf).这种核心类库由于忽略了一 些描述符及反射,要比全类库小得多.这种模式经常在移动手机平台应用多一些
      syntax = "proto3";
      option optimize_for = SPEED;
      message NameRequest{
           string name = 1;
      }
    • deprecated:表示废弃了

      syntax = "proto3";
      message NameRequest{
           string name = 1 [deprecated = true];
      }

生成访问类

  • 如上我们都是在IDEA插件的帮助下,自动完成编译的,自己接触的是Java,所以在这里列一下Java的生成类的方式,如果你是别的语言,那么请参考给出的参考博文

    protoc --proto_path=IMPORT_PATH --java_out=DST_DIR path/to/file.proto
    • IMPORT_PATH声明了一个.proto文件所在的解析import具体目录,如果忽略该值,则使用当前目录,如果有多个目录则可以多次调用--proto_path,-I=IMPORT_PATH--proto_path的简化形式
    • --java_out 在目标目录DST_DIR中产生Java代码
    • 作为一个方便的拓展,如果DST_DIR以.zip或者.jar结尾,编译器会将输出写到一个ZIP格式文件或者符合JAR标准的.jar文件中.注意如果输出已经存在则会被覆盖
  • 好了到这就是Protobuf的基本用法,本文参考网上的博文,链接如下,如果本文有什么错误请及时指出 , 谢谢咯

开发学院
简书
CSDN

网友评论

登录后评论
0/500
评论
期待l
+ 关注