二進制是計算技術中普遍採用的一種數制。二進制數據是用0和1兩個數碼來表示的數。它的基數爲2,進位規則是「逢二進一」,借位規則是「借一當二」,由18世紀德國數理哲學大師 萊布尼茲發現。—— 百度百科css
二進制數據就像上圖同樣,由0和1來存儲數據。普通的十進制數轉化成二進制數通常採用"除2取餘,逆序排列"法,用2整除十進制整數,能夠獲得一個商和餘數;再用2去除商,又會獲得一個商和餘數,如此進行,直到商爲小於1時爲止,而後把先獲得的餘數做爲二進制數的低位有效位,後獲得的餘數做爲二進制數的高位有效位,依次排列起來。例如,數字10轉成二進制就是1010
,那麼數字10在計算機中就以1010
的形式存儲。html
而字母和一些符號則須要經過 ASCII 碼來對應,例如,字母a對應的 ACSII 碼是 97,二進制表示就是0110 0001
。JavaScript 中可使用 charCodeAt
方法獲取字符對應的 ASCII:前端
除了ASCII外,還有一些其餘的編碼方式來映射不一樣字符,好比咱們使用的漢字,經過 JavaScript 的 charCodeAt 方法獲得的是其 UTF-16
的編碼。node
JavaScript 在誕生初期主要用於表單信息的處理,因此 JavaScript 天生擅長對字符串進行處理,能夠看到 String 的原型提供特別多便利的字符串操做方式。linux
可是,在服務端若是隻能操做字符是遠遠不夠的,特別是網絡和文件的一些 IO 操做上,還須要支持二進制數據流的操做,而 Node.js 的 Buffer 就是爲了支持這些而存在的。好在 ES6 發佈後,引入了類型數組(TypedArray)的概念,又逐步補充了二進制數據處理的能力,如今在 Node.js 中也能夠直接使用,可是在 Node.js 中,仍是 Buffer 更加適合二進制數據的處理,並且擁有更優的性能,固然 Buffer 也能夠直接看作 TypedArray 中的 Uint8Array。除了 Buffer,Node.js 中還提供了 stream 接口,主要用於處理大文件的 IO 操做,相對於將文件分批分片進行處理。git
Buffer 直譯成中文是『緩衝區』的意思,顧名思義,在 Node.js 中實例化的 Buffer 也是專門用來存放二進制數據的緩衝區。一個 Buffer 能夠理解成開闢的一塊內存區域,Buffer 的大小就是開闢的內存區域的大小。下面來看看Buffer 的基本使用方法。github
早期的 Buffer 經過構造函數進行建立,經過不一樣的參數分配不一樣的 Buffer。shell
建立大小爲 size(number) 的 Buffer。json
new Buffer(5) // <Buffer 00 00 00 00 00>
使用八位字節數組 array 分配一個新的 Buffer。gulp
const buf = new Buffer([0x74, 0x65, 0x73, 0x74]) // <Buffer 74 65 73 74> // 對應 ASCII 碼,這幾個16進制數分別對應 t e s t // 將 Buffer 實例轉爲字符串獲得以下結果 buf.toString() // 'test'
拷貝 buffer 的數據到新建的 Buffer 實例。
const buf1 = new Buffer('test') const buf2 = new Buffer(buf1)
建立內容爲 string 的 Buffer,指定編碼方式爲 encoding。
const buf = new Buffer('test') // <Buffer 74 65 73 74> // 能夠看到結果與 new Buffer([0x74, 0x65, 0x73, 0x74]) 一致 buf.toString() // 'test'
因爲 Buffer 實例因第一個參數類型而執行不一樣的結果,若是開發者不對參數進行校驗,很容易致使一些安全問題。例如,我要建立一個內容爲字符串 "20"
的 Buffer,而錯誤的傳入了數字 20
,結果建立了一個長度爲 20 的Buffer 實例。
能夠看到上圖,Node.js 8 以前,爲了高性能的考慮,Buffer 開闢的內存空間並未釋放以前已存在的數據,直接將這個 Buffer 返回可能致使敏感信息的泄露。所以,Buffer 類在 Node.js 8 先後有一次大調整,再也不推薦使用 Buffer 構造函數實例 Buffer,而是改用Buffer.from()
、Buffer.alloc()
與 Buffer.allocUnsafe()
來替代 new Buffer()
。
該方法用於替代 new Buffer(string)
、new Buffer(array)
、new Buffer(buffer)
。
該方法用於替代 new Buffer(size)
,其建立的 Buffer 實例默認會使用 0 填充內存,也就是會將內存以前的數據所有覆蓋掉,比以前的 new Buffer(size)
更加安全,由於要覆蓋以前的內存空間,也意味着更低的性能。
同時,size 參數若是不是一個數字,會拋出 TypeError。
該方法與以前的 new Buffer(size)
保持一致,雖然該方法不安全,可是相比起 alloc
具備明顯的性能優點。
前面介紹過二進制數據與字符對應須要指定編碼,同理將字符串轉化爲 Buffer、Buffer 轉化爲字符串都是須要指定編碼的。
Node.js 目前支持的編碼方式以下:
hex
:將每一個字節編碼成兩個十六進制的字符。ascii
:僅適用於 7 位 ASCII 數據。此編碼速度很快,若是設置則會剝離高位。utf8
:多字節編碼的 Unicode 字符。許多網頁和其餘文檔格式都使用 UTF-8。utf16le
:2 或 4 個字節,小端序編碼的 Unicode 字符。ucs2
:utf16le
的別名。base64
:Base64 編碼。latin1
:一種將 Buffer
編碼成單字節編碼字符串的方法。binary
:latin1
的別名。比較經常使用的就是 UTF-8
、UTF-16
、ASCII
,前面說過 JavaScript 的 charCodeAt
使用的是 UTF-16
編碼方式,或者說 JavaScript 中的字符串都是經過 UTF-16
存儲的,不過 Buffer 默認的編碼是 UTF-8
。
能夠看到一個漢字在 UTF-8
下須要佔用 3 個字節,而 UTF-16
只須要 2 個字節。主要緣由是 UTF-8
是一種可變長的字符編碼,大部分字符使用 1 個字節表示更加節省空間,而某些超出一個字節的字符,則須要用到 2 個或 3 個字節表示,大部分漢字在 UTF-8
中都須要用到 3 個字節來表示。UTF-16
則所有使用 2 個字節來表示,對於一下超出了 2 字節的字符,須要用到 4 個字節表示。 2 個字節表示的 UTF-16
編碼與 Unicode 徹底一致,經過漢字Unicode編碼表能夠找到大部分中文所對應的 Unicode 編碼。前面提到的 『漢』,經過 Unicode 表示爲 6C49
。
這裏提到的 Unicode 編碼又被稱爲統一碼、萬國碼、單一碼,它爲每種語言都設定了統一且惟一的二進制編碼,而上面說的 UTF-8
、UTF-16
都是他的一種實現方式。更多關於編碼的細節再也不贅述,也不是本文的重點,若是想了解更多可自行搜索。
咱們常常會出現一些亂碼的狀況,就是由於在字符串與 Buffer 的轉化過程當中,使用了不一樣編碼致使的。
咱們先新建一個文本文件,而後經過 utf16 編碼保存,而後經過 Node.js 讀取改文件。
const fs = require('fs') const buffer = fs.readFileSync('./1.txt') console.log(buffer.toString())
因爲 Buffer 在調用 toString 方法時,默認使用的是 utf8 編碼,因此輸出了亂碼,這裏咱們將 toString 的編碼方式改爲 utf16 就能夠正常輸出了。
const fs = require('fs') const buffer = fs.readFileSync('./1.txt') console.log(buffer.toString('utf16le'))
前面咱們說過,在 Node.js 中能夠利用 Buffer 來存放一段二進制數據,可是若是這個數據量很是的大使用 Buffer 就會消耗至關大的內存,這個時候就須要用到 Node.js 中的 Stream(流)。要理解流,就必須知道管道的概念。
在 類Unix 操做系統(以及一些其餘借用了這個設計的操做系統,如Windows)中, 管道是一系列將 標準輸入輸出連接起來的 進程,其中每個進程的 輸出被直接做爲下一個進程的 輸入。 這個概念是由 道格拉斯·麥克羅伊爲 Unix 命令行發明的,因與物理上的 管道類似而得名。-- 摘自維基百科
咱們常常在 Linux 命令行使用管道,將一個命令的結果傳輸給另外一個命令,例如,用來搜索文件。
ls | grep code
這裏使用 ls
列出當前目錄的文件,而後交由 grep
查找包含 code
關鍵詞的文件。
在前端的構建工具 gulp
中也用到了管道的概念,由於使用了管道的方式來進行構建,大大簡化了工做流,用戶量一會兒就超越了 grunt
。
// 使用 gulp 編譯 scss const gulp = require('gulp') const sass = require('gulp-sass') const csso = require('gulp-csso') gulp.task('sass', function () { return gulp.src('./**/*.scss') .pipe(sass()) // scss 轉 css .pipe(csso()) // 壓縮 css .pipe(gulp.dest('./css')) })
前面說了這麼多管道,那管道和流直接應該怎麼聯繫呢。流能夠理解爲水流,水要流向哪裏,就是由管道來決定的,若是沒有管道,水也就不能造成水流了,因此流必需要依附管道。在 Node.js 中全部的 IO 操做均可以經過流來完成,由於 IO 操做的本質就是從一個地方流向另外一個地方。例如,一次網絡請求,就是將服務端的數據流向客戶端。
const fs = require('fs') const http = require('http') const server = http.createServer((request, response) => { // 建立數據流 const stream = fs.createReadStream('./data.json') // 將數據流經過管道傳輸給響應流 stream.pipe(response) }) server.listen(8100)
// data.json { "name": "data" }
使用 Stream 會一邊讀取 data.json
一邊將數據寫入響應流,而不是像 Buffer 同樣,先將整個 data.json
讀取到內存,而後一次性輸出到響應中,因此使用 Stream 的時候會更加節約內存。
其實 Stream 在內部依然是運做在 Buffer 上。若是咱們把一段二進制數據比作一桶水,那麼經過 Buffer 進行文件傳輸就是直接將一桶水倒入到另外一個桶裏面,而使用 Stream,就是將桶裏面的水經過管道一點點的抽取過去。
這裏若是隻是口頭說說可能感知不明顯,如今分別經過 Stream 和 Buffer 來複制一個 2G 大小的文件,看看 node 進程的內存消耗。
// Stream 複製文件 const fs = require('fs'); const file = './file.mp4'; fs.createReadStream(file) .pipe(fs.createWriteStream('./file.copy.mp4')) .on('finish', () => { console.log('file successfully copy'); })
// Buffer 複製文件 const fs = require('fs'); const file = './file.mp4'; // fs.readFile 直接輸出的是文件 Buffer fs.readFile(file, (err, buffer) => { fs.writeFile('./file.copy.mp4', buffer, (err) => { console.log('file successfully copy'); }); });
經過上圖的結果能夠看出,經過 Stream 拷貝時,只佔用了我電腦 0.6% 的內存,而使用 Buffer 時,佔用了 15.3% 的內存。
在 Node.js 中,Steam 一共被分爲五種類型。
全部的流均可以經過 .pipe
也就是管道(相似於 linux 中的 |
)來進行數據的消費。另外,也能夠經過事件來監聽數據的流動。不論是文件的讀寫,仍是 http 的請求、響應都會在內部自動建立 Stream,讀取文件時,會建立一個可讀流,輸出文件時,會建立可寫流。
雖然叫作可讀流,可是可讀流也是可寫的,只是這個寫操做通常是在內部進行的,外部只須要讀取就好了。
可讀流通常分爲兩種模式:
stram.read()
。可讀流在建立時,默認爲暫停模式,一旦調用了 .pipe
,或者監聽了 data
事件,就會自動切換到流動模式。
const { Readable } = require('stream') // 建立可讀流 const readable = new Readable() // 綁定 data 事件,將模式變爲流動模式 readable.on('data', chunk => { console.log('chunk:', chunk.toString()) // 輸出 chunk }) // 寫入 5 個字母 for (let i = 97; i < 102; i++) { const str = String.fromCharCode(i); readable.push(str) } // 推入 `null` 表示流已經結束 readable.push(null)
const { Readable } = require('stream') // 建立可讀流 const readable = new Readable() // 寫入 5 個字母 for (let i = 97; i < 102; i++) { const str = String.fromCharCode(i); readable.push(str) } // 推入 `null` 表示流已經結束 readable.push('\n') readable.push(null) // 經過管道將流的數據輸出到控制檯 readable.pipe(process.stdout)
上面的代碼都是手動建立可讀流,而後經過 push
方法往流裏面寫數據的。前面說過,Node.js 中數據的寫入都是內部實現的,下面經過讀取文件的 fs 建立的可讀流來舉例:
const fs = require('fs') // 建立 data.json 文件的可讀流 const read = fs.createReadStream('./data.json') // 監聽 data 事件,此時變成流動模式 read.on('data', json => { console.log('json:', json.toString()) })
#### 可寫流(Writable)
可寫流對比起可讀流,它是真的只能寫,屬於只進不出的類型,相似於貔貅。
建立可寫流的時候,必須手動實現一個 _write()
方法,由於前面有下劃線前綴代表這是內部方法,通常不禁用戶直接實現,因此該方法都是在 Node.js 內部定義,例如,文件可寫流會在該方法中將傳入的 Buffer
寫入到指定文本中。
寫入若是結束,通常須要調用可寫流的 .end()
方法,表示結束本次寫入,此時還會調用 finish
事件。
const { Writable } = require('stream') // 建立可寫流 const writable = new Writable() // 綁定 _write 方法,在控制檯輸出寫入的數據 writable._write = function (chunk) { console.log(chunk.toString()) } // 寫入數據 writable.write('abc') // 結束寫入 writable.end()
_write
方法也能夠在實例可寫流的時候,經過傳入對象的 write
屬性來實現。
const { Writable } = require('stream') // 建立可寫流 const writable = new Writable({ // 同,綁定 _write 方法 write(chunk) { console.log(chunk.toString()) } }) // 寫入數據 writable.write('abc') // 結束寫入 writable.end()
下面看看 Node.js 中內部經過 fs 建立的可寫流。
const fs = require('fs') // 建立可寫流 const writable = fs.createWriteStream('./data.json') // 寫入數據,與本身手動建立的可寫流一致 writable.write(`{ "name": "data" }`) // 結束寫入 writable.end()
看到這裏就能理解,Node.js 在 http 響應時,須要調用 .end()
方法來結束響應,其實內部就是一個可寫流。如今再回看前面經過 Stream 來複制文件的代碼就更加容易理解了。
const fs = require('fs'); const file = './file.mp4'; fs.createReadStream(file) .pipe(fs.createWriteStream('./file.copy.mp4')) .on('finish', () => { console.log('file successfully copy'); })
雙工流同時實現了 Readable 和 Writable,具體用法能夠參照可讀流和可寫流,這裏就不佔用文章篇幅了。
前面介紹了經過管道(.pipe()
)能夠將一個桶裏的數據轉移到另外一個桶裏,可是有多個桶的時候,咱們就須要屢次調用 .pipe()
。例如,咱們有一個文件,須要通過 gzip 壓縮後從新輸出。
const fs = require('fs') const zlib = require('zlib') const gzip = zlib.createGzip() // gzip 爲一個雙工流,可讀可寫 const input = fs.createReadStream('./data.json') const output = fs.createWriteStream('./data.json.gz') input.pipe(gzip) // 文件壓縮 gzip.pipe(output) // 壓縮後輸出
面對這種狀況,Node.js 提供了 pipeline()
api,能夠一次性完成多個管道操做,並且還支持錯誤處理。
const { pipeline } = require('stream') const fs = require('fs') const zlib = require('zlib') const gzip = zlib.createGzip() const input = fs.createReadStream('./data.json') const output = fs.createWriteStream('./data.json.gz') pipeline( input, // 輸入 gzip, // 壓縮 output, // 輸出 // 最後一個參數爲回調函數,用於錯誤捕獲 (err) => { if (err) { console.error('壓縮失敗', err) } else { console.log('壓縮成功') } } )