1、前言
在开始本篇文章,想先提问几个问题:
- 二进制编码是什么?数据在底层的传输不都是二进制流吗?
- 二进制编码具体要怎么做呢?
- 二进制编码很好吗?那为什么Web领域没有广泛地在前后端通信使用呢?
如果你对此有同样的疑惑,那本文可以带给你帮助。
2、二进制编码
1、二进制编码是什么
简介:把文本数据转成二进制数据进行传输。
核心思想:把数据(往往就是JSON格式)的核心信息提取出来,并最大程度的压缩,封装成二进制的形式(封包pack
)。
2、这么做收益是什么
数据体积变小。
3、为什么会变小
举个例子,我们要传输4294967296(2 的32次方)
这个数字,如果我们用文本形式来传输,需要10个字节
,但是使用二进制形式来传输,只需要4个字节
。
4、具体怎么做,过程是怎么样的
const obj ={
id: 1
name: "你好啊"
}
以上面这个json
为例,其实这个json
的字段名本身对我们没有意义,我们需要传输的只是这些字段值,而二进制编码的第一步,就是只保留字段值,并填充进二进制数组。
那被丢弃的字段名要怎么还原呢?这就要使用我们的idl
(thrift
或者protobuf
)实现。
通信双端保留定义idl
:
syntax = "proto3";
message People {
int32 id = 1;
string name = 2;
}
实际传输的值:
//id、name
[1, "你好啊"]
有人说这不是二进制数组吗,为什么会有字符串!没错这里只是为了方便看,实际他长下面这个样子
//十六进制形式
[0x01, 0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd, 0xe5, 0x95, 0x8a]
上面这个又是什么,只有1能认出来,但是后面完全看不出来。
要理解这个变换过程,就需要介绍一下unicode
和utf-8
unicode:是一个字符的集合,包含了这个世界上所有的字符,每个字符通过4个字节来表示
utf-8: 是一种可变长编码方式,由于unicode每个字符都需要4个字节,对于ascii这种本身只在需要1个字节就能表示的,显然就会造成浪费,因此往往会通过utf-8编码来处理unicode值。
那首先第一步,我们就需要先得到某个字符的unicode
值,我们通过charCodeAt
方法得到(以“王”字为例)
得到unicode
值之后,那utf-8
是怎么处理unicode
值的呢?这里给出一张图:
utf-8
把unicode
划分成四个区间,每个区间对应着一个模板,首先我们找到王这个字的模板,也就是第三个。
然后我们把“王”这个字的十六进制转成二进制形式
最后把他倒序的填入模版当中
自此我们成功的把一个字符变成了十六进制数。
下面给出函数的形式
export const strencode = (str: string) => {
let byteArray: number[] = [];
for (let i = 0; i < str.length; i++) {
let charCode = str.charCodeAt(i);
if (charCode <= 0x7f) {
byteArray.push(charCode);
} else if (charCode <= 0x7ff) {
byteArray.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f));
} else if (charCode <= 0xffff) {
byteArray.push(0xe0 | (charCode >> 12), 0x80 | ((charCode & 0xfc0) >> 6), 0x80 | (charCode & 0x3f));
} else {
byteArray.push(
0xf0 | (charCode >> 18),
0x80 | ((charCode & 0x3f000) >> 12),
0x80 | ((charCode & 0xfc0) >> 6),
0x80 | (charCode & 0x3f)
);
}
}
return new Uint8Array(byteArray);
};
export const strdecode = (bytes: Uint8Array) => {
let array: number[] = [];
let offset = 0;
let charCode = 0;
let end = bytes.length;
while (offset < end) {
if (bytes[offset] < 128) {
charCode = bytes[offset];
offset += 1;
} else if (bytes[offset] < 224) {
charCode = ((bytes[offset] & 0x3f) << 6) + (bytes[offset + 1] & 0x3f);
offset += 2;
} else if (bytes[offset] < 240) {
charCode = ((bytes[offset] & 0x0f) << 12) + ((bytes[offset + 1] & 0x3f) << 6) + (bytes[offset + 2] & 0x3f);
offset += 3;
} else {
charCode =
((bytes[offset] & 0x07) << 18) +
((bytes[offset + 1] & 0x3f) << 12) +
((bytes[offset + 1] & 0x3f) << 6) +
(bytes[offset + 2] & 0x3f);
offset += 4;
}
array.push(charCode);
}
return String.fromCharCode.apply(null, array);
};
但是现在还有一个问题,字符串的长度是不一定的,进行了编码之后,我要怎么知道读取几个数字才能还原“你好啊”这个字符了,这就需要说到TLV
格式:
TLV
格式中,T代表唯一标识,L代表数据长度,V就是数据值
在pack
时,我们会给字符串加一个长度的数值,代表这个字符串有多长。
通过这种方式,就能解决字符串,数组等不定长的问题。
那二进制编码还需要做什么呢?就是尽最大程度的压缩数据。
syntax = "proto3";
message People {
int32 id = 1;
string name = 2;
}
例如我们定义的这个结构体,id
我们定义为了int32
,也就是4个字节
,但是我们实际传输的值是1,其实只需要1个字节
就可以了,因此一般二进制编码框架都会用varint
(可变长整型)来处理数值,让体积变得更小!
4、实战(创建任意web项目和node项目即可)
- 安装依赖(双端都需要)
yarn add google-protobuf protobufjs protobufjs-cli
- 针对接口数据定义结构体
syntax = "proto3";
package haha;
message Region {
repeated OfficeList OfficeList = 1;
}
message OfficeList{
string id = 1;
string name = 2;
}
- 通过cli编译idl
"scripts": {
"proto": "mkdirp ./src/auto-gen-http && pbjs -t json-module -w commonjs -o ./src/auto-gen-http/index.js src/network/proto/*.proto"
},
- node端
//protoRoot就是生成的文件
import protoRoot from '../../auto-gen-http';
//设置相应头,返回二进制数据
ctx.set({ 'Content-Type': 'application/octet-stream', });
//这个字符串是proto文件的package+message拼凑出来,得到对应的编译器
const coder = protoRoot.lookup('haha.Region');
//通过编译器编译数据,生成buffer数组,ta是TypeArray的简写
const ta = coder.encode(response).finish();
ctx.body = new Buffer(ta);
此时接口返回的是二进制数据,非常不方便调试
5. web端
const result = await axios
.get(`/api/xxx`, {
//告诉axios我的返回值是二进制数据
responseType: 'arraybuffer',
})
//通过协议得到编译器
const coder = protoRoot.lookup('haha.Region')
//ab是ArrayBuffer的简写
const ab = result.data;
//接收到的数据是ArrayBuffer,转成TypeArray
const ta = new Uint8Array(arr);
//解码
const data = coder.decode(ta);
5、结果
数据体积从261kb
下降到105kb
PS:开启Gzip压缩可以叠加压缩效果
没压缩 | Gzip | ProtoBuf | ProtoBuf+Gzip |
---|---|---|---|
261kb | 35.1kb | 105kb | 27.8kb |