Protobuf 语法


Message 定义


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

例如,定义一个搜索请求的消息格式 SearchRequest ,每个请求包含查询字符串、页码、每页数目,每个字段声明以分号结尾,具体内容如下所示:

syntax = "proto3";
message SearchRequest {
    string query = 1;
    int32  page_number = 2;
    int32  result_per_page = 3;
}

首行要求明确声明使用的 protobuf 版本proto3 ,如果不声明,编译器默认使用 proto2 ,该声明必须在文件的首行;一个 .proto 文件中可以定义多个 message ,一般用于同时定义多个相关的 message 。

例如,在同一个 .proto 文件中同时定义搜索请求和响应消息,具体内容如下:

syntax = "proto3";
message SearchRequest {
    string query = 1;
    int32  page_number = 2;
    int32  result_per_page = 3;
}
message SearchResponse {
    ...
}

字段类型声明


所有的字段需要前置声明数据类型,上面的示例指定了两个数值类型和一个字符串类型,除了基本的标量类型还有复合类型(如枚举、map、数组、甚至其它 message 类型等)。


分配 Tags


消息的定义中,每个字段都有一个唯一的数值标签。这些标签用于标识该字段在消息中的二进制格式,使用中的类型不应该随意改动。[1-15] 内的标识在编码时占用一个字节,包含标识和字段类型;[16-2047] 之间的标识符占用 2 个字节。

最小的标识符可以从 1 开始,最大到 229 - 1 或 536、870、911,不能使用 [19000-19999] 之间的标识符, Protobuf 协议实现中预留了这些标识符,在 .proto 文件中使用这些预留标识号,编译时会报错。


字段规则


(1)单数形态:一个 message 内同名单数形态的字段不能超过一个;

(2)repeated :前置 repeated 关键词,声明该字段为数组类型;

(3)proto3 不支持 proto2 中的 required 和 optional 关键字。


添加注释


.proto 文件中添加注释,支持 C/C++ 风格双斜线 // 单行注释,如以下的形式:

syntax = "proto3";              // 协议版本声明
// SearchRequest 搜索请求消息
message SearchRequest {
    string query = 1;           // 查询字符串
    int32  page_number = 2;     // 页码
    int32  result_per_page = 3; // 每页条数
}

保留字段名与Tag


使用 reserved 关键字可以指定保留字段名和标签,如以下的形式:

message Foo {
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
}

注:不能在一个 reserved 声明中混合字段名和标签。


proto 文件编译结果


当使用 protocol buffer 编译器运行 xxx.proto 文件时,编译器将生成所选语言的代码,用于在 xx.proto 文件中定义的消息类型、服务接口约定等,不同语言生成的代码格式不同:

  • C++:每个 proto 文件生成一个 .h 文件和一个 .cc 文件,每个消息类型对应一个类;

  • Java:生成一个 .java 文件,同样每个消息对应一个类,同时还有一个特殊的 Builder 类用于创建消息接口;

  • Python:每个 proto 文件中的消息类型生成一个含有静态描述符的模块,该模块与一个元类 metaclass 在运行时创建需要的 Python 数据访问类;

  • Go: 生成一个 .pb.go 文件,每个消息类型对应一个结构体。

各种语言的更多的使用方法参考 官方API文档


基本数据类型


ProtoBuf 支持的数据类型对应不同语言的数据类型,具体内容如下所示:

.protoC++JavaPythonGo
doubledoubledoublefloatfloat64
floatfloatfloatfloatfloat32
int32int32intintint32
int64int64longing/longint64
uint32uint32int[1]int/long[3]uint32
uint64uint64long[1]int/long[3]uint64
sint32int32intintjint32
sint64int64longint/long[3]int64
fixed32uint32int[1]intuint32
fixed64uint64long[1]int/long[3]uint64
sfixed32int32intintint32
sfixed64int64longint/long[3]int64
boolboolbooleanbooleanbool
stringstringStringstr/unicode[4]string
bytesstringByteStringstr[]byte

关于这些类型在序列化时的编码规则请参考 Protocol Buffer Encoding


默认值


  • 字符串类型默认为空字符串;

  • 字节类型默认为空字节;

  • 布尔类型默认 false ;

  • 数值类型默认为 0 值;

  • enums 类型默认为第一个定义的枚举值,必须是 0 ;

针对不同语言的默认值的具体行为参考 generated code guide


枚举(Enum)


当定义一个 message 时,若一个字段只能是一个预定义好的值列表内的一个值,就需要用到 enum 类型了。

例如这里定义一个名为 Corpus 的 enum 类型值,并且指定一个字段为 Corpus 类型,具体形式如下:

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  // 定义enum类型
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4; // 使用Corpus作为字段类型
}

注:每个 enum 定义的第一个元素值必须是 0 。

还可以通过给不同的 enum 元素赋相同的值来定义别名,要求设置 allow_alias 选项的值为 true ,否则会报错,例如如下的形式:

