gRPC 笔记(02)— Google Protocol Buffer 协议语法规则

1. 简介

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

它有以下特点:

  • 跨语言、支持多种语言,包括 C++JavaPython 等;
  • 编码后的消息更小,更加有利于存储和传输;
  • 编解码的性能非常高;
  • 支持不同协议版本的前向兼容;
  • Google 开源的二进制 RPC 通讯协议;

Protobuf 并没有定义消息边界,也就是没有消息头。消息头一般由用户自己定义,通常使用长度前缀法来定义边界。

同样 Protobuf 也没有定义消息类型,当服务器收到一串消息时,它必须知道对应的类型,然后选择相应消息类型的解码器来读取消息。这个类型信息也必须由用户自己在消息头里面定义。

2. 二进制与文本协议

2.1 二进制协议

网络协议是计算机网络中进行数据交换而建立的规则、标准或约定的集合。而二进制协议是在进行网络传输时,传输的并不是类似 JSON 这样的文本文件,而是类似 BSON 这样的二进制数据。

二进制协议就是一串字节流,通常包括消息头header)和消息体body),消息头的长度固定,并且消息头包括了消息体的长度。这样就能够从数据流中解析出一个完整的二进制数据。如下是一个典型的二进制协议结构模型:
gRPC 笔记(02)— Google Protocol Buffer 协议语法规则_第1张图片
其中,

  • Guide 用于标识协议起始;
  • Length 是消息体 Data 的长度,为了数据完整性,还会加上相应的校验(DataCRCHeaderCRC);
  • Data 中又分为命令字(CMD) 和命令内容。命令字是双方协议文档中规定好的,比如 0x01 代表登录,0x02 代表登出等,一般数据字段的长度也是固定的。又因为长度的固定,所以少了冗余数据,传输效率较高。

优点

  • 空间占用小,没有冗余字段;
  • 运算规则简单,解析效率高。

缺点

  • 可读性差,难于调试;
  • 扩展性差,对于旧版本不太容易兼容新字段。

2.2 文本协议

文本协议一般是由一串 ACSII 字符组成的数据,这些字符包括数字、大小写字母、百分号、回车(\r)、换行(\n)以及空格等等。

文本协议设计的目的就是方便人们理解、读懂。所以,协议中通常会加入一些特殊字符用于分隔,比如如下数据:

git commit -m "fix: 修改BUG"

这是一条文本指令,尽管没有说明,我们也能够直观的看出,这 git 里面的 commit 指令,-m 后面跟随的是当前提交的描述信息。

但为了便于解析,文本协议不得不添加一些冗余的字符用于分隔命令,降低了其传输的效率;而且只适于传输文本,很难嵌入其他数据,比如一张图片、一段音频等。

优点

  • 可读性高,易于调试;
  • 扩展性好,可以很好的扩展新字段,兼容老版本;

缺点

  • 空间占用大,存在大量冗余字段;
  • 运算规则复杂,解析效率比较低;

2.3 二进制与文本协议优劣

我们大致总结文本协议和二进制协议的优缺点:

  • 文本协议,直观、描述性强,容易理解,便于调试,缺点就是冗余数据较多,不适宜传输二进制文件,解析复杂;
  • 二进制协议,没有冗余字段,传输高效,方便解析,缺点就是定义得比较死,哪个位置有哪些东西,是什么意义是定义死的,场景单一且不易扩展。

所以:

  1. 如果想要高效传输,比如一些传感器数据的收集、密集指令的传输,使用二进制协议;
  2. 如果想要便于调试,比如 RPC 接口的调用等,可使用文本协议;
  3. 如果命令较少,比如即时通讯软件,可以使用文本协议;
  4. 二进制数据的难理解性,自然加密,对数据安全有一定要求的可以使用二进制协议;

3. 语法规则

Protobuf 源码已在 GitHub 上开源:https://github.com/protocolbuffers/protobuf

官方文档地址为:https://developers.google.com/protocol-buffers/

以下示例默认使用 proto3 库编写。

Protobuf 传输的是一系列的键值对,如果连续的键重复了,那说明传输的值是一个列表 repeated 。图中的 key3 就是一个列表类型 repeated
协议格式
key 两部分组成:tagtype

  • tag

