1、基本规范

  • 文件以.proto做为文件后缀,除结构定义外的语句以分号结尾

  • 结构定义可以包含:messageserviceenum

  • rpc方法定义结尾的分号可有可无。

  • Message命名采用驼峰命名方式,字段 命名 采用 小写字母下划线 分隔方式。

    message SongServerRequest {
      required string song_name = 1;
    }
  • Enum类型名采用 驼峰命名 方式,字段 命名采用 大写字母下划线 分隔方式

    enum Foo {
      FIRST_VALUE = 1;
      SECOND_VALUE = 2;
    }
  • Servicerpc方法 名统一采用 驼峰式 命名。

    service SearchService {
      rpc Search (SearchRequest) returns (SearchResponse) {}
    }

2、字段规则

字段格式:

限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值]

2.1 限定修饰符

包含 required \ optional \ repeated

  • Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。

    发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。

  • Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。

    因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。

  • Repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值

2.2 数据类型

  • Protobuf定义了一套基本数据类型。几乎都可以映射到C++\Java等语言的基础数据类型

  • N 表示打包的字节并不是固定。而是根据数据的大小或者长度

  • 关于 fixed32 和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高。

2.3 字段名称

  • 字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的

  • protobuf 建议字段的命名采用以下划线分割的驼峰式。例如 first_name 而不是firstName

2.4 字段编码值

  • 有了该值,通信双方才能互相识别对方的字段,相同的编码值,其限定修饰符和数据类型必须相同,编码值的取值范围为 1~2^32(4294967296)

  • 其中 1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低,所以建议经常要传递的值其字段编码设置为1-15之间的值

  • 1900~2000编码值为Google protobuf 系统内部保留值,建议不要在自己的项目中使用。

2.5 字段默认值

  • 当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端

3、各类型具体定义示例

3.1 Message如何定义

一个message类型定义描述了 一个请求 或 响应 的消息格式,可以包含多种类型字段

例如定义一个搜索请求的消息格式,每个请求包含查询 字符串、页码、每页数目

syntax = "proto3";

message SearchRequest {
    string key = 1;
    int64 page = 2;
    int64 page_size = 3;
}
  • 字段名用小写,转为go文件后自动变为大写,message就相当于结构体。

  • 首行声明使用的protobuf版本为proto3。

  • SearchRequest 定义了三个字段,每个字段声明以分号结尾,.proto文件支持双斜线 // 添加单行注释。

  • 一个.proto文件中可以定义多个消息类型,一般用于同时定义多个相关的消息。

例如在同一个.proto文件中同时定义搜索请求和响应消息。

syntax = "proto3";

message SearchRequest {
    string key = 1;
    int64 page = 2;
    int64 page_size = 3;
}

message SearchResponse {
    ......
}
  • message支持嵌套使用,作为另一message中的字段类型。
syntax = "proto3";

message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;     // 表示字符串数组
}


message SearchResponse {
    repeated Result result = 1;      // 表示Result数组
}
  • 嵌套消息,可以包含另一个消息作为其字段,也可以在消息内定义一个新的消息。
    内部声明的message类型名称只可在内部直接使用

    message SearchResponse {
      message Result {                   // 转为go结构体时,名称为 SearchResponse_Result
          string url = 1;
          string title = 2;
          repeated string snippets = 3;     // 表示字符串数组
      }
      repeated Result result = 1;           // 表示Result数组
    }
  • 支持多层嵌套。
    这里只定义了,未在嵌套消息中引用。

    message Outer {                // Level 0
      message MiddleAA {         // Level 1        // 转为go结构体时,名称为 Outer_MiddleAA
          message Inner {        // Level 2        // 转为go结构体时,名称为 Outer_MiddleAA_Inner
              int64 ival = 1;
              bool  booly = 2;
          }
      }
      message MiddleBB {          // Level 1       // 转为go结构体时,名称为 Outer_MiddleBB
          message Inner {         // Level 2       // 转为go结构体时,名称为 Outer_MiddleBB_Inner
              int32 ival = 1;
              bool  booly = 2;
          }
      }
    }

3.2 enum如何定义

当希望字段 仅 具有预定义的值列表之一时,可以定义 enum 类型消息;

例如,假设想添加一个 corpus 字段每个 SearchRequest,其中语料库可以 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或 VIDEO。