// 正确示例
enum EnumAllowingAlias {
  option allow_alias = true; // 开启allow_alias选项
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;  // RUNNING和STARTED互为别名
}
// 错误示例
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;  // 未开启allow_alias选项,编译会报错
}

enum 类型值同样支持文件级定义和 message 内定义,引用方式与 message 嵌套一致。


使用其它 Message


可以使用其它 message 类型作为字段类型。例如,在 SearchResponse 中包含 Result 类型的消息,可以在相同的 .proto 文件中定义 Result 消息类型,然后在 SearchResponse 中指定字段类型为 Result ,具体参考如下的形式:

message SearchResponse {
    repeated Result results = 1;
}
message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
}

导入定义(import)


上面的例子中,Result 类型和 SearchResponse 类型定义在同一个 .proto 文件中,还可以使用 import 语句导入使用其它描述文件中声明的类型,具体参考如下的形式:

import "others.proto";

默认情况,只能使用直接导入的 .proto 文件内的定义,但有时需要移动 .proto 文件到其它位置,为了避免更新所有相关文件,可以在原位置放置一个模型 .proto 文件,使用 public 关键字,转发所有对新文件内容的引用,例如以下的方式:

// new.proto
// 所有新的定义在这里
// old.proto
// 客户端导入的原来的proto文件
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// 这里可以使用old.proto和new.proto文件中的定义,但是不能使用other.proto文件中的定义

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


Message 嵌套


上面已经介绍过可以嵌套使用另一个 message 作为字段类型,其实还可以在一个 message 内部定义另一个 message 类型,作为子级 message ,例如 Result 类型在 SearchResponse 类型中定义并直接使用,作为 results 字段的类型,具体的内容参考如下:

message SearchResponse {
    message Result {
        string url = 1;
        string title = 2;
        repeated string snippets = 3;
    }
    repeated Result results = 1;
}

内部声明的 message 类型名称只可在内部直接使用,在外部引用需要前置父级 message 名称,如 Parent.Type :

message SomeOtherMessage {
    SearchResponse.Result result = 1;
}

支持多层嵌套,具体参考如下的形式:

message Outer {                // Level 0
    message MiddleAA {         // Level 1
        message Inner {        // Level 2
            int64 ival = 1;
            bool  booly = 2;
        }
    }
    message MiddleBB {         // Level 1
        message Inner {        // Level 2
            int32 ival = 1;
            bool  booly = 2;
        }
    }
}

Map 类型


proto3 支持 map 类型声明,具体的使用参考如下:

map<key_type, value_type> map_field = N;
message Project {...}
map<string, Project> projects = 1;
  • key_type 类型可以是内置的标量类型(除浮点类型和 bytes );

  • value_type 可以是除 map 以外的任意类型;

  • map 字段不支持 repeated 属性;

  • 不要依赖 map 类型的字段顺序。


包(Packages)

.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 选项。


定义服务(Service)


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

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

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

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


选项(Options)


在定义 .proto 文件时可以标注一系列的 options ,Options 并不改变整个文件声明的含义,但却可以影响特定环境下处理方式。完整的可用选项可以查看google/protobuf/descriptor.proto ,一些选项是文件级别的,意味着它可以作用于顶层作用域,不包含在任何消息内部、enum 或服务定义中;一些选项是消息级别的,可以用在消息定义的内部。


基本规范


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

  • 结构定义包括:message、service、enum ;

  • RPC 方法定义结尾的分号可有可无;

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

  • Enums 类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式;

  • Service 名称与 RPC 方法名统一采用驼峰式命名。


编译


通过定义好的 .proto 文件生成 Java、Python、C++、Go 等代码,需安装编译器 Protocol Buffers v3 ,安装方法依次执行以下命令:

wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protobuf-all-21.12.zip
unzip protobuf-all-21.12.zip
cd protobuf-21.12/
./configure
make
make install

检查是否安装成功,输入如下命令:

protoc --version

若出现以下错误,执行 ldconfig 命命令。

protoc: error while loading shared libraries: libprotobuf.so.15: cannot open shared object file: No such file or directory

编译文件,执行以下命令:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

示例


编写如下 proto 文件并进行相应的说明:

syntax = "proto3"; ➊

package ecommerce; ➋

service ProductInfo { ➌

rpc addProduct(Product) returns (ProductID); ➍

rpc getProduct(ProductID) returns (Product); ➎

}

message Product { ➏

string id = 1; ➐

string name = 2;

string description = 3;

}

message ProductID { ➑

string value = 1;

}

❶ 服务定义首先声明所使用的 protocol buffers 版本(proto3)。

❷ 用来防止协议消息类型之间发生命名冲突的包名,该包名也会用来生成代码。

❸ 定义 gRPC 服务的接口。

❹ 添加商品的远程方法,该方法会返回商品 ID 作为响应。

❺ 基于商品 ID 获取商品的远程方法。

❻ 定义 Product 的消息格式或类型。

❼ 保存商品 ID 的字段(名–值对),具有唯一的字段编号,该编号用来在二进制格式消息中识别字段。

❽ 用于商品标识号的用户定义类型。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

물の韜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值