假設我們現在要蓋一座房子,我們買了一些磚塊,廠家正在送貨。現在我們有兩個選擇,一是等所有磚塊都到了以后再開始動工;二是到一批磚塊就開始動工,磚塊到多少我們就用多少。
這兩種方式哪種效率更高呢?顯然是第二種。這就是流(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 。