文章为在下以前开发时的一些记录与当时的思考, 学习之初的内容总会有所考虑不周, 如果出错还请多多指教.
TL;DR
在浏览器中处理二进制数据,需要使用 Typed Array
、ArrayBuffer
、DataView
.
二进制数据使用的数据类型:Typed Array
在浏览器环境中使用的二进制数据类型一般为 Typed Array(类型数组)
,它和普通的数组很像,只不过里面的成员类型是严格要求,并且长度固定的.
类型数组拥有以下几种:
- Int8Array:每个成员是有符号的 8 位整形,取值范围 -128 - 127.
- Uint8Array:每个成员是无符号的 8 位整形,取值范围 0 - 255.
- Uint8ClampedArray:每个成员是无符号的 8 位整形,取值范围 0 - 255,和上面的类型不同的是,若成员超过 255 或小于 0,则取相应最大值 255 或 最小值 0,而 Uint8Array 会进行类推取一个越界后的映射值. 当在处理色彩相关逻辑时非常有用.
- Int16Array:每个成员为有符号的 16 位整形,取值范围 -32768 - 32767.
- Uint16Array:每个成员为无符号的 16 位整形,取值范围 0 - 65535.
- Int32Array:每个成员为有符号的 32 位整形,取值范围 -2147483648 - 2147483647.
- Uint32Array:每个成员为无符号的 32 位整形,取值范围 0 - 4294967295.
- Float32Array:浮点数版本的 Int32Array.
- Float64Array:64 位版本的 Float32Array.
简单举例:
const uInt8Array = new Uint8Array(10)uInt8Array.length // 10uInt8Array[0] = 255 // 可以操作下标.复制代码
类型数组的详细文档您可以在查阅.
存放数据的容器:ArrayBuffer
一个类型数组是需要存放到一个容器中的,这个容器叫做 ArrayBuffer
.
ArrayBuffer
用来向浏览器申请一块区域存放类型数组,作用有点像 malloc
的感觉.
创建类型数组时可以先创建一个 ArrayBuffer
然后传入,也可以直接创建指定长度的类型数组;如果直接创建,则浏览器会自动创建一个 ArrayBuffer
来存储此类型数组:
const int8 = new Int8Array(10)int8.buffer // 这个就是存储这个类型数组的 ArrayBuffer.// 当然也可以显式创建:const buffer = new ArrayBuffer(10) // 申请 10 字节长度.const int8 = new Int8Array(buffer)复制代码
ArrayBuffer
的详细说明请看.
方便操作二进制数据的工具:DataView
实际上类型数组可以使用下标的方式来读写数组,只不过,太痛苦了点吧……还有大小端问题……
因此对于复杂的逻辑,我们可以使用 DataView
这个对象来对类型数组进行操作:
const buffer = new ArrayBuffer(16)const dataView = new DataView(buffer, 0)dataView.setInt16(2, 20) // 在第二个 16 位数的位置上以大端写入 20.dataView.getInt16(2) // 20// 将 buffer 数据映射至一个 Int8Array 中查看 buffer 结构:const int8Array = new Int8Array(buffer) // 类型数组也可以传入一个 ArrayBuffer 来创建,将直接映射这个 ArrayBuffer.console.log(int8Array) // [0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]// 以小端的方式再写一次:dataView.setInt16(2, 20, true)console.log(int8Array) // [0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]复制代码
DataView
提供了一些 API,详细的还请各位慢慢查阅.
使用案例:拼一个 goim 弹幕协议的数据包
是 B 站搞的一个弹幕协议,在 WebSocket 上同样可以根据此协议进行数据设计,不过在 WebSocket 上使用它的话是就要使用二进制方式传输数据,而非文本,所以数据包是需要在浏览器进行拼接的.
根据 的文档可以看到其数据包格式:
四个字节表示包长度,两个字节表示头部长度,两个字节表示协议版本,四个字节表示当前操作,四个字节为顺序 ID 标记,剩下的为数据本体.
那么就可以写一个简单的创建代码:
// 根据文档定义 offset.const packetOffset = 0const headerOffset = 4const verOffset = 6const opOffset = 8const seqOffset = 12const bodyOffset = 16// 弹幕协议包头的基础长度为 16.const headerLength = 16/** * 创建一个数据包. * * @param {IPacketOption} option * @returns {ArrayBuffer} */function createPacket (option: IPacketOption): ArrayBuffer { const headerBuffer = new ArrayBuffer(headerLength) const headerView = new DataView(headerBuffer, 0) const bodyBuffer = stringToArrayBuffer(option.body) headerView.setInt32(packetOffset, headerLength + bodyBuffer.byteLength) // 设置包长度, 长度 4 字节。 headerView.setInt16(headerOffset, headerLength) // 设置头部度. 4 字节. headerView.setInt16(verOffset, option.version) // 设置版本. 2 字节. headerView.setInt32(opOffset, option.operation) // 设置操作标识符, 4 字节. headerView.setInt32(seqOffset, option.sequence) // 设置序列号, 4 字节. return mergeArrayBuffer(headerBuffer, bodyBuffer)}/** * Packet 创建参数. * * @interface IPacketOption */interface IPacketOption { version: number operation: number sequence: number body: string}/** * 将字符串转换为基于 Int8Array 的 ArrayBuffer. * * @param {string} content * @returns {ArrayBuffer} */function stringToArrayBuffer (content: string): ArrayBuffer { const buffer = new ArrayBuffer(content.length) const bufferView = new Int8Array(buffer) for (let i = 0, length = content.length; i < length; i++) { bufferView[i] = content.charCodeAt(i) } return buffer}/** * 合并多个 ArrayBuffer 至同一个 ArrayBuffer 中. * * @param {...ArrayBuffer[]} arrayBuffers * @returns {ArrayBuffer} */function mergeArrayBuffer (...arrayBuffers: ArrayBuffer[]): ArrayBuffer { let totalLength = 0 arrayBuffers.forEach(item => { totalLength += item.byteLength }) const result = new Int8Array(totalLength) let offset = 0 arrayBuffers.forEach(item => { result.set(new Int8Array(item), offset) offset += item.byteLength }) return result.buffer}复制代码
好像可以减少操作?
这里有一段好像可以减少操作?
// 这段代码的大致意思是将 "存储了像素信息的数组中的数据绘制在 Canvas 中".const buffer = new ArrayBuffer(imageData.data.length)const buffer8 = new Uint8ClampedArray(buffer)const data = new Uint32Array(buffer)for (let y = 0; y < canvasHeight; y++) { for (let x = 0; x < canvasWidth; x++) { if (typeof pixelArr[y] === 'undefined') { continue } const value = pixelArr[y][x] if (typeof value === 'undefined' || value === null) { continue } data[y * canvasWidth + x] = 255 << 24 | value[2] << 16 | value[1] << 8 | value[0] }}imageData.data.set(buffer8)context.putImageData(imageData, 0, 0)复制代码