Protocol Buffer
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。
Protocol buffers 在序列化数据方面,它是灵活的,高效的。相比于 XML 来说,Protocol buffers 更加小巧,更加快速,更加简单。一旦定义了要处理的数据的数据结构之后,就可以利用 Protocol buffers 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
- 数据体积小, 比 xml 小 3 - 5 倍
- 二进制格式, 数据描述自定义, 语义化清晰
- 性能高效, 比 xml 快 20-100 倍
- 跨平台语言, 前后端通用
- 向后兼容性好
局限:
- 不适合用来对基于文本的标记文档(如 HTML)建模
- 除非你有
.proto
定义 , 否则无法直接读取数据内容
以一段信息描述为例
xml
<person>
<name>James</name>
<age>30</age>
</person>
经过压缩以后,去掉所有空格正常传输
protocol buffers
person {
name: "James"
age: "30"
}
通过编码以后,以二进制的方式进行数据传输
示例
命名规则: 包名. 消息名.proto
syntax = "proto3"; // 定义使用版本
package xx; // 可选的 package 声明符,用来防止不同的消息类型有命名冲突
import xx; // 导入其他.proto 文件
message xx // 消息命名
{字段限制(可不写) 字段类型 字段名 = 标识号(标记作用);
}
注意:
- 如果开头第一行不声明
syntax = "proto3";
,则默认使用 proto2 进行解析。 - 语法说明 (syntax) 前只能是空行或者注释
- 每个字段由字段限制、字段类型、字段名和编号四部分组成
packageName.MessageName.proto
syntax = "proto3";
package lm;
message helloworld
{
int32 id = 1; // ID
string str = 2; // str
int32 opt = 3; //optional field
}
字段限制
-
required
: 必须赋值的字段 -
optional
: 可有可无的字段 -
repeated
: 可重复字段(变长字段)
由于一些历史原因,基本数值类型的 repeated 的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项 [packed=true] 来保证更高效的编码
字段类型
.proto Type | NotesNotes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | PHP Type |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | |
float | float | float | float | float32 | Float | float | float | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用 sint64 替代 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
uint32 | 使用变长编码 | uint32 | int | int/long | uint32 | Fixnum 或者 Bignum(根据需要) | uint | integer |
uint64 | 使用变长编码 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sint32 | 使用变长编码,这些编码在负值时比 int32 高效的多 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的 int64 高效。 | int64 | long | int/long | int64 | Bignum | long | integer/string |
fixed32 | 总是 4 个字节,如果数值总是比总是比 228 大的话,这个类型会比 uint32 高效。 | uint32 | int | int | uint32 | Fixnum 或者 Bignum(根据需要) | uint | integer |
fixed64 | 总是 8 个字节,如果数值总是比总是比 256 大的话,这个类型会比 uint64 高效。 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sfixed32 | 总是 4 个字节 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
sfixed64 | 总是 8 个字节 | int64 | long | int/long | int64 | Bignum | long | integer/string |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | 一个字符串必须是 UTF- 8 编码或者 7 -bit ASCII 编码的文本。 | string | String | str/unicode | string | String (UTF-8) | string | string |
bytes | 可能包含任意顺序的字节数据。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string |
标识号
在消息定义中,每个字段都有唯一的一个 数字标识符 。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15] 之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用 2 个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
基本语法
预留字段
如果消息的字段被移除或注释掉,但是使用者可能重复使用字段编码,就有可能导致例如数据损坏、隐私漏洞等问题。一种避免此类问题的方法就是指明这些删除的字段是保留的。如果有用户使用这些字段的编号,protocol buffer 编译器会发出告警。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
缺省值
如果没有指定默认值, 则会使用系统默认值,对于 string
默认值为空字符串,对于 bool
默认值为 false,对于 数值类型
默认值为 0,对于 enum
默认值为定义中的第一个元素,对于 repeated
默认值为空。
枚举
message SearchRequest {
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
}
Corpus corpus = 1;
}
即参数限定只能其中一种
注意:
- 通过设置可选参数
allow_alias
为 true,就可以在枚举结构中使用别名(两个值元素值相同) - 由于枚举值采用 varint 编码,所以为了提高效率,不建议枚举值取负数。这些枚举值可以在其他消息定义中重复使用。
嵌套类型
可以使用一个消息的定义作为另一个消息的字段类型。
message SearchResponse {repeated Result results = 1;}
message Result {
string url = 1;
string title = 2;
}
或者
message SearchResponse {
message Result {
string url = 1;
string title = 2;
}
repeated Result results = 1;
}
表映射
map<key_type, value_type> map_field = N;
key_type
可以是除浮点指针或 bytes
外的其他基本类型,value_type
可以是任意类型
- Map 的字段不可以是重复的(repeated)
- 线性顺序和 map 值的的迭代顺序是未定义的,所以不能期待 map 的元素是有序的
- maps 可以通过 key 来排序,数值类型的 key 通过比较数值进行排序
- 线性解析或者合并的时候,如果出现重复的 key 值,最后一个 key 将被使用。从文本格式来解析 map,如果出现重复 key 值则解析失败。
命名规范
message 采用首字母大写开头驼峰写法。字段名采用下划线分隔命名。
message SongServerRequest {required string song_name = 1;}
枚举类型全部大写,并且采用下划线分隔命名。
enum Foo {
FIRST_VALUE = 0;
SECOND_VALUE = 1;
}
每个枚举值用分号结束,不是逗号。
RPC(远程过程调用)
如果要使用 RPC(远程过程调用)系统的消息类型,可以在 .proto
文件中定义 RPC 服务接口,protocol buffer 编译器将使用所选语言生成服务接口代码和 stubs。
例如,如果你定义一个 RPC 服务 GetGameList,入参是 GameListReq 返回值是 GameListRes,你可以在你的 .proto
文件中定义它,如下所示:
service GameService {rpc GetGameList (GameListReq) returns (GameListRes);
}
ProtoBuf.js
pb 是一个跨语言, 跨平台, 可扩展的序列化结构数据格式的方式. 用于通讯协议和数据保存等等, 最初由谷歌设计.
pb 是一个由 typescript 完成的纯 javascript 实现, 支持 nodejs 和浏览器, 它容易使用, 速度高效和.proto 文件一起工作.
官网示例
// awesome.proto
package awesomepackage;
syntax = "proto3";
message AwesomeMessage {string awesome_field = 1; // becomes awesomeField}
protobuf.load("awesome.proto", function(err, root) {if (err)
throw err;
// 获取消息类型
var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage");
// 有效荷载(即参数)
var payload = {awesomeField: "AwesomeString"};
// 如果有需要的话验证参数, 可能不完整或无效
var errMsg = AwesomeMessage.verify(payload);
if (errMsg)
throw Error(errMsg);
// 创建新消息
var message = AwesomeMessage.create(payload); // 如果需要转换的话就使用.fromObject
// 将消息编码成 Uint8Array (browser) 或者 Buffer (node)
var buffer = AwesomeMessage.encode(message).finish();
// ... do something with buffer
// 将 Uint8Array (browser) or Buffer (node)解码回消息
var message = AwesomeMessage.decode(buffer);
// ... do something with message
// 如果应用使用长度限制的 buffer, 也提供 encodeDelimited 和 decodeDelimited.
// 可以将消息转换会普通对象
var object = AwesomeMessage.toObject(message, {
longs: String,
enums: String,
bytes: String,
// see ConversionOptions
});
});
实际项目使用我们不直接解析.proto 文件, 而是将他们合并打包成成 json 文件引用.
首先全局安装 pb
npm/cnpm i -g protobufjs
使用内置的 pbjs 将.proto 合并转成 json 使用
pbjs -t json xx.proto xx.proto > bundle.json
命令行接口 (CLI) 可用于在文件格式之间进行转换,并生成静态代码和 TypeScript 定义。
对于生产环境, 建议把你所有的.proto 文件打包单个 json 文件, 它最小化了网络请求的数量, 并避免了任何解析器的开销(提示: 使用光库):
映射和静态区别
Source | Library | Advantages | Tradeoffs |
---|---|---|---|
.proto | full | 易于编辑, 与其他库的互操作性, 没有编译步骤 | 一些解析和可能的网络开销 |
JSON | light | 易于编辑,没有解析开销,单个包(没有网络开销) | protobuf.js 特有的一个编译步骤 |
static | minimal | 在 eval 访问被限制, 完整记录,Small footprint for small protos | 很难编辑, 没有反射, 有一个编译步骤 |
大体流程
1, 将.proto 文件转义成 json;
2, 引入文件构建类型对象;
3, 实例化类型对象序列化成二进制数据发送给服务端;
4, 从服务端拿到二进制数据反序列化解析成类型对象转成 js 对象使用;