|
Protocol Buffer 是什么
Protocol Buffers are a language-neutral, platform-neutral, extensible way of serializing structured data for use in communications protocols, data storage, and more, originally designed at Google
简单来说 Protocol Buffers(后面简称为 protobuf)是一种序列化(serializing)的方式,可以将数据序列化为二进制,同时也可以反序列化。至于序列化之后的二进制数据你是拿来传输,还是存储那就随便了。
什么是序列化
说到序列化,我们先来看一个我们最熟悉最常见的序列化方式 JSON.stringify()
const obj = { message: "Hello" };
const str = JSON.stringify(obj);
const resultObj = JSON.parse(str);
console.log(str);
console.log(resultObj);
// 结果
// Below is a string value
// {"message": "Hello"}
// Below is an JavaScript object
// {message: "Hello"}
上面我们将一个 JavaScript Object 序列化为一个字符串,然后又反序列化恢复成一个 Object。这个操作有什么用呢,就像上面说的,我们可以将序列化后的数据做传输或者存储。一个传输的例子,比如可以吧序列化后的 string 当作 query parameter 拼在请求 url 后面,一个存储的例子比如可以将序列化后的 string 存储在 LocalStorage。
二进制
讲完序列化,我们来了解下二进制。上面我们介绍的 JSON.stringify() 实际上序列化后的结果是文本格式,所以我们看起来和 JavaScript Object 没什么区别,依然是 human-readable。大家都知道,对人类友好的,对机器都不太友好,所以文本格式解析起来比二进制更麻烦一些。这也是为什么 HTTP/1.1 是纯文本协议,而 HTTP/2 引入了二进制分帧层,将文本转换为二进制再传输。
这里简单介绍下这两者的区别,纯文本协议的解析是通过分隔符(比如 \n\r)判断当前解析是否结束,而二进制本身每一位都有对应意义,可以理解为解析到某一个位置就好了,后面的另有他用,因此效率要高的多。
/* Node.js environment only */
const helloBuffer = Buffer.from("Hello");
const helloString = helloBuffer.toString();
console.log(helloBuffer);
console.log(helloString);
// 结果
// <Buffer 48 65 6c 6c 6f>
// Hello
上面是一个使用 Node.js 将文本转换为二进制再转回文本的例子,是不是也有点类似序列化的处理方式?
Protocol Buffer
理解了序列化和二进制后,我们就能开始讲 protobuf 了,首先我们需要通过创建一个 .proto 文件定一个 schema(类似 JSON Schema),这里我们可以理解为是一个约定的“规则”,我们的 encode, decode 都需要依据这个“规则”来操作。
// ./helloworld.proto
syntax = &#34;proto3&#34;;
package hellopackage;
message HelloWorld {
// [type] [key] = [tag]
string message = 1;
}在引入这个规则(可以看到下面 load 了文件)的前提下,我们对需要传输/存储的信息做和上面类似的序列化处理(下面的 encode, decode 函数)。这里我们使用的是 protobuf.js 提供的 API。
const protobuf = require(&#34;protobufjs&#34;);
protobuf.load(&#34;helloworld.proto&#34;, function (err, root) {
if (err) throw err;
const helloMessage = root.lookupType(&#34;hellopackage.HelloWorld&#34;);
const payload = { message: &#34;Hello&#34; };
const message = helloMessage.create(payload);
const buffer = helloMessage.encode(message).finish();
const message = helloMessage.decode(buffer);
const object = helloMessage.toObject(message);
console.log(buffer);
console.log(object);
// 结果
// <Buffer 0a 05 48 65 6c 6c 6f>
// { message: &#39;Hello&#39; }
});
Protocol Buffer 编码
下面我们对比下直接转换为二进制的 buffer 和通过 protobuf 序列化后的 buffer
// 同样是 &#34;Hello&#34;
Buffer.from();
// <Buffer 48 65 6c 6c 6f>
helloMessage.encode(message).finish();
// <Buffer 0a 05 48 65 6c 6c 6f>
我们发现后面五位 hex code 是一样的,分别对应 Hello 的每个字母的 ASCII 码,但是使用 protobuf 的方式比直接将 &#34;Hello&#34; 转换为二进制多了前两位 0a 和 05。这两位就是在协议设计中不可避免的冗余位,判断一个协议设计的好坏实际上就是,在保证正常使用的情况下,是否将冗余降到最低
这里我们先说简单的,05 表示后面的 string 长度为 16x0+5=5 位,这里细心的读者可能发现,两位 16 进制最大只能表示 16x16-1=255,如果 string 的长度超过 255 该怎么办?这个挺复杂,建议看下这篇文章
然后就是 0a 了,这里我们将 0a 转换为二进制 0 0 0 0 1 0 1 0,这里我们对前五位和后三位做一个拆分,得到 0 0 0 0 1 和 0 1 0。其中前五位表示 tag,这里 0 0 0 0 1 对应我们上面的 string message = 1 里面的 1。后三位表示 type,这里 0 1 0 表示 type = 2,对应为 string 类型。这里又有个问题,三位二进制最多只能表示 8 种类型,这个倒是没什么问题。但是 message 的字段最多只能表示 2^5-1=31 个就不够用了。实际上远远不止这些,但是这个也挺复杂的,具体看这里
总结
可以看出 Protocol Buffer 在传输过程中其实只会传输四个信息,分别是
- 类型(string, int...)
- 字段映射的值(也就是 tag)
- value 长度
- value 本身
对比 JSON 我们可以发现,key(也就是字段名)在 Protobuf 中是没有传输的,而是通过 tag 做了映射,这样的好处是,传输的信息更少(原来需要传 &#34;message&#34;,现在只用传 1)。但同样缺点是 client 和 server 需要共同维护一个 schema(也就是 .proto 文件),相较于 JSON 引入了依赖,也算是一种变相的空间换时间。
Ref
- https://medium.com/@FloSloot/node-js-protobuf-grpc-and-discovery-services-fd099a3fe51a
- https://github.com/protobufjs/protobuf.js/blob/master/README.md
- https://zhuanlan.zhihu.com/p/73549334
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|