以前用JavaScript主要是處理常規的數字、字符串、數組對象等數據,基本沒有試過用JavaScript處理二進制數據塊,最近的項目中涉及到這方面的東西,就花一段時間學了下這方面的API,在此總結一下。
首先瀏覽器是沒有主動讀取本地文件的權限的,所以對JavaScript處理二進制數據能力的學習,應該從運行在服務器端的nodejs看起。
Nodejs 中的 Buffer
為了方便處理二進制數據,nodejs特地封裝了一個Buffer模塊。文檔地址:http://nodejs.cn/doc/node/buffer.html
buffer模塊的基礎API
可以通過下面的方式來初始化一個Buffer對象,傳入參數50,這樣就在內存中申請了一個50byte,400bit的區域來備用,這塊區域的大小一旦申請就不能改變,然后通過Buffer對象的fill方法來填充這塊內存區域,。
傳入的“abc”字符串,默認會按照utf8編碼的方式解碼為二進制數據,存入到這一塊內存區域中對應的位置。

可以看到,Buffer對象在debug工具中顯示的是一個長度為50的Uint8Array數組,這個Uint8Array對象是啥后邊會解釋。
數組的每一位上都存儲着一個無符號8位整數,也就是一個字節,0~255。

Buffer對象初始化方法還包括:

Buffer對象還有一些比較、拼接、復制、填充等方法,在上面的文檔中都有

nodejs的fs模塊
不光手動初始化可以獲取一個Buffer對象,通過fs模塊來讀取文件,也可以獲取一個buffer對象。
dnf.exe是一個存放於硬盤中的31733096字節的文件

通過fs模塊的readFileSync方法讀取此文件,可以得到一個Buffer對象,此Buffer對象

運行之后結果如下:

這樣dnf.exe文件就被讀取到了內存中,nodejs中有了一個Buffer對象,其占用的內存空間是31733096byte。
Buffer對象可以被寫入本地文件系統,或者通過網絡寫入遠程的機器中,或者轉換為字符串來做更多的操作,或者不做任何處理。
上面例子中的場景,我們可以在當前目錄下用次buffer寫入一個新的文件

運行完畢后:

這個例子可以行得通,然而事情不會一直這么簡單。
我的電腦內存大小是8G,當我想用這段代碼copy一個12G的文件時,會發生什么事情呢? 在第一步讀取文件的時候,我們需要創建一個占用12G大小內存空間的Buffer對象,這樣顯然是行不通的,內存會爆掉。那怎么辦呢?
Nodejs中的 Stream
什么是Stream,為什么要有Stream
上一小節的例子中,我們遇到了內存不夠用的情況,顯然我們就需要
把數據分成一小塊一小塊,一塊一塊的放到內存中去處理,這樣內存就不會爆掉了~
在這里我想比喻一下,方便理解:
CPU相當於一個
工人
工人需要操作工具加熱水(CPU需要運行代碼執行計算)
硬盤相當於一個
水池
水池里邊可以蓄水,容量很大,但是不能在水池里邊直接加熱水,需要把水放進鍋中(硬盤可以存放數據,容量很大,但是不能直接在硬盤中利用數據執行計算,需要把數據讀進內存中)
內存相當於一口
鍋
鍋可以盛水並加熱,但是容量不大(內存中可以存放數據用於計算,但是容量不大)
加熱-導入水(計算-導入數據)
那么上面小節中我們遇到的問題就是:池子中的水太多,一次倒鍋里去就溢出來了,加熱不了了。於是我們采取一種措施:
從水池中連接一個管道到鍋中,這根
管道(stream.Readable類)可以把水從池子中導入鍋中
管子一開始是封閉的,我們可以把開關打開(綁定 Event: 'data',此事件綁定之后即刻觸發)
也可以暫停導入(readable.pause())
可以恢復導入(readable.resume())
還可以手動導入(readable.read())
加熱完畢—導出水(計算完畢-導出數據)
當導入的一鍋水燒好以后,需要把這一鍋水倒出去才能處理下一鍋,於是
從鍋中連接一根
管道(stream.Writable類)到另外的容器中(可能是硬盤中的另外一塊區域,也可能是遠程的另外一台機器)
和上面的readStream一樣,
管子可以向另外一個地方導出水(writable.write)
不計算只中轉
當鍋只是起一個中轉的作用時,可以把導入管接到導出管上去(readable.pipe(destination[, options]))
過程中也可以把他們分開(readable.unpipe([destination]))
既可以導出也可以導入的Stream
有的管道既可以導入,也可以導出(stream.Transform)
Stream模塊基本用法
基礎API當然還是官方文檔:
http://nodejs.cn/doc/node/stream.html
把前邊的例子改寫:



