假設我們現在要蓋一座房子,我們買了一些磚塊,廠家正在送貨。現在我們有兩個選擇,一是等所有磚塊都到了以後再開始動工;二是到一批磚塊就開始動工,磚塊到多少我們就用多少。
這兩種方式哪種效率更高呢?顯然是第二種。這就是流(stream)的理念。在電腦科學中,流是隨時間可用的一系列資料元素。就像傳送帶運輸物品一樣,使用流可以實現一次處理一個數據元素。
在 NodeJS 中,stream 模組實現了流的功能。即使我們沒有直接使用過這個模組,我們也間接使用過流,比如讀寫檔案、網路等。
水流,資訊流
資訊就像水流一樣,以位元流(strem of bits)的形式從一個地方流到另一個地方。比如讀取檔案,資訊就從磁碟流向了應用程式。
但是,流的兩端處理資訊的速度是不同的,通常流的一端會比另一端要慢,因此就需要一個快取來作為緩衝(buffer)。
如下圖所示,上面的水龍頭水流較大,下面的水龍頭水流較小,因此需要一個容器來暫時儲存下面的水龍頭來不及處理的水。
NodeJS 中流的基本原理也是這樣的,stream 模組實現了這些能力。
在 NodeJS 中有兩種基本的流可以使用:
- 可讀流(Readable Streams)
- 可寫流(Writable Streams)
同時還有兩種讀寫混合的流:
- 雙工流(Duplex Streams)-- 可讀可寫的流
- 轉換流(Transform Streams)-- 可以轉換資料的雙工流
可讀流(Readable Stream)
可讀流可以從一個地方讀取資料,比如從檔案中讀取資訊。讀取的資料可以暫時存放在可讀流中的快取(Buffer)裡,防止應用程式無法及時處理。
常見的可讀流有 process.stdin
、fs.createReadStream
以及 HTTP 服務中的 IncomingMessage
物件。
可寫流(Writable Stream)
可寫流可以將資料寫到一個地方,比如將資料寫入檔案中。為了防止因為寫入目標處理速度太慢導致資料丟失,寫入的資料可以暫存在可寫流內部的快取(Buffer)中。
常用的可寫流有 process.stdout
、process.stderr
和 fs.createWriteStream
.
雙工流(Duplex Streams)
雙工流是可讀流和可寫流的混合體。連線到雙工流之後,應用程式既可以從流中讀取資料,也可以向流中寫入資料。在雙工流中,可讀流和可寫流有各自獨立的快取(Buffer)。
最常用的雙工流就是 net.Socket
。
轉換流(Transform Stream)
轉換流是更加特殊的混合流,在轉換流中,可讀流是通過某種方式連線到可寫流上的。
最常見的轉換流是有 Cipher
建立的流。在這個流中,應用程式寫入資料,然後再從流中讀取加密後的資料。
管道(Pipe)
通常流在連線到一起之後才能發揮更大的作用。我們通過管道來連線流。
比如我們可以將一個可讀流連線到一個可寫流或者雙工流上,僅僅使用可讀流的 pipe()
方法即可。
常見的管道場景就是複製檔案。將 fs.createReadStream()
建立的流通過 pipe()
方法連線到 fs.createWriteStream()
建立的流上去。
使用流複製資料
我們可以將流連線到多個其他流上。這在一些需要重複讀取原始資料的場景中非常有用。因為可讀流只能讀取一次資料,因此我們可以通過 pipe()
方法將可讀流連線到多個流上,這樣這些被連線的流就可以直接消費資料,不需要建立多個可讀流。
const fs = require('fs')
const original = fs.createReadStream('./original.txt')
const copy1 = fs.createWriteStream('./copy1.txt')
const copy2 = fs.createWriteStream('./copy2.txt')
original.pipe(copy1)
original.pipe(copy2)
高水位線控制(highWaterMark)
在最開始的例子中,我們通過水箱蓄水的例子描述了流的快取特性。因為上方的水流始終比下方的水流快,水箱中的水越來越多,終究會超過水箱的容積而溢位。
因此我們需要一個高水位警戒線,當水箱中的水位高於這個警戒線的時候,就需要通知上方的水龍頭暫時停止放水了。
在流中也是同樣的原理,可讀流和可寫流內部都有快取,這些快取的最大可儲存量是系統的可用記憶體量。NodeJS 流通過 highWaterMark
這個配置項來控制快取中的水位線。
舉個例子,如下圖,可讀流連線到可寫流之後,可寫流通過 highWaterMark
來檢測快取中的水位是否過高,高於這條線之後,可寫流會通知可讀流暫停寫入資料。
需要注意的是,highWaterMark
只是一個警示線,並不是一個硬性約束條件。也就是說,如果自定義的流沒有正確處理這個警示線的話,可能會導致資料丟失。
流的應用
我們來舉個例子綜合說明如何使用流。
假設我們有一個裁減圖片的應用程式。使用者將圖片的地址告訴應用程式,應用程式從網路上讀取原始圖片,裁減之後再返回給使用者。那麼我們可以藉助於流來實現這個應用程式的功能,如下圖。
常見面試知識點、技術解決方案、教程,都可以掃碼關注公眾號“眾裡千尋”獲取,或者來這裡 https://everfind.github.io 。