文章为在下以前开发时的一些记录与当时的思考, 学习之初的内容总会有所考虑不周, 如果出错还请多多指教.
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 // 10
uInt8Array[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 弹幕协议的数据包
goim 是 B 站搞的一个弹幕协议,在 WebSocket 上同样可以根据此协议进行数据设计,不过在 WebSocket 上使用它的话是就要使用二进制方式传输数据,而非文本,所以数据包是需要在浏览器进行拼接的.
根据 Github 的文档可以看到其数据包格式:
四个字节表示包长度,两个字节表示头部长度,两个字节表示协议版本,四个字节表示当前操作,四个字节为顺序 ID 标记,剩下的为数据本体.
那么就可以写一个简单的创建代码:
// 根据文档定义 offset.
const packetOffset = 0
const headerOffset = 4
const verOffset = 6
const opOffset = 8
const seqOffset = 12
const 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)
复制代码