欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

在websocket实现zlib压缩通信(Z_SYNC_FLUSH模式的应用)

程序员文章站 2024-03-13 23:35:10
...

前言

在 zlib 使用中,不得不面临一个问题,如果使用原生 websocket ,这将使得单次通信最大包大小为 0xffff(65535),所以即使在 zlib 压缩情况下,很大的数据仍然要受此限制。

下面我们模拟通讯的情景,实现一种解决方案。

解决

pako.js

pako.js 是一个提供 zlib 和 gzip 压缩与解压功能的依赖库,我们基于此来实现。

Github:nodeca / pako

官方文档:Api Document

pako.js 使用是非常简单的,使用方法在 api 文档内都有,就不做详细说明了,下面简单介绍两个常用方法:

pako.Deflate

压缩方法,默认使用 zlib 压缩,使用 push() 方法添加数据。

// 初始化压缩方法实例
const deflate = new pako.Deflate()
/*
	data: 要压缩的数据,常传入 string / Uint8Array / ArrayBuffer
	mode: 控制压缩是否完毕,传入 false 代表还要 push 新的数据;
		  传入 true 或 flush-mode(刷新模式)代表 push 数据完毕,
		  当传入 true 时,不采用刷新模式( Z_NO_FLUSH ) 
*/
deflate.push(data, mode)
// 获取压缩数据结果( 默认为 Uint8Array )
const result = deflate.result

pako.Inflate

// 初始化解压方法实例
const inflate = new pako.Inflate()
/*
	data: 要解压的数据,Uint8Array
	mode: 控制解压是否完毕,传入 false 代表还要 push 新的数据;
		  传入 true 或 flush-mode(刷新模式)代表 push 数据完毕,
		  当传入 true 时,不采用刷新模式( Z_NO_FLUSH ) 
*/
inflate.push(data, mode)
// 获取解压后的数据结果
const result = inflate.result

注:如果你还不知道 Uint8ArrayArrayBuffer 是什么,可以自行先学习了解一下。

安装依赖

先安装 pako.js 依赖:

	yarn add pako

压缩数据

第一步,压缩数据,采用 ws 单次传输上限 65535 的 chunkSize

const pako = require('pako')

// 准备了两段文字来示范压缩
const str = ['a这是一段文字', 'b这也是一段文字']

const deflate = new pako.Deflate({
	// websocket 单次通信上限数据大小
    chunkSize: 65535
})
//  push 第一段文本, mode 指定为 false ,说明未 push 结束
deflate.push(str[0], false)
// push 第二段文本, mode 指定为 Z_SYNC_FLUSH 刷新模式
deflate.push(str[1], mode = pako.Z_SYNC_FLUSH)

// 获取压缩结果
const result = deflate.result
console.log(result)

/*  
	Uint8Array(37) [
	  120, 156,  74, 124, 177, 127, 230, 179,
	   25, 235, 159, 236, 104, 120, 182, 110,
	  235, 179, 105, 237,  79, 215,  78,  79,
	    2,  10,  61, 217,  57,  31,  77,  20,
	    0,   0,   0, 255, 255
	]
*/

这里最重要的即刷新模式,如果在 push() 的第二个参数传入 true 代表压缩完毕的话,默认不采用压缩模式,这会导致没有压缩尾部界定符,在解压时无法判断是不是最后一个数据包。

Z_SYNC_FLUSH

该刷新模式会在压缩数据尾部添加界定符 00 00 ff ff ,有关 Z_SYNC_FLUSH 的更多说明见:zlib 1.2.11 Manual

Normally the parameter flush is set to Z_NO_FLUSH, which allows deflate to decide how much data to accumulate before producing output, in order to maximize compression.

If the parameter flush is set to Z_SYNC_FLUSH, all pending output is flushed to the output buffer and the output is aligned on a byte boundary, so that the decompressor can get all input data available so far. (In particular avail_in is zero after the call if enough output space has been provided before the call.) Flushing may degrade compression for some compression algorithms and so it should be used only when necessary. This completes the current deflate block and follows it with an empty stored block that is three bits plus filler bits to the next byte, followed by four bytes (00 00 ff ff).

数据分块

为了模拟 websocket 单次接收上限情况,我们对得到的 Uint8Array 进行分块:

let dataStream = []
const chunkSize = 7
for(let i = 0; i < result.length / chunkSize; i++) {
    dataStream.push(result.slice(i * chunkSize, (i + 1) * chunkSize))
}
console.log(dataStream)

我们把分块后的数据放在了 dataStream 这个列表里。

解压缩

const inflate = new pako.Inflate({
    chunkSize: 65535,
    // 将解压后的结果直接转为 string ,如果不指定 to 字段,默认得到的是 Uint8Array
    to: 'string'
})

// 分块计数器
let count = 0

// 判断 zlib 压缩尾部界定符的函数
const over = (data) => {
    const length = data.length
    return (length >= 4
                && data[length - 4] === 0x00 
                && data[length - 3] === 0x00 
                && data[length - 2] === 0xff
                && data[length - 1] === 0xff) || length < 4
}

while(true) {
    let currentData = dataStream[count]
    let end = over(currentData)
    inflate.push(currentData, end ? pako.Z_SYNC_FLUSH : false)
    if(end) {
        break
    }
    count++
}

const origin = inflate.result
console.log(origin)

/*
	a这是一段文字b这也是一段文字
*/

在实际中,我们只需要对 websocket 收到的数据包进行循环判断是否有 zlib 尾部界定符即可。

在上文中,我们提到了 Z_SYNC_FLUSH 刷新模式可以添加 zlib 压缩尾部界定符 00 00 ff ff ,由此我们写出了 over() 判断界定符函数。

大数据策略

base64

对于大数据包,有两种策略,第一种是在服务端做 base64 转为字符串后压缩,到了客户端再将字符串解压 base64 解码。

如果是 node.js 服务端,会使用到 TextEncoder() 方法:

const encoder = new TextEncoder()
const view = encoder.encode('€')
console.log(view); // Uint8Array(3) [226, 130, 172]

在客户端将使用 TextDecoder() 方法:

let utf8decoder = new TextDecoder();
let u8arr = new Uint8Array([240, 160, 174, 183]);
console.log(utf8decoder.decode(u8arr));

详见 MDN :

二进制

如果你是 node.js ,通过 FileReader() 方法你可以得到文件的二进制 ArrayBuffer ,在服务端压缩后 ws.send()

在客户端,显示指定 websocket 通信数据方式为 arraybuffer ,再进行解压缩:

	const ws = new WebSocket(url, options)
	ws.binaryType = 'arraybuffer'

其他

服务端不是 node.js 的话也无伤大雅,只需要 zlib 使用 Z_SYNC_FLUSH 刷新模式返回数据即可,在 java 成熟生态下做 zlib 也是非常简单的,客户端使用 pako.js 进行解压接收。