或者更簡單的

或者

readstream中的內容默認是Buffer對象,在讀取之前設置字符編碼,讀取的時候即可獲取字符串。
Stream的應用場景
用到Stream的地方有
剛才說的file system
http模塊的request對象和response對象
net模塊的數據傳輸
當我們需要持續的向一個地址寫入二進制數據or從一個地址讀出二進制數據時,我們也可以根據nodejs官方提供的API實現我們自己的讀寫stream

當我們用nodejs做http代理的時候,對於客戶端,nodejs是服務器,response對象是一個writeStream;對於目標服務器,nodejs是客戶端,res對象是一個readStream,
因此直接res.pipe(response)就把目標服務器的數據轉發給客戶端了。

http服務端向客戶端寫入文件的時候,也可以直接把文件的readStream對接到response上
瀏覽器中的 ArrayBuffer

上面nodejs中的Buffer對象在ws控制台中顯示出來的是Uint8Array,對於這一點我查了下,發現es5其實是有二進制處理的API的,只是在瀏覽器端用的實在不多,所以之前並沒有關注到。
下面是msdn文檔里邊對ArrayBuffer的解釋:
ArrayBuffer 對象表示用於存儲不同類型化數組的數據的原始數據緩沖區。無法直接讀取或寫入 ArrayBuffer,但可以將它傳遞給類型化數組或 DataView 對象 來解釋原始緩沖區。可以使用 ArrayBuffer 來存儲任何類型的數據(或混合類型的數據)。


存儲二進制數據,定義時length以byte來計數,看起來是不是和nodejs環境下的Buffer很像?
但是看API這東西並不能直接寫入和讀取數據,所以,接着往下看吧!
ArrayBuffer的查看和編輯視圖——類型化數組和DataView
什么是類型化數組
剛剛說到的ArrayBuffer,看起來有點像nodejs中的Buffer對象,但是又沒有Buffer對象的讀寫API,這個不太科學嘛,所以肯定需要一種辦法來操作它。
實際上,ArrayBuffer對象就是一塊靜止的二進制數據存儲區:
00000001 00000010 000000011 00000100
當需要寫入或者操作這段內存區時,如果直接開幾個API讓你寫入01010101這種數據,我想你的內心一定是崩潰的。
所以es5提供了一些視圖來表示和操作這些二進制數據,比如,每8位二進制數轉換成一個10進制的8位無符號整數,存入一個特殊數組中,就是
Uint8Array [1,2,3,4]
這種數組的原型並沒有指向Array.prototype,它不具有JavaScript普通數組的那些操作方法,同時這種數組里邊所有的元素一定是一個0~255的整數。
當我們操作 arr[3]=1之后,Uint8Array就變成了
[1,2,3,1]
而ArrayBuffer的內容就變成了
00000001 00000010 000000011 00000001
要注意的是,ArrayBuffer是原始數據,根據原始數據可以創建多份不同類型的數據視圖,當任何一個視圖改變了原始數據后,其他視圖所看到的數據都會發生變化,下面會給出例子。
這個Uint8Array 是不是很眼熟呢? 看起來nodejs中的Buffer和瀏覽器環境下的Uint8Array有某種聯系啊!
類型化數組類型
這種視圖的類型有很多:




每種數組中的數據類型不同,看名字可以明白他們存放的數據類型,例如Float32Array,就是把每32個二進制數轉換成一個無符號浮點數,存入這種類型化數組中。
類型化數組操作API
初始化方法:

屬性解釋:

操作方法:


DataView
DataView是一個可以獲取ArrayBuffer數據和編輯ArrayBuffer數據的對象,它取ArrayBuffer中的一個片段,提供一些方法來獲取或編輯這些片段中的數據,不像類型化數組一樣把數據放入一個數組結構中。

方法
下表列出了 DataView 對象的方法。
方法 |
描述 |
---|---|
在相對於視圖開始處的指定字節偏移量位置處獲取 Int8 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處獲取 Uint8 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處獲取 Int16 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處獲取 Uint16 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處獲取 Int32 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處獲取 Uint32 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處獲取 Float32 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處獲取 Float64 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處存儲 Int8 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處存儲 Uint8 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處存儲 Int16 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處存儲 Uint16 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處存儲 Int32 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處存儲 Uint32 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處存儲 Float32 值。 |
|
在相對於視圖開始處的指定字節偏移量位置處存儲 Float64 值。 |
一種特殊的類型化數組
Uint8ClampedArray和Uint8Array類似,唯一不同的是,當插入數組的值不在0~255之間的時候,取值策略不同,Uint8Array是將輸入的數字取模,而Uint8ClampedArray是大於255取255,小於0取0
Uint8ClampedArray在canvas的getImageData時用到,因為顏色值#ffffff,剛好用3個字節來表示,比較合適~

類型化數組理解時要注意的特點
初始化時字節取值范圍
ArrayBuffer初始化時的length是以字節(byte)為單位的而不是位(bit),
通過緩沖區初始化類型化數組的時候,offerset代表的緩沖區中的偏移量,也
而類型化數組初始化時的length是類型化數組中的length代表的並不是此次初始化要使用的緩沖區中的字節長度,而是類型化數組中的元素個數。
Uint32Array = new Uint32Array ( buffer, byteOffset, length);
所以例如Uint32Array實際被使用的緩沖區中的字節應該是從第byteOffset個到第byteOffset+4*length個字節,而不是從第byteOffset個字節到byteOffset+length個字節
類型化數組只是視圖,原始數據存在ArrayBuffer中,原始數據發生變化,所有視圖都變化


瀏覽器中的使用實例
1.ajax接收ArrayBuffer數據
var req = new XMLHttpRequest(); req.open('GET', "http://www.example.com"); req.responseType = "arraybuffer"; req.send(); req.onreadystatechange = function () { if (req.readyState === 4) { var buffer = req.response; var dataview = new DataView(buffer); var ints = new Uint8Array(buffer.byteLength); for (var i = 0; i < ints.length; i++) { ints[i] = dataview.getUint8(i); } alert(ints[10]); } }
2.websocket也支持ArrayBuffer類型的數據接收
var websocket = new WebSocket(sUri); websocket.binaryType = "arraybuffer" ;
3.用於在前端創建blob對象,file對象下載or上傳
4.瀏覽器雖然無權限讀取本地文件,但是有權限向本地寫入文件,寫入文件到本地時可能會用到
5.視頻音頻播放(寫文件和播放視音頻我沒有驗證)
后面我會學習這些使用形式,到時候再做總結~
nodejs中的buffer和瀏覽器中的Uint8Array的關系

可見,nodejs中實現的Buffer實際上是Uint8Arr數組的一個子類,是nodejs為了進一步提升JavaScript的二進制數據處理能力而封裝的一個類。
實際上ArrayBuffer和類型化數組也並不是瀏覽器環境下獨有的東西,他們是es5規范里邊的內容,在nodejs環境下也可以使用,例如:

————————————————end———————————————
以上內容,如有錯誤,歡迎斧正!