找回密码
 立即注册
查看: 362|回复: 2

从前端的角度理解 Protobuf

[复制链接]
发表于 2021-11-29 07:45 | 显示全部楼层 |阅读模式
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 = "proto3";
package hellopackage;

message HelloWorld {
    // [type] [key] = [tag]
    string message = 1;
}在引入这个规则(可以看到下面 load 了文件)的前提下,我们对需要传输/存储的信息做和上面类似的序列化处理(下面的 encode, decode 函数)。这里我们使用的是 protobuf.js 提供的 API。
const protobuf = require("protobufjs");

protobuf.load("helloworld.proto", function (err, root) {
  if (err) throw err;

  const helloMessage = root.lookupType("hellopackage.HelloWorld");
  const payload = { message: "Hello" };
  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: 'Hello' }
});
Protocol Buffer 编码

下面我们对比下直接转换为二进制的 buffer 和通过 protobuf 序列化后的 buffer
// 同样是 "Hello"

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 的方式比直接将 "Hello" 转换为二进制多了前两位 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 做了映射,这样的好处是,传输的信息更少(原来需要传 "message",现在只用传 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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-11-29 07:55 | 显示全部楼层
但同样缺点是 client 和 server 需要共同维护一个 schema(也就是 .proto 文件),相较于 JSON 引入了依赖,也算是一种变相的空间换时间。这是好处,不是缺点,有相同的依赖,ci 就可以发现 breakage
发表于 2021-11-29 08:00 | 显示全部楼层
我终于看明白的一篇理解
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-25 15:59 , Processed in 0.100157 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表