文章為在下以前開發時的一些記錄與當時的思考, 學習之初的內容總會有所考慮不周, 如果出錯還請多多指教.
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)
複製程式碼