1. 简介
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。 ? 它有以下特点:
- 跨语言、支持多种语言,包括
C++ 、Java 和 Python 等; - 编码后的消息更小,更加有利于存储和传输;
- 编解码的性能非常高;
- 支持不同协议版本的前向兼容;
Google 开源的二进制 RPC 通讯协议;
Protobuf 并没有定义消息边界,也就是没有消息头。消息头一般由用户自己定义,通常使用长度前缀法来定义边界。
同样 Protobuf 也没有定义消息类型,当服务器收到一串消息时,它必须知道对应的类型,然后选择相应消息类型的解码器来读取消息。这个类型信息也必须由用户自己在消息头里面定义。
2. 二进制与文本协议
2.1 二进制协议
网络协议是计算机网络中进行数据交换而建立的规则、标准或约定的集合。而二进制协议是在进行网络传输时,传输的并不是类似 JSON 这样的文本文件,而是类似 BSON 这样的二进制数据。 ? 二进制协议就是一串字节流,通常包括消息头(header )和消息体(body ),消息头的长度固定,并且消息头包括了消息体的长度。这样就能够从数据流中解析出一个完整的二进制数据。如下是一个典型的二进制协议结构模型: ? 其中,
Guide 用于标识协议起始;Length 是消息体 Data 的长度,为了数据完整性,还会加上相应的校验(DataCRC 、HeaderCRC );Data 中又分为命令字(CMD ) 和命令内容。命令字是双方协议文档中规定好的,比如 0x01 代表登录,0x02 代表登出等,一般数据字段的长度也是固定的。又因为长度的固定,所以少了冗余数据,传输效率较高。
优点
- 空间占用小,没有冗余字段;
- 运算规则简单,解析效率高。
缺点
- 可读性差,难于调试;
- 扩展性差,对于旧版本不太容易兼容新字段。
2.2 文本协议
文本协议一般是由一串 ACSII 字符组成的数据,这些字符包括数字、大小写字母、百分号、回车(\r )、换行(\n )以及空格等等。 ? 文本协议设计的目的就是方便人们理解、读懂。所以,协议中通常会加入一些特殊字符用于分隔,比如如下数据:
git commit -m "fix: 修改BUG"
这是一条文本指令,尽管没有说明,我们也能够直观的看出,这 git 里面的 commit 指令,-m 后面跟随的是当前提交的描述信息。 ? 但为了便于解析,文本协议不得不添加一些冗余的字符用于分隔命令,降低了其传输的效率;而且只适于传输文本,很难嵌入其他数据,比如一张图片、一段音频等。 ? 优点
- 可读性高,易于调试;
- 扩展性好,可以很好的扩展新字段,兼容老版本;
缺点
- 空间占用大,存在大量冗余字段;
- 运算规则复杂,解析效率比较低;
2.3 二进制与文本协议优劣
我们大致总结文本协议和二进制协议的优缺点:
- 文本协议,直观、描述性强,容易理解,便于调试,缺点就是冗余数据较多,不适宜传输二进制文件,解析复杂;
- 二进制协议,没有冗余字段,传输高效,方便解析,缺点就是定义得比较死,哪个位置有哪些东西,是什么意义是定义死的,场景单一且不易扩展。
所以:
- 如果想要高效传输,比如一些传感器数据的收集、密集指令的传输,使用二进制协议;
- 如果想要便于调试,比如
RPC 接口的调用等,可使用文本协议; - 如果命令较少,比如即时通讯软件,可以使用文本协议;
- 二进制数据的难理解性,自然加密,对数据安全有一定要求的可以使用二进制协议;
3. 语法规则
Protobuf 源码已在 GitHub 上开源:https://github.com/protocolbuffers/protobuf
官方文档地址为:https://developers.google.com/protocol-buffers/
以下示例默认使用 proto3 库编写。 ? Protobuf 传输的是一系列的键值对,如果连续的键重复了,那说明传输的值是一个列表 repeated 。图中的 key3 就是一个列表类型 repeated 。 键 key 两部分组成:tag 和 type 。
Protobuf 将对象中的每个字段和正数序列 (tag) 对应起来,对应关系的信息是由生成的代码来保证的。在序列化的时候用整数值来代替字段名称,于是传输流量就可以大幅缩减。如果字段较少,它就使用 4 个 bits 来表示,最多支持 16 个字段。如果字段数量超过 16 个,那就再加 1 个字节,如果还不够那就再加 1 个字节。你也许猜到了,这个 tag 值使用的是 varint 编码。理论上字段的长度不设上限,因为 varint 可以通过扩展字节数支持任意大的非负整数。
Protobuf 将字段类型也和正数序列 (type) 对应起来,每一种原生的 java 类型都唯一对应一个正数,类型信息也非常节省。type 使用 3 个 bits 表示,最多支持 8 种类型。
也许你会认为 8 种类型怎么够呢?放心,肯定够的!因为一个 zigzag 类型可以表示所有的类整数类型,byte/short/int/long/bool/enum/unsigned byte/unsigned short/unsigned int/unsigned long 这些类型都可以使用 zigzag 表示。
而 Python 语言的整数更加特别,它根本就不区分整数的位数,甚至可以是一个 BigInteger。varint 的特长就在于此,它可以无限扩展位数大小,可以表示无限的整数值。而字节数组、字符串和嵌套对象都可以使用一种称之为 length 前缀 (length-delimited) 的类型来表示。另外 float 和 double 类型各占一个类型。最终你看,连 8 个类型都没有使用到。
Protobuf 的整数数值使用 zigzag 编码。zigzag 编码支持负数值,varint 编码的都是非负数。
浮点数分为 float 和 double ,它们分别使用 4 个字节和 6 个字节序列化,这两个类型的 value 没有做什么特殊处理,它就是标准的浮点数。
Protobuf 的字符串值使用长度前缀编码。第一个字节是字符串的长度,后面相应长度的字节串就是字符串的内容。如果字符串长度很长,那么长度前缀就不止一个字节,它可能是两字节三字节四字节,你也许猜到了,这个长度采用的是 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 类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式;service 与 rpc 方法名统一采用驼峰式命名;
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 字段可以被转化为扩展字段,反之亦然; int32 、uint32 、int64 、uint64 和 bool 这些类型是相互兼容的;sint32 和 sint64 相互兼容,但是不和其他数字类型兼容;string 和 bytes 相互兼容,前提是二进制内容是有效的 UTF-8 ;optional 和 repeated 相互兼容,即当给定的输入字段是 repeated 的时候,而如果接收方期待的是一个 optional 的字段的话,对与原始类型的字段,它会取最后一个值,对于消息类型的字段,他会将所有的输入合并起来。
3.12 proto3 与 proto2 的区别
- 使用
proto3 语法时,第一行必须写:syntax = "proto3" ; - 字段规则移除了
required 和 optional ,所有字段默认是 optional 类型的; repeated 字段默认采用 packed 编码,在 proto2 中,需要明确使用 [packed=true] 来为字段指定比较紧凑的 packed 编码方式;- 语言增加
Go 、Ruby 、JavaNano 支持; - 移除了
default 选项; - 枚举类型的第一个字段必须为
0 ; - 移除了对分组的支持;
- 移除了对扩展的支持,新增了
Any 类型; - 增加了
JSON 映射特性;
4. 其它参考
https://blog.csdn.net/u011518120/article/details/54604615 https://blog.csdn.net/mzpmzk/article/details/80824839
|