简述Protobuf V3语法

453 阅读3分钟

前言

在上一篇文章初识Protobuf及其编译工具的安装中,我们简单家介绍了如何使用protoc工具来编译.proto文件,接下来我们来介绍一下Protobuf的语法,我们在日常开发工作中如何编写正确的.proto文件来满足我们的开发需求;

语法介绍

我们以java语法来类比,创建一个java类语法如下:

class Name {
  
}

Protobuf和上面的语法类似,只不过使用message替代了class声明:

message Name {
​
}
  • 基本语法

    接下来我们定义一个简单的类型:

    syntax = "proto3";
    ​
    message Person {
      string name = 1;
      int32 age = 2;
      float height = 3;
      double weight = 4;
      bool gender = 5;
    }
    

    从上述代码Person中,我们可以大概了解到Protobuf语法:

    1. 需要声明syntax表示Protobuf语法版本,不声明默认是proto2;这个声明必须是文件的第一个非空非注释行;
    2. 通过关键字message声明一个类型;
    3. 属性声明结构为[字段类型] [字段名] = [字段编号]格式;字段编号从1开始,最大的字段数是 536,870,911;[19000~19999]范围编号不可以使用,是预留给Protocol Buffers协议实现;
  • 列表

    如果想在消息类型中声明一个列表字段,在java中使用List<>表示,那么Protobuf中如何编写呢?

    syntax = "proto3";
    ​
    message Result{
      repeated string snippets = 1;
    }
    

    我们可以通过关键字repeated来表示List列表或者数组,被repeated修饰的字段值是可以出现多次的;

  • 枚举

    在定义消息类型时,我们希望其中一个字段只能是预定义的值列表中的一个值。处理这种需求的最好方式当然是定义一个枚举类型啦:

    syntax = "proto3";
    ​
    enum Corpus {
      UNIVERSAL = 0;
      WEB = 1;
      IMAGES = 2;
      LOCAL = 3;
      NEWS = 4;
      PRODUCTS = 5;
      VIDEO = 6;
    }
    ​
    message SearchRequest {
      string query = 1;
      int32 page_number = 2;
      int32 result_per_page = 3;
      Corpus corpus = 4;
    }
    

    枚举类型使用关键字enum修饰,枚举数据需要一个常量类映射,第一个枚举映射必须是0;定义枚举类型需要遵守以下规则:

    1. 必须有一个零值,这样我们就可以使用0作为数值默认值。
    2. 零值必须是第一个元素,以便与 proto2语义兼容,其中第一个枚举值总是默认值。
  • Map键值对

    如果你想创建一个关联映射作为你数据定义的一部分,protocol buffers提供了一个方便的快捷语法:

    map<key_type, value_type> map_field = N;
    

    key_type可以是任何整型或字符串类型,枚举不可以作为有效的key_type

  • oneof类型

    我们在日常开发中可能会在一个消息中有多个字段,并且最多只能同时设置其中一个字段,那么我们就可以通过oneof来实现:

    syntax = "proto3";
    ​
    message SearchResponse {
      string code = 1;
      oneof data {
        string empty_result = 2;
        string result = 3;
      }
    }
    

    SearchResponse消息类型中,分别有三个属性,但是empty_resultresult两个属性只能二者取其一,如果同时对empty_resultresult两个属性赋值,那么最后被赋值的属性才会最终生效;

    注意:oneof 不支持repeatedmap

  • import导入定义

    如果我们需要导入其他文件中的消息类型,那么我们可以使用import关键字来处理:

    import "myproject/other.proto";
    

    如果我们需要将.proto文件移动到新的位置,为了避免修改所有引用了该文件的.proto文件,我们可以使用import public解决该问题:

    移动到新位置.protonew.proto

    // new.proto
    // All definitions are moved here
    

    我们只需要在原先的.proto文件添加一行:

    // old.proto
    // This is the proto that all clients are importing.
    import public "new.proto";
    import "other.proto";
    

    这样的话,所有之前引用该old.proto文件的还是依旧不变:

    // client.proto
    import "old.proto";
    // You use definitions from old.proto and new.proto, but not other.proto
    
  • 默认值

    在解析消息时,如果消息类型中的属性没有被赋值,那么该属性值将被赋予默认值:

    1. 对于字符串,默认值为空字符串。
    2. 对于字节,默认值为空字节。
    3. 对于布尔值,默认值为 false。
    4. 对于数值类型,默认值为零。
    5. 对于枚举,默认值是第一个定义的枚举值,该值必须为0。
  • 预留值

    在项目迭代的过程中,我们会不断地优化定义的消息类型,如果我们在迭代过程中,有一些字段被删掉了,建议我们将这些被删掉的字段或编号用reserved修饰:

    enum Foo {
      reserved 2, 15, 9 to 11, 40 to max;
      reserved "FOO", "BAR";
    }
    

    注意,不能在同一个保留语句中混合字段名和数值。

    本文正在参加「金石计划」