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
。
- 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
编码的都是非负数。
- 浮点数
浮点数分为 float
和 double
,它们分别使用 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
类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式;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.protomessage 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