enum Corpus {
    CORPUS_UNIVERSAL = 0;
    CORPUS_WEB = 1;
    CORPUS_IMAGES = 2;
    CORPUS_LOCAL = 3;
    CORPUS_NEWS = 4;
    CORPUS_PRODUCTS = 5;
    CORPUS_VIDEO = 6;
}


message SearchRequest {
    string key = 1;
    int64 page = 2;
    int64 page_size = 3;
    Corpus corpus = 4;    // message 嵌套定义的 enum 类型 作为一个字段
}

3.3 service如何定义

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

例如,想要定义一个RPC服务并具有一个方法,该方法接收SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

service SearchService {
    rpc Search (SearchRequest) returns (SearchResponse) {}
}

生成的接口代码作为 客户端 与 服务端 的约定,服务端 必须实现定义的所有接口方法,客户端 直接调用同名方法向服务端发起请求。

比较麻烦的是,即便业务上不需要参数也必须指定一个请求消息,一般会定义一个 空 message

3.4 proto3的Map类型

proto3支持map类型声明

    map<key_type, value_type> map_field = N;

    message Project {...}
    map<string, Project> projects = 1;
  • 键、值类型可以是内置的类型,也可以是自定义message类型

  • 字段不支持repeated属性。

其中 key_type 可以是 任何整数 或 字符串类型(因此,除了浮点类型和bytes之外的任何标量类型)。

请注意, enum 不是有效的key_type。

value_type可以是任何类型的,除了map类型。

3.4.1 向后兼容

map 语法等效于以下内容,因此不支持 map的protocol buffers实现仍然可以处理数据:

message MapFieldEntry {
    key_type key = 1;
    value_type value = 2;
}
repeated MapFieldEntry map_field = N;

任何支持映射的protocol buffers实现都必须生成和接受上述定义可以接受的数据。

4、import导入定义

  • 可以使用import语句导入使用其它描述文件中声明的类型。

  • protobuf 接口文件可以像C语言的h文件一个,分离为多个,在需要的时候通过 import 导入需要对文件。其行为和C语言的#include或者java的import的行为大致相同,例如import "others.proto";

  • protocol buffer编译器会在 -I / --proto_path参数指定的目录中查找导入的文件,如果没有指定该参数,默认在当前目录中查找。

例如:上面的示例中,Result消息类型是在同一个文件中定义的SearchResponse——如果想用作字段类型的消息类型已经在另一个.proto文件中定义了怎么办?

.proto可以通过导入其他文件中的定义来使用它们。要导入 another.proto的定义,请在文件顶部添加一个 import 语句:

import "myproject/other_protos.proto";

公共导入功能

默认情况下,您只能使用直接导入的定义 .proto文件。 但是,有时您可能需要移动一个 .proto文件到新位置。

而不是移动 .proto直接归档并更新所有 引用的文件 单个更改,您可以放置一个占位符 .proto文件在旧位置 使用 import public概念。

公共导入功能在 Java 中不可用。

import public依赖可以被任何代码传递依赖 导入包含的原型 import public 声明。

示例:

// new.proto
// All definitions are moved here  新的定义文件,所有定义都移动到这里
// old.proto
// This is the proto that all clients are importing.     // 这个文件还是被所有  客户端 导入
import public "new.proto";                         // 设置公共导入,传递依赖
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
// 这里你可以 通过 old.proto 使用 new.proto 中声明的类型,但是不能 使用  other.proto 中的类型

5、包的使用

  • 在.proto文件中使用package声明包名,避免命名冲突。
syntax = "proto3";
package foo.bar;
message Open {...}
  • 在其他的消息格式定义中可以使用包名+消息名的方式来使用类型,如:
message Foo {
    ...
    foo.bar.Open open = 1;          // 使用包名+消息名  来使用定义的类型
    ...
}
  • 在不同的语言中,包名定义对编译后生成的代码的影响不同。

    • C++ 中:对应C++命名空间,例如Open会在命名空间foo::bar
    • Java 中:package会作为Java包名,除非指定了option jave_package选项
    • Python 中:package被忽略
    • Go 中:默认使用package名作为包名,除非指定了option go_package选项
    • JavaNano 中:同Java
    • C# 中:package会转换为驼峰式命名空间,如Foo.Bar,除非指定了option csharp_namespace选项
作者:joker.liu  创建时间:2023-05-18 17:45
最后编辑:joker.liu  更新时间:2023-05-22 11:02