Protocol Buffers Encoding
Protocol Buffers 是 Google 提出的一种序列化数据格式,最早的 public release 是在08年的7月份推出。相比于 XML 和 JSON ,它的传输大小更小,速度更快,结构更简单,在数据传输和存储的场景下更具有优势。同时 Google 非常友好的提供了不同平台的版本供开发者们使用,其对外的简洁性旨在让开发者们更注重业务的细节。不过本文并没有对源码进行剖析,我们重点分析的是 Protocol Buffers 是如何做到减小体积,也就是 Encoding 的过程。
为了便于理解,以下均已官网例子为例。
Message 定义
1 | message Test1 { |
我们从 Message 定义入手,这是一个简单的 Test1 Message 定义,里面有一个 int32 类型的 a 字段。假设我们将
a 赋值为150,那么在序列化后它在流中的内容是08 96 01
,大小只有3字节,而经过 JSON 序列化后有17字节。相比之下大小减小了很多,那08 96 01
的含义是什么呢?我们先来认识一下 varints
。
Base 128 Varints
Varints 是一种用一个或多个字节来表示一个整数的方法。
简单一点,比如整数1用一个字节就表示为
1 | 0000 0001 |
当超过128时,比如整数300,它的表示方式则稍微复杂一点了
1 | 1010 1100 0000 0010 |
这个是不是有点看不懂。首先 varint 中除去最后一个字节,其余都设有一个最高有效位
,用来表示是否有其他的字节出现。而一个字节中的低七位用于保存一个数的二补数。在多组字节进行计算的时候,低位有效组优先,也就是将字节的顺序反序。
根据 varint 的三条规则,我们推导一下1010 1100 0000 0010
是如何得到300的。
首先去掉每个字节中的最高有效位(因为最后一个字节的最高有效位是0,说明这是数字的尾端)
1 | 1010 1100 0000 0010 |
再将字节反序后计算
1 | 000 0010 010 1100 |
Message 结构
回到最初的例子,我们可以看到每个字段由四部分组成:field rule
、 field type
、field name
和 field number
。具体可见:Language Guide (proto3)
1 | message Test1 { |
一个 protocol buffer message 是由一系列的键值对组成,在转换成二进制的过程中只使用 field number
和 wire type
作为键,name 和定义的 type 需配合 .proto
文件解析得到,这样可以极大的减小传输体积,不过需要留意的是 .proto 文件中字段的定义和顺序,否则会解析出错。
wire type 这里实在不知道该如何翻译…我将其叫做编码后的类型
。下表是定义的 type 和编码后类型的对应关系:
上面提到的键则是经过 (field_number << 3) | wire_type
的计算得出,也就是后三位表示 wire type。
现在我们来分析一下08 96 01
。它是一个 key/value 对,我们将其拆成两块08
和 96 01
。
08转换为二进制表示0000 1000
,首先后三位的值为0,对应上表中 Varint,用于 int32, int64等等,a 定义的类型 int32 属于其中。然后再右移三位得到0000 0001
,值为1,也就是 a 的 field numer 。对照 .proto
文件就解析出 a 字段的位置了。
96 01转换为二进制表示1001 0110 0000 0001
,根据 varint 的三条规则,我们来还原它的值
1 | 96 01 = 1001 0110 0000 0001 |
综合一下这就是我们想要得到的 a = 150
。
Messages 嵌套
Message 定义也支持嵌套,比如
1 | message Test3 { |
同样 Test1 中 a 的值为150,它在流中的内容是1a 03 08 96 01
,最后三个字节就是上面 a 的内容。1a 03
我们同样拆分为1a
和 03
。
1a 转换为二进制表示0001 1010
,对应的 wire type 为2,对应到 embedded messages
,对应的 field number 为3。
03 这个的含义有所不同 ,它表示后面有三个字节,也就是08 96 01
。
WebSocket + Protocol Buffer
客户端用 SocketRocket ,服务端用 Node ,搭建一套 WebSocket 的环境。
以及用 Pods 安装 protobuf OC 版本,npm 安装 JS 版本。
首先我们定义一个简单的 Message 结构 Person,里面包含了 name 和 age 两个字段,生成 .proto 文件,在客户端和服务端各自维护一份。
1 | syntax = "proto3"; |
然后我们启动后端服务,监听1234端口,并对接收到的数据根据 Person.proto 文件定义的结构进行 decode ,然后打印数据。
1 | const WebSocket = require('ws'); |
服务启动成功之后,开始在客户端建立连接,在收到连接成功的回调之后,将 person 对象进行encode 并发送
1 | _ws = [[SRWebSocket alloc] initWithURL:[NSURL URLWithString:@"ws://localhost:1234"]]; |
在客户端 log 中我们看到输出的 data 内容为 0a03797a 79101c
,你可以尝试一下对其 decode ,看它是否和传输的内容一致。
同时服务端接收到数据后,进行 decode ,在对应的 log 中也正确输出了
1 | name is yzy |
OK,这样一个简单的 Protobuf 的使用就完成了。