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

1. 简介

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

它有以下特点:

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

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

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

2. 二进制与文本协议

2.1 二进制协议

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

二进制协议就是一串字节流,通常包括消息头header)和消息体body),消息头的长度固定,并且消息头包括了消息体的长度。这样就能够从数据流中解析出一个完整的二进制数据。如下是一个典型的二进制协议结构模型:
2
其中,

  • 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 定义的数据结构与编译后的数据结构对比:

TypeC++ TypeJava TypeNotes
doubledoubledouble
floatfloatfloat
int32int32int使用可变长编码方式。编码负数时不够高效 —— 如果你的字段可能含有负数,那么请使用 sint32
int64int64long使用可变长编码方式。编码负数时不够高效 —— 如果你的字段可能含有负数,那么请使用 sint64
uint32intuint32
uint64longuint64
sint32intuint32使用可变长编码方式。有符号的整型值。编码时比通常的 int32 高效
sint64longuint64使用可变长编码方式。有符号的整型值。编码时比通常的 int64 高效
fixed32intuint32总是 4 个字节。如果数值总是比总是比 2 的 28 次幂大的话,这个类型会比 uint32 高效
fixed64longuint64总是 8 个字节。如果数值总是比总是比 2 的 56 次幂大的话,这个类型会比 uint64 高效
sfixed32intint32总是 4 个字节
sfixed64longint64总是 8 个字节
boolbooleanbool
stringStringstring一个字符串必须是 UTF-8 编码或者 7-bit ASCII 编码的文本
bytesByteStringstring可以包含任意字节序列

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值