Protocol Buffers
是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC
数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
它有以下特点:
C++
、Java
和 Python
等;Google
开源的二进制 RPC
通讯协议;Protobuf
并没有定义消息边界,也就是没有消息头。消息头一般由用户自己定义,通常使用长度前缀法来定义边界。
同样 Protobuf
也没有定义消息类型,当服务器收到一串消息时,它必须知道对应的类型,然后选择相应消息类型的解码器来读取消息。这个类型信息也必须由用户自己在消息头里面定义。
网络协议是计算机网络中进行数据交换而建立的规则、标准或约定的集合。而二进制协议是在进行网络传输时,传输的并不是类似 JSON
这样的文本文件,而是类似 BSON
这样的二进制数据。
二进制协议就是一串字节流,通常包括消息头(header
)和消息体(body
),消息头的长度固定,并且消息头包括了消息体的长度。这样就能够从数据流中解析出一个完整的二进制数据。如下是一个典型的二进制协议结构模型:
其中,
Guide
用于标识协议起始;Length
是消息体 Data
的长度,为了数据完整性,还会加上相应的校验(DataCRC
、HeaderCRC
);Data
中又分为命令字(CMD
) 和命令内容。命令字是双方协议文档中规定好的,比如 0x01 代表登录,0x02 代表登出等,一般数据字段的长度也是固定的。又因为长度的固定,所以少了冗余数据,传输效率较高。优点
缺点
文本协议一般是由一串 ACSII
字符组成的数据,这些字符包括数字、大小写字母、百分号、回车(\r
)、换行(\n
)以及空格等等。
文本协议设计的目的就是方便人们理解、读懂。所以,协议中通常会加入一些特殊字符用于分隔,比如如下数据:
git commit -m "fix: 修改BUG"
这是一条文本指令,尽管没有说明,我们也能够直观的看出,这 git
里面的 commit
指令,-m
后面跟随的是当前提交的描述信息。
但为了便于解析,文本协议不得不添加一些冗余的字符用于分隔命令,降低了其传输的效率;而且只适于传输文本,很难嵌入其他数据,比如一张图片、一段音频等。
优点
缺点
我们大致总结文本协议和二进制协议的优缺点:
所以:
RPC
接口的调用等,可使用文本协议;Protobuf
源码已在 GitHub
上开源:https://github.com/protocolbuffers/protobuf
官方文档地址为:https://developers.google.com/protocol-buffers/
以下示例默认使用 proto3
库编写。
Protobuf
传输的是一系列的键值对,如果连续的键重复了,那说明传输的值是一个列表 repeated
。图中的 key3
就是一个列表类型 repeated
。
键 key
两部分组成:tag
和 type
。
Protobuf
将对象中的每个字段和字段索引 tag
对应起来,字段索引就是在 proto
文件中定义消息时,为每个消息字段所设置的唯一数字。在序列化的时候用整数值来代替字段名称,于是传输流量就可以大幅缩减。
如果字段较少,它就使用 4 个 bits 来表示,最多支持 16 个字段。如果字段数量超过 16 个,那就再加 1 个字节,如果还不够那就再加 1 个字节。你也许猜到了,这个 tag
值使用的是 varint
编码。理论上字段的长度不设上限,因为 varint
可以通过扩展字节数支持任意大的非负整数。
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
编码。
通过 .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 | 可以包含任意字节序列 |
.proto
做为文件后缀;message
命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式;enum
类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式;service
与 rpc
方法名统一采用驼峰式命名;单行注释
// 单行注释
多行注释
/**
* 多行注释
*/
解析消息时,如果编码消息不包含特定的单位元素,则解析对象中的相应字段将设置为该字段的默认值,这些默认值是特定于类型的:
false
;0
;在消息定义中,每个字段都有唯一的一个数字标签。这些标签是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。
注:
- [1, 15] 之内的标签在编码的时候会占用一个字节。
- [16, 2047] 之内的标签则占用两个字节。
所以应该为那些频繁出现的消息元素保留 [1, 15] 之内的标签。切记:要为将来有可能添加的、频繁出现的字段预留一些标签。
在 .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
字段声明包名。
当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。 则通过向消息定义中添加一个枚举( enum
)并且为每个可能的值定义一个常量就可以了。
syntax = "proto3"
message Course {
// ...
CourseType type = 1;
}
enum CourseType {
CHINESE = 0;
MATH = 1;
ENGLISH = 2;
}
Any
类型是一种不需要在 .proto
文件中定义就可以直接使用的消息类型,使用前需要导入文件:
syntax = "proto3"
import google/protobuf/any.proto
message Request {
string userId = 1;
google.protobuf.Any data = 2;
int64 timestamp = 3;
}
如果想在 RPC
系统中使用消息类型,就需要在 .proto
文件中定义 RPC
服务接口,然后使用编译器生成对应语言的存根。
syntax = "proto3"
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
如果在更新字段的时候为了重用标签,我们手动删除或者注释了旧版本的标签,那么当使用旧版本加载新的 .proto
文件时会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是指定保留标签,Protocol Buffer
的编译器会警告未来尝试使用这些域标签的用户。
syntax = "proto3"
message Person {
reserved 2, 3, 4 to 8; // 标识号
reserved "name", "age"; // 字段名
}
如果一个现在的消息不再能满足你的需求,比如要增加新的字段,但是你仍然希望兼容旧版本生成的消息的话,你需要安装以下规则更新消息,否则会出现兼容性问题(包括 proto2 的部分语法功能):
optional
或者 repeated
。required
字段可以被移除,但是对应的数字标签不能被重用;required
字段可以被转化为扩展字段,反之亦然;int32
、uint32
、int64
、uint64
和 bool
这些类型是相互兼容的;sint32
和 sint64
相互兼容,但是不和其他数字类型兼容;string
和 bytes
相互兼容,前提是二进制内容是有效的 UTF-8
;optional
和 repeated
相互兼容,即当给定的输入字段是 repeated
的时候,而如果接收方期待的是一个 optional
的字段的话,对与原始类型的字段,它会取最后一个值,对于消息类型的字段,他会将所有的输入合并起来。proto3
语法时,第一行必须写:syntax = "proto3"
;required
和 optional
,所有字段默认是 optional
类型的;repeated
字段默认采用 packed
编码,在 proto2
中,需要明确使用 [packed=true] 来为字段指定比较紧凑的 packed
编码方式;Go
、Ruby
、JavaNano
支持;default
选项;0
;Any
类型;JSON
映射特性;https://blog.csdn.net/u011518120/article/details/54604615
https://blog.csdn.net/mzpmzk/article/details/80824839