文章爲在下之前開發時的一些記錄與當時的思考, 學習之初的內容總會有所考慮不周, 若是出錯還請多多指教.git
在瀏覽器中處理二進制數據,須要使用 Typed Array
、ArrayBuffer
、DataView
.github
在瀏覽器環境中使用的二進制數據類型通常爲 Typed Array(類型數組)
,它和普通的數組很像,只不過裏面的成員類型是嚴格要求,而且長度固定的.typescript
類型數組擁有如下幾種:canvas
簡單舉例:數組
const uInt8Array = new Uint8Array(10)
uInt8Array.length // 10
uInt8Array[0] = 255 // 能夠操做下標.
複製代碼
類型數組的詳細文檔您能夠在這裏查閱.瀏覽器
一個類型數組是須要存放到一個容器中的,這個容器叫作 ArrayBuffer
.工具
ArrayBuffer
用來向瀏覽器申請一塊區域存放類型數組,做用有點像 malloc
的感受.學習
建立類型數組時能夠先建立一個 ArrayBuffer
而後傳入,也能夠直接建立指定長度的類型數組;若是直接建立,則瀏覽器會自動建立一個 ArrayBuffer
來存儲此類型數組:ui
const int8 = new Int8Array(10)
int8.buffer // 這個就是存儲這個類型數組的 ArrayBuffer.
// 固然也能夠顯式建立:
const buffer = new ArrayBuffer(10) // 申請 10 字節長度.
const int8 = new Int8Array(buffer)
複製代碼
ArrayBuffer
的詳細說明請看這裏.spa
實際上類型數組可使用下標的方式來讀寫數組,只不過,太痛苦了點吧……還有大小端問題……
所以對於複雜的邏輯,咱們可使用 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 上使用它的話是就要使用二進制方式傳輸數據,而非文本,因此數據包是須要在瀏覽器進行拼接的.
根據 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)
複製代碼