Protobuf 将对象中的每个字段和字段索引 tag 对应起来,字段索引就是在 proto 文件中定义消息时,为每个消息字段所设置的唯一数字。在序列化的时候用整数值来代替字段名称,于是传输流量就可以大幅缩减。

如果字段较少,它就使用 4 个 bits 来表示,最多支持 16 个字段。如果字段数量超过 16 个,那就再加 1 个字节,如果还不够那就再加 1 个字节。你也许猜到了,这个 tag 值使用的是 varint 编码。理论上字段的长度不设上限,因为 varint 可以通过扩展字节数支持任意大的非负整数。

  • type

Protobuf 将字段类型也和正数序列 type 对应起来,type 使用 3 个 bits 表示,最多支持 8 种类型。

也许你会认为 8 种类型怎么够呢?放心,肯定够的!因为一个 zigzag 类型可以表示所有的类整数类型,byte/short/int/long/bool/enum/unsigned byte/unsigned short/unsigned int/unsigned long 这些类型都可以使用 zigzag 表示。

protocol buffers 使用不同的编码技术来编码不同类型的数据。对于字符串值,protocol buffers 会使用 UTF-8 对值进行编码;对于 int32 字段类型的整型值,它会使用名为 Varint 的编码技术。

  • 整数

Protobuf 的整数数值使用 zigzag 编码。zigzag 编码支持负数值,varint 编码的都是非负数。

  • 浮点数

浮点数分为 floatdouble,它们分别使用 4 个字节和 6 个字节序列化,这两个类型的 value 没有做什么特殊处理,它就是标准的浮点数。

  • 字符串

protocol buffers 中,字符串类型属于基于长度分隔类型,这意味着首先会有一个经过 Varint 编码的长度值,随后才是指定数量的字节数据。

字符串值会使用 UTF-8 字符编码格式来进行编码。第一个字节是字符串的长度,后面相应长度的字节串就是字符串的内容。如果字符串长度很长,那么长度前缀就不止一个字节,它可能是两字节三字节四字节,你也许猜到了,这个长度采用的是 varint 编码。

3.1 字段类型

通过 .proto 文件编译对应的代码文件时,proto3 定义的数据结构与编译后的数据结构对比:

Type C++ Type Java Type Notes
double double double
float float float
int32 int32 int 使用可变长编码方式。编码负数时不够高效 —— 如果你的字段可能含有负数,那么请使用 sint32
int64 int64 long 使用可变长编码方式。编码负数时不够高效 —— 如果你的字段可能含有负数,那么请使用 sint64
uint32 int uint32
uint64 long uint64
sint32 int uint32 使用可变长编码方式。有符号的整型值。编码时比通常的 int32 高效
sint64 long uint64 使用可变长编码方式。有符号的整型值。编码时比通常的 int64 高效
fixed32 int uint32 总是 4 个字节。如果数值总是比总是比 2 的 28 次幂大的话,这个类型会比 uint32 高效
fixed64 long uint64 总是 8 个字节。如果数值总是比总是比 2 的 56 次幂大的话,这个类型会比 uint64 高效
sfixed32 int int32 总是 4 个字节
sfixed64 long int64 总是 8 个字节
bool boolean bool
string String string 一个字符串必须是 UTF-8 编码或者 7-bit ASCII 编码的文本
bytes ByteString string 可以包含任意字节序列

3.2 编码规则

  • 描述文件以 .proto 做为文件后缀;
  • message 命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式;
  • enum 类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式;
  • servicerpc 方法名统一采用驼峰式命名;

3.3 注释

单行注释

// 单行注释

多行注释

 /**
    * 多行注释
 */

3.4 默认值

解析消息时,如果编码消息不包含特定的单位元素,则解析对象中的相应字段将设置为该字段的默认值,这些默认值是特定于类型的:

  • 对于字符串类型,默认值为空字符串;
  • 对于字节类型,默认值为空字节;
  • 对于布尔类型,默认值为 false
  • 对于数字类型,默认值为零;
  • 对于枚举,默认值是第一个定义的枚举值,该值必须为 0
  • 对于消息字段,未设置该字段,它的确切值取决于语言;
  • 重复字段的默认值为空(通常是相应语言的空列表);

3.5 分配标签

在消息定义中,每个字段都有唯一的一个数字标签。这些标签是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。

注:

  • [1, 15] 之内的标签在编码的时候会占用一个字节。
  • [16, 2047] 之内的标签则占用两个字节。

所以应该为那些频繁出现的消息元素保留 [1, 15] 之内的标签。切记:要为将来有可能添加的、频繁出现的字段预留一些标签。

3.6 message 定义

.proto 文件定义消息,message.proto 文件最小的逻辑单元,由一系列 name-value 键值对构成:

syntax = "proto3"

package com.sid.demo;

message Person {
    int32 id = 1;
    string name = 2;
    int32 age = 3;
    repeated string friend = 4;
}

message 消息包含一个或多个编号唯一的字段,每个字段由“字段限制 + 字段类型 + 字段名 + 编号”组成,字段限制分为 optional(可选的,已移除)、required(必须的,已移除)、repeated(重复的)。

消息可以定义在多个文件里面,可通过 import 导入;同样,消息的定义也支持嵌套。

为了防止不同的消息之间有类名的冲突,可以使用 package 字段声明包名。

3.7 枚举

当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。 则通过向消息定义中添加一个枚举( enum)并且为每个可能的值定义一个常量就可以了。

syntax = "proto3"

message Course {
    // ...
    CourseType type = 1;
}

enum CourseType {
    CHINESE = 0;
    MATH = 1;
    ENGLISH = 2;
}

3.8 Any(任意消息类型)

Any 类型是一种不需要在 .proto 文件中定义就可以直接使用的消息类型,使用前需要导入文件:

syntax = "proto3"

import google/protobuf/any.proto

message Request {
    string userId = 1;
    google.protobuf.Any data = 2;
    int64 timestamp = 3;
}

3.9 服务

如果想在 RPC 系统中使用消息类型,就需要在 .proto 文件中定义 RPC 服务接口,然后使用编译器生成对应语言的存根。

syntax = "proto3"

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

3.10 保留标识符

如果在更新字段的时候为了重用标签,我们手动删除或者注释了旧版本的标签,那么当使用旧版本加载新的 .proto 文件时会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是指定保留标签,Protocol Buffer 的编译器会警告未来尝试使用这些域标签的用户。

syntax = "proto3"

message Person {
  reserved 2, 3, 4 to 8; // 标识号
  reserved "name", "age"; // 字段名
}

3.11 更新协议

如果一个现在的消息不再能满足你的需求,比如要增加新的字段,但是你仍然希望兼容旧版本生成的消息的话,你需要安装以下规则更新消息,否则会出现兼容性问题(包括 proto2 的部分语法功能):

  • 不要改变任何已有的数字标签;
  • 你新添加的字段需要是 optional 或者 repeated
  • required 字段可以被移除,但是对应的数字标签不能被重用;
  • 只要保证数字标签一致,一个非 required 字段可以被转化为扩展字段,反之亦然;
  • int32uint32int64uint64bool 这些类型是相互兼容的;
  • sint32sint64 相互兼容,但是不和其他数字类型兼容;
  • stringbytes 相互兼容,前提是二进制内容是有效的 UTF-8
  • optionalrepeated 相互兼容,即当给定的输入字段是 repeated 的时候,而如果接收方期待的是一个 optional 的字段的话,对与原始类型的字段,它会取最后一个值,对于消息类型的字段,他会将所有的输入合并起来。

3.12 proto3 与 proto2 的区别

  • 使用 proto3 语法时,第一行必须写:syntax = "proto3"
  • 字段规则移除了 requiredoptional,所有字段默认是 optional 类型的;
  • repeated 字段默认采用 packed 编码,在 proto2 中,需要明确使用 [packed=true] 来为字段指定比较紧凑的 packed 编码方式;
  • 语言增加 GoRubyJavaNano 支持;
  • 移除了 default 选项;
  • 枚举类型的第一个字段必须为 0
  • 移除了对分组的支持;
  • 移除了对扩展的支持,新增了 Any 类型;
  • 增加了 JSON 映射特性;

4. 其它参考

https://blog.csdn.net/u011518120/article/details/54604615
https://blog.csdn.net/mzpmzk/article/details/80824839

你可能感兴趣的:(RPC,protocol,buffer)