接口数据体积压缩之二进制编码

1、前言

在开始本篇文章,想先提问几个问题:

  1. 二进制编码是什么?数据在底层的传输不都是二进制流吗?
  2. 二进制编码具体要怎么做呢?
  3. 二进制编码很好吗?那为什么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能认出来,但是后面完全看不出来。

要理解这个变换过程,就需要介绍一下unicodeutf-8

unicode:是一个字符的集合,包含了这个世界上所有的字符,每个字符通过4个字节来表示

utf-8: 是一种可变长编码方式,由于unicode每个字符都需要4个字节,对于ascii这种本身只在需要1个字节就能表示的,显然就会造成浪费,因此往往会通过utf-8编码来处理unicode值。

那首先第一步,我们就需要先得到某个字符的unicode值,我们通过charCodeAt方法得到(以“王”字为例)

得到unicode值之后,那utf-8是怎么处理unicode值的呢?这里给出一张图:

utf-8unicode划分成四个区间,每个区间对应着一个模板,首先我们找到王这个字的模板,也就是第三个。

然后我们把“王”这个字的十六进制转成二进制形式

最后把他倒序的填入模版当中

自此我们成功的把一个字符变成了十六进制数。

下面给出函数的形式

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项目即可)

  1. 安装依赖(双端都需要)
yarn add google-protobuf protobufjs protobufjs-cli
  1. 针对接口数据定义结构体
syntax = "proto3";





package haha;

message Region {
  repeated OfficeList OfficeList = 1;
}


message OfficeList{
  string id = 1;
  string name = 2;
}
  1. 通过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"
},
  1. 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);

此时接口返回的是二进制数据,非常不方便调试
image.png
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

image.png
下降到105kb

image.png

PS:开启Gzip压缩可以叠加压缩效果

没压缩 Gzip ProtoBuf ProtoBuf+Gzip
261kb 35.1kb 105kb 27.8kb

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYesUKAY' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片