Web 前端開發日誌(二):JavaScript 的二進位制操作

LancerComet發表於2018-03-06

文章為在下以前開發時的一些記錄與當時的思考, 學習之初的內容總會有所考慮不周, 如果出錯還請多多指教.

TL;DR

在瀏覽器中處理二進位制資料,需要使用 Typed ArrayArrayBufferDataView.

二進位制資料使用的資料型別: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 的文件可以看到其資料包格式:

Web 前端開發日誌(二):JavaScript 的二進位制操作

四個位元組表示包長度,兩個位元組表示頭部長度,兩個位元組表示協議版本,四個位元組表示當前操作,四個位元組為順序 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)
複製程式碼

相關文章