? ? 流是Node中最重要的組件和模式之一。在社區里有一句格言說:讓一切事務流動起來。這已經足夠來描述在Node中流的角色。Dominic Tarr,Node社區中頂尖的貢獻者,將流定義為node中最佳并且最難理解的概念。這里使Node的流機制如此吸引人的原因有很多,再次強調,它并不僅僅與技術屬性相關,比如說性能和效率,更進一步來說更加關注其優雅程度和它的方式是否完美適用于Node的哲學思想。
\ ? 在這篇文章中你將了解如下話題:
? ? ?為什么流機制在Node中如此重要?
? ? ?使用并創造流
? ? ?流式作為一種編程范式,在各種不同的上下文而不僅僅是I/O情境下使用它的能力
? ? ?在不同的配置下,管道模式與流連接相結合
Discovering the importance of streams
? ? 在一個基于事件機制的平臺比如Node中,最高效處理I/O業務的方式是實時模式,對輸入變量一旦其提供資源就盡快進行消費,一旦應用產生輸出值就盡快把它發出。
? ? 在本節,我們將初步介紹Node流機制和它們的優勢。請記住,這只是一個概述,之后的章節中會有更細節的分析來介紹如何使用和構建流。
Buffering vs Streaming
????幾乎所有至今我們在這本書所看到的異步API所工作的時候都采用緩沖區模式(buffer mode)。對于一個輸入操作,緩沖區模式(buffer mode)導致從一個資源收集到的所有數據被注入一個緩沖區中,一旦整個資源被讀取則緩沖區中的數據將會被傳遞給回調函數(callback),接下來展示的虛擬模式代表了這種編程范式:
????在前面的圖中,我們可以看到,在t1時刻時,一些數據從資源中被收集,并被保存在緩沖區。在時刻t2時,另一個數據塊(最后一個)被接收,它結束了讀取操作并導致整個緩沖區被發送給消費者。????
????另一方面而言,流機制允許你當數據從資源到達時對該數據進行處理,這個過程如下圖所示:
????此刻,上圖展示了數據中的每個分塊由資源發出來被接收并且立刻被提供給消費者,現在消費者擁有機會來立即處理這些數據分塊而不需要等待所有的數據在緩沖區被收集。
????但是針對這兩種方式之間的主要區別,我們可以總結為2個主方面:
????? 空間效率
????? 時間效率
????然而,Node中的流式具有另外一方面很重要的優勢:可組合性。讓我們一起來看看,這些特性對我們設計和編寫我們應用的方式會造成哪些影響。
Spatial efficiency
????首先,流式允許我們去做一些,緩沖數據并一次性處理模式不可能做到的事情。例如,我們考慮一種業務情境:我們必須讀取一個非常大的文件,比如說,幾百MB甚至是GB的數量級規模。顯然,使用API來在該文件被完全讀取時返回?一個很大的緩沖區buffer,這并不是一個好主意。想象一下,如果并發的讀取一批這樣的大文件,我們的應用將會很容易造成內存耗盡。除此之外,緩沖區buffer在V8引擎中不能超過0x3FFFFFFFbytes(略小于1GB)。所以我們可能在耗盡物理內存之前,就遭遇屏障。
Gzipping using a buffered API
????作為一個具體例子,讓我們來考慮一個簡單的命令行接口CLI應用程序,通過Gzip方式來實現文件壓縮。使用一個緩沖模式buffered的API,這樣一個應用的代碼將以Node形式展示為以下代碼(錯誤處理略為簡潔):
var fs = require('fs');
var zlib = require('zlib');
var file = process.argv[2];
fs.readFile(file, function(err, buffer) {
????zlib.gzip(buffer, function(err, buffer) {
????????fs.writeFile(file + '.gz', buffer, function(err) {
????????????console.log('File successfully compressed');
????????});
????});
});????
????現在,我們可以把之前代碼放進一個名為gzip.js?的文件中,并且通過以下命令行運行:
node gzip <path to file>
????如果我們選擇一個足夠大的文件,比如說稍微大于1 GB,我們將收到一個很好的錯誤信息,來告訴我們:嘗試去讀取的文件,比允許的最大緩沖區大小要大,結果如下:
RangeError: File size is greater than possible Buffer: 0x3FFFFFFF bytes
????這正是我們所預期的結果,并且這個結果代表了我們使用了錯誤的方式。
Gzipping using streams
????最簡單的方式來修復我們的壓縮應用程序并且使其能夠針對大文件工作的方式是使用流式API。讓我們一起來看看這種方式是如何實現的。我們把剛才所寫模塊中的內容替換為以下代碼:
var fs = require('fs');
var zlib = require('zlib');
var file = process.argv[2];
?fs.createReadStream(file)
? ? .pipe(zlib.createGzip())
? ? .pipe(fs.createWriteStream(file + '.gz'))
? ? .on('finish', function() {
? ? ? ? console.log('File successfully compressed');
? ? ?});
????是這種方式么?你可能會產生疑問。是的,正如我們所介紹的,流式是可以帶來驚喜的,因為其擁有的接口化和可組合性,由此可以組織干凈、優雅、簡潔的代碼。我們將在一段時間之后更細節的看到它的效果,但是現在最重要的事情是理解這種方式可以針對任何規模size的文件來完美運行程序,針對內存利用率不斷優化。請自己嘗試一下(但是考慮到壓縮一個大文件需要花一些時間)。
Time efficiency
????讓我們現在來一起考慮一個應用程序來壓縮文件然后將其上傳到一個遠程的HTTP服務器,在其上解壓縮并且將其存儲在文件系統上。如果我們的客戶端是以緩沖模式API實現的話,上傳過程只能在整個文件被讀取和壓縮之后開始。從另一個方面來說,解壓過程將會在所有的數據被讀取之后再服務器端開始。更好的解決方案來獲得相同的結果,涉及使用流模式。在客戶端機制中,流模式允許你一旦能夠從文件系統讀取到數據塊就將它發送出去。與此同時,在服務器端,它能夠允許你去在它從其它遠程服務器接收到數據時,去壓縮每一個數據塊。為了證明這一觀點,我們一起來構建以前提到過的一個應用程序,由服務器端開始:
????讓我們一起創建一個名為gzipReceive.js的模塊,并在其中編寫以下代碼:
var http = require('http');
var fs = require('fs');
var zlib = require('zlib');
var server = http.createServer(function (req, res) {
????var filename = req.headers.filename;
????console.log('File request received: ' + filename);
????req
????.pipe(zlib.createGunzip())
????.pipe(fs.createWriteStream(filename))
????.on('finish', function() {
????????res.writeHead(201, {'Content-Type': 'text/plain'});
????????res.end('That\'s it\n');
????????console.log('File saved: ' + filename);
????});
});
server.listen(3000, function () {
????console.log('Listening');
});
????通過Node的流式機制,服務器由網絡接收數據塊,并且一旦完成接受就將它們壓縮并保存。
????我們的應用程序中的客戶端代碼,將被注入一個名為gzipSend.js的模塊,并且其內容如下所示:
var fs = require('fs');
var zlib = require('zlib');
var http = require('http');
var path = require('path');
var file = process.argv[2];
var server = process.argv[3];
var options = {
????hostname: server,
????port: 3000, path: '/',
????method: 'PUT',
????headers: {
????????filename: path.basename(file),
????????'Content-Type': 'application/octet-stream' ,
????????'Content-Encoding': 'gzip'
????}
};
var req = http.request(options, function(res) {
????console.log('Server response: ' + res.statusCode);
});
fs.createReadStream(file)
????.pipe(zlib.createGzip())
????.pipe(req)
????.on('finish', function() {
????????console.log('File successfully sent');
????});
????在上述代碼中,我們再次使用流模式來從文件中讀取數據,并且針對每個數據塊當其從文件系統完成讀取時,進行壓縮和發送流程。????
????現在,來嘗試運行我們的應用程序,使用以下命令來嘗試運行我們的服務器:
node gzipReceive
????之后,我們將以的方式指定所要發送的文件和服務器地址(比如:localhost)的方式,來部署客戶端程序:
node gzipSend <path to file> localhost
????如果我們選擇了一個足夠大的文件,我們將更容易觀察到數據流如何從客戶端向文件流傳輸,但是為什么在這種模式下,相對于緩沖(buffer)API模式下明顯更加高效?以下示意圖將給我們一個提示:
????當一個文件被處理時,它經過以下序列狀態:
????1.[Client]由文件系統讀取
????2.[Client]壓縮數據
????3.[Client]將壓縮之后的數據發送給server
????4.[Server]由客戶端接收
????5.[Server]解壓縮這份數據
????6.[Server]將這份數據寫入硬盤
????為了完成這個處理過程,我們不得不經像一個流水線一樣來經過每一個狀態步驟,串行序列化的依次進行,直到結束。在前面的圖例中,我們可以看到,使用一個緩沖buffer模式的API,流程整體時串行序列化的。為了壓縮數據,我們首先要等待整個文件被讀取,之后,去發送數據,我們需要等待直到整個文件不僅被讀取而且被壓縮,此后,我們將代替原有模式使用流模式,當我們接收到第一個數據塊時,整個流水線將會發起進行,而不需要等待整個文件被讀取。但是更加令人驚訝的是,當下一個數據塊已經可以提供時,在這里不需要等待之前的一系列任務被完成,而是,另外一條流水線并行的被部署。這種機制工作的很好,因為我們所執行的每一個業務都是異步的,所以任務可以被Node并行的處理。唯一的限制是:數據塊到達每個狀態的順序需要被保留(并且Node的流模式為我們專門處理了這個部分)。
????正如我們在之前數據所看到的那樣,使用流模式的結果是:整個處理流程使用更少的時間,因為我們不再浪費時間來等待所有數據被讀取然后全部數據一次性處理。
Composability
????迄今為止,我們所看到的代碼給我們展示了一個關于流模式如何組成的全景,感謝pipe()方法。通過pipe()方法,能夠允許我們連接不同的處理單元,每個處理單元采用完美的Node風格針對單獨的函數進行負責。這可能是因為流模式下具備一套統一的接口標準,可以依據API讓不同的處理模塊理解對方。唯一的先決條件是:在流水線上的下一個流的必須要支持之前流所創建的數據類型,這種數據類型可能涵蓋二進制,文本,甚至對象,如同我們將在這節稍后內容所看到的一樣:
? ? 為了來觀察到另一個關于這個屬性能力的證明,我們可以添加一個加密層到我們之前建立的gzipReceive/gzipSend應用程序上去。為了達到這一目的,我們只需要更新客戶端來將另外一個流加入流水線。更確切地說,是由crypto.createChipher()所返回的流。結果代碼如下所示:
var crypto = require('crypto');
[...]
fs.createReadStream(file)
????.pipe(zlib.createGzip())
????.pipe(crypto.createCipher('aes192', 'a_shared_secret'))
????.pipe(req)
????.on('finish', function() {
????????console.log('File succesfully sent');
????});
? ? 通過一個相似方式,我們更新服務器端,來使數據在被解壓縮之前,首先進行解密:
var crypto = require('crypto');
[...]
var server = http.createServer(function (req, res) {
????[...]
????????req
????????????.pipe(crypto.createDecipher('aes192', 'a_shared_secret')) ????????????.pipe(zlib.createGunzip())
????????????.pipe(fs.createWriteStream(filename))
????????????.on('finish', function() {
????????????????[...]
????????????});
});
? ? 只需要一點工作量(僅僅加入幾行代碼),我們在應用程序的基礎上加入了一層加密層。我們僅僅通過在我們已有的流水線上復用一個已經可以提供的傳輸流。通過一種相似方式,我們可以加入或者結合其他流,如圖我們玩樂高積木一樣。
? ? 很顯然,這種方式的主要好處是可重用性,但是從我們所列舉出的代碼來看,流模式也同樣使得代碼更加干凈并且更加模塊化?;谝陨显?,流不僅僅用于處理I/O,,也是一種方式來簡化和模塊化代碼。
Getting started with streams
? ? 在之前小節,我們了解了為什么流模式如此強大,并且它的應用遍及Node的各處,由它的核心模塊開始。舉個例子,我們看到fs模塊具有createReadStream()函數來讀取一個文件,createWriteStream()函數來寫入一個文件,http請求和應答的對象是關鍵的流,并且zlib模塊允許我們通過使用一個流接口來壓縮和解壓縮數據。
? ? 現在我們知道為什么流模式如此重要,讓我們退回一步來開始更細節的探索這些特性。
Anatomy of streams
? ? 在Node中的每一個流是stream核心模塊所提供的4個基礎抽象類中之一的實現:
????? stream.Readable
????? stream.Writable
????? stream.Duplex
????? stream.Transform
? ? 每一個stream類同時也是EventEmitter的一個實例。流,實際上,可以創造很多類型的事件,比如說end(當一個可讀的流完成讀?。?,或者error(當一些情況下出錯)。
? ? 請注意,為了簡潔起見,在本節所呈現的例子中,我們往往會忽略適當的錯誤管理。然而,在生產應用程序中,常常建議去注冊一個error錯誤時間監聽器來針對于你所涉及的所有流。
? ? 流模式之所如此靈活的一個原因是基于事實上它不僅僅可以處理二進制數據,但是事實上,幾乎任何的JavaScript值,實際上都可以支持兩種操作模式。
????? 二進制模式: 這種模式是指在其中數據被以塊的形式流化,比如緩沖區或者字符串形式。
????? 對象模式: 這種模式中流數據被當做一個嚴謹對象(被允許使用幾乎所有的JavaScript值)的序列。
? ? 這兩種模式下允許我們去使用流不僅僅針對于I/O業務情境,同時作為一種工具來一種函數式風格來優雅的組成處理單元,如我們稍后將在本節所看到的一樣。
? ? 在本節中,我們主要討論了被認識為Version 2的流接口,這種流接口從Node 0.10版本本引入。跟進一步細節的來比對和老接口的區別:
????請參考官方Node的博客:http://blog.nodejs.org/2012/12/20/ streams2/.
Readable streams
? ? 一個可讀流代表一個數據源:在Node中,它是通過使用stream模塊中所提供的Readable抽象類來實現的。
Reading from a stream
? ? 這里有兩種方式來接收Readable流的數據:non-flowing and flowing。讓我們以更細節的方式來分析這些模式。
The non-flowing mode
? ? 從一個可讀Readable流中讀取的默認模式,包括針對于readable事件添加一個監聽器,這個事件指示新數據是否可供讀取。然后,在循環中,我們將讀取所有的數據直到內部緩沖區被清空。這個過程可以通過使用read()方法來完成,這個方法可以同步的從內部緩沖區讀取數據,并返回Buffer 或者 String類型對象,來代表數據塊。read()方法呈現以下寫法:
readable.read([size])?
????使用這種方式,數據是根據需要來確定從流中拉出來的。
? ? 為了說明這個過程是如何工作的,讓我們創建一個名為readStdin.js的新模塊,在其中實現了一個簡單的程序,它能夠從標準輸入(a Readable stream)中讀取,并回到標準輸出將其中的所有內容echoes。
process.stdin
? ??.on('readable', function() {
? ??????var chunk;
????????console.log('New data available');
? ??????while((chunk = process.stdin.read()) !== null) {
????????????console.log(
? ??????????????'Chunk read: (' + chunk.length + ') "' +
? ??????????????chunk.toString() + '"'
? ? ? ? );
????}
})
.on('end', function() {
????process.stdout.write('End of stream');
});
? ??read()方法是一個同步操作,從一個可讀Readable流的內部緩沖區中來出數據塊。所返回的數據塊(默認情況下),如果該流以二進制模式而工作時,是一個Buffer對象。
? ? 在一個以二進制模式工作的可讀Readable流中,我們可以讀取字符串strings而不是緩沖區內容buffers,以在流中調用setEncoding(encoding)的方式來實現,并提供一個有效的編碼格式(比如:utf8)。
? ? 數據可以在可讀監聽器內部唯一的讀取,當新數據可讀時就可以調用它。read()方法會返回null當內部緩沖區內沒有更多的數據處于可提供的狀態,在這樣的業務情境下,我們不得不等到另外的可讀事件被觸發-來告訴我們可以再次讀取-或者等到最后的事件來標識流的結束。當一個流在二進制狀態下工作時,我們同樣可以指定我們有興趣去讀取一個特定數量的數據,來通過傳遞一個size值到read()方法。這種特別有用,當所實現的網絡協議或者解析一個特殊的數據格式。
? ? 現在,我們已經準備好去運行readStdin模塊并且針對它做實驗。讓我們一起在console控制臺中鍵入一些字符并且按下Enter鍵來看看在標準輸出中回顯echoed的數據。為了終止流,進而生成一種優雅的結束事件,我們需要插入一個EOF (End-Of-File)字符,windows上(使用Ctrl + Z),Linux上(使用Ctrl + D)。
? ? 我們可以嘗試把我們的程序與其他處理過程連接:這個步驟很可能使用管道操作符pipe operator (|),這個符號可以重定向一個程序的標準輸出到另外一個程序的標準輸入。例如,我們可以運行一個命令,如下所示:
cat ?<path to a file> | node readStdin
? ? 這是個令人激動地演示關于流式編程范式是如何成為一種通用接口標準的,這種情況將使我們的程序之間能夠通信,而不用考慮它們所編寫基于的程序語言。
The flowing mode
? ? 另外一個方式從流中讀取數據,是通過針對data事件,加入一個監聽器來實現的。這種實現方法將會轉變流進入使用流flowing的模式,在其中數據將不會再通過read()方法進行拉取pull,而是每當新的可供使用的數據到達,就將其推送push到data監聽器端。例如,我們之前創建的readStdin應用程序,通過使用flow模式,將表現為如下所示:
process.stdin
????.on('data', function(chunk) {
????????console.log('New data available');
????????console.log(
????????????'Chunk read: (' + chunk.length + ')" ' +
????????????chunk.toString() + '"'
????????);
????}) .on('end', function() {
????????process.stdout.write('End of stream');
????});
? ??flowing模式是一種針對舊版流接口(也被成為Streams1),并且提供更少的靈活性來控制數據的流。伴隨著Streams2接口的引入,以下模式將不再是磨人的工作模式。為了實現這種模式,需要添加一個監聽器來針對data事件或者明確針對tresume()方法進行調用。為了臨時性的終止流去發射data事件,我們需要在之后調用pause()方法沒來造成很多輸入數據被緩存在內部緩沖區。
? ? 調用pause()函數,并不能造成流轉回non-flowing模式。
Implementing Readable streams
? ? 現在我們知道如何從一個流中讀取數據,下一步是學習如何實現一個新的可讀流。為了達到這個目的,需要通過繼承stream.Readable原型來創建一個新的類。具體的流需要提供一個關于_read()方法的實現,它形如以下寫法:
readable._read(size)
? ??Readable類的內部將會調用_read()方法,這個方法將會反過來通過使用push()方法來裝填內部緩沖區buffer。
readable.push(chunk)
? ? 請注意read()是一個被流消費者調用的方法,而_read()是一個由stream子類實現的方法,并且不能直接調用。下劃線一般表示,方法是非公開的(不是public的),并且不能直接調用。
? ? 為了實現如何使用新形式的Readable流,我們可以嘗試實現一個流來產生隨機字符串。我們來產生一個新的名為randomStream.js的模塊,這個模塊將包含我們之后編寫的字符串生成器代碼。在文件的最上部,我們將下載我們的依賴:
var stream = require('stream');
var util = require('util');
var chance = require('chance').Chance();
? ? 這里的處理沒有什么特別的,除了我們加載一個npm模塊。
????這個模塊被稱為chance(https://npmjs.org/package/chance),這個模塊是一個庫來產生各種類型的隨機值,從數字numbers到字符串strings到整個語句。
? ? 下一個步驟是去創建一個新的被稱為RandomStream的類,并且指定stream.Readable作為它的父類:
function RandomStream(options) {
????stream.Readable.call(this, options);
}
util.inherits(RandomStream, stream.Readable);
? ? 在上述代碼中,我們將調用父類的構造器constructor,來初始化它的內部聲明,然后將options參數作為一個輸入來接收,可能通過options對象來傳遞的參數包括:
? 編碼參數用來將Buffers 轉換為 Strings(默認類型轉到null)。
? 一個標志位來授權對象模式(objectMode defaults to false)。
? 在內部緩沖區中的數據存儲上限如果超過,將不能再從資源中讀取更多數據((highWaterMark defaults to 16 KB)。
? ? 好的現在我們具備了RandomStream構造器constructor,我們可以實現_read()方法如下:
RandomStream.prototype._read = function(size) {
????var chunk = chance.string(); //[1]
????console.log('Pushing chunk of size:' + chunk.length);
????this.push(chunk, 'utf8'); //[2]
????if(chance.bool({likelihood: 5})) {
????????//[3] this.push(null);
????}
}
module.exports = RandomStream;
????上述代碼將被解釋如下:
? ??1. 這個方法通過chance.函數來產生一個隨機字符串。
????2. 它把字符串推進內部讀取緩沖區。注意到,既然我們推入一個String類型數據,我們也要指定編碼格式為utf8(這個是非必需的,如果這個數據塊時一個二進制的緩沖區Buffer)。
????3. 它能夠隨機終止流,以概率likelihood為5%的指標,通過將null推入內部緩沖區buffer來標識一個EOF情況,或者換句話,流的終結。
????我們可以看到針對于_read()函數輸入值中的size參數被忽略,因為它被作為一種建議參數。我們可以簡單的推入push所有可用的數據,但是在同一個調用中有很多推送pushes的過程,之后我們需要檢查push()是否能夠返回false,因為這個將會意味著內部的緩沖區已經達到了highWaterMark限制并且我們需要停止加入更多的數據到它里面。
? ? 這是RandomStream的方式,我們并不準備用他。我們來創建一個新的模塊命名為:generateRandom.js,在其中我們實例化一個RandomStream對象,并且從中拉取一些數據。
var RandomStream = require('./randomStream');
var randomStream = new RandomStream();
randomStream.on('readable', function() {
????var chunk;
????while((chunk = randomStream.read()) !== null) {
????????console.log("Chunk received: " + chunk.toString());
????}
});
? ? 現在所有的準備工作已經具備了,我們可以嘗試定義新模式的流。只需要如同平時一樣執行一下generateRandom模塊,并且觀察到一組隨機序列的字符串流在屏幕上出現。
Writable streams
? ? 一個可寫入writable的流代表了一個數據目的地。在Node中,它通過可寫Writable的抽象類(由stream模塊提供),來實現其構建。
Writing to a stream
? ? 將一些數據從可寫writable流中推下,是一件很簡單的業務。我們需要去使用的是write()方法,可以如下寫法編寫:
writable.write(chunk, [encoding], [callback])
? ? 編碼參數是可選的并且可以被指定,如果數據塊時String類型的(默認為utf8編碼格式,忽略數據塊是緩沖Buffer類型的情況),當數據塊被沖入底層資源時,回調函數callback將被調用,并且也同樣是可選的。
? ? 為了表明沒有更多數據被寫入流,我們不得不使用end()方法:
writable.end([chunk], [encoding], [callback])
? ? 我們可以通過end()方法來提供一個最后的數據塊。在這個實例中,callback回調函數等價于:注冊一個針對于最終事件finish event的監聽器,這個最終事件將會在所有被寫入流中的數據沖入底層資源的時候得到觸發。
? ? 我們現在來看看這個機制是如何工作的,通過建立一個實例:一個小的HTTP服務器會輸出一個隨機序列的字符串:
var chance = require('chance').Chance();
require('http').createServer(function (req, res) {
????res.writeHead(200, {'Content-Type': 'text/plain'}); //[1]
????while(chance.bool({likelihood: 95})) {//[2]
????????res.write(chance.string() + '\n'); //[3]
????}
????res.end('\nThe end...\n'); //[4]
????res.on('finish', function() { //[5]
????????console.log('All data was sent');
????});
}).listen(8080, function () {
????console.log('Listening');
});
? ? 我們所創建的HTTP服務器注入了res對象,它是一個http.ServerResponse的實例并且同時是一個可寫Writable的流。所要發生的被解釋如下所示:
1.我們首先編寫HTTP響應(response)的頭部。注意到writeHead()并不是可寫Writable接口的一部分。事實上,它是由http.ServerResponse類暴露的輔助方法。
2..我們開始一個循環,并將其終止于似然率為5%(我們引入chance.bool()函數賴在95%的時間內返回true)。
3.在循環內部,我們編寫了隨機字符串來注入流。
4.一旦我們來到了循環外部,我們調用stream流中的end()函數,標志這里已經沒有更多的數據被寫入。與此同時,我們提供一個最終final字符串來在終止流之前,將其寫入流中。
5.最終,我們針對完成事件finish event注冊一個監聽器,這個監聽器將被觸發,當所有的數據被沖入底層socket套接字中時。
? ? 我們稱這個小模塊為entropyServer.js,并且在之后運行它。為了測試服務器,我們可以打開一個瀏覽器鍵入地址:http://localhost:8080,或者使用終端的cur,如下:l
curl localhost:8080
? ? 此時,服務器應該會開始發送隨機字符串到你所選擇的HTTP客戶端(請記住,一些瀏覽器可能會緩存數據,并且流的表現可能不會是明顯的)。
? ? 一個有趣的事實是:http.ServerResponse實際上是一個老的Stream類(http://nodejs.
org/docs/v0.8.0/api/stream.html)的實例,它的聲明很重要,盡管,這個機制并不影響我們的例子,因為可寫端的接口和表現,將在新建立的流可寫類stream.Writable class中保持幾乎一致。
Back-pressure
? ? 類似于在一個實際管道系統中的液體流動。Node的流也會遭遇一個瓶頸問題,在這種情況下數據寫入會加快,超過流消耗(消費:consumer)所寫入數據的速度。處理這個問題的機制,包括緩沖區緩存所到來的數據。然而,如果這條流并不急于寫入者任何的回饋feedback,我們將遭受一種情境,在其中:越來越多的數據被注入內部緩沖區,導致不能預期等級的內存占用率。
? ? 為了阻止這種情況的發生,writable.write()函數將會當內部緩沖區超過highWaterMark限制時,返回false??蓪慦ritable流擁有一個highWaterMark屬性,將會限制內部緩沖區的大小,當超過這個標準時,write()方法將會開始返回false,表明應用程序現在需要停止寫入。當緩沖區已經耗盡,耗盡事件(drain event)將被發射,來溝通說明它很安全去再次開啟寫入操作。
????這個機制被稱為:背壓(back-pressure)。
? ? 本節所描述的機制也被相似的應用于Readable流。事實上,背壓back-pressure問題同樣也在可讀Readable流中存在。當push()方法(在_read()方法內部)被返回false的時候,它將被觸發。然而,這個問題具體到流的實現者,所以我們將以較低頻率來處理它。
? ? 我們將很快展示如何將可寫Writable流中的背壓back-pressure問題考慮進來,來通過將我們之前所創造的entropyServer模塊進行改進。
var chance = require('chance').Chance();
require('http').createServer(function (req, res) {
????res.writeHead(200, {'Content-Type': 'text/plain'});
????function generateMore() { //[1]
????????while(chance.bool({likelihood: 95})) {
????????????var shouldContinue = res.write(
????????????????chance.string({length: (16 * 1024) – 1}) //[2]
????????????);
????????????if(!shouldContinue) { //[3]
????????????????console.log('Backpressure');
????????????????return res.once('drain', generateMore);
????????????} }
????????????res.end('\nThe end...\n', function() {
????????????????console.log('All data was sent');
????????????});
????}
????generateMore();
}).listen(8080, function () {
????console.log('Listening');
});
? ? 上述代碼中最關鍵的步驟,可以總結如下:
1.我們將主要邏輯包裝在一個名為generateMore()的函數中。
2.為了提高接收到背壓back-pressure的機會,我們將數據塊的大小size提高到16KB-1Byte,這個數值和默認的highWaterMark門限值非常接近。
3.當寫入一個數據塊時,我們檢查res.write()函數的返回值,如果我們接收到了false,意味著內部緩沖區是滿的,而我們需要停止發送更多的數據。在這種情況下,我們從這個函數中退出,并且注冊下一個循環的寫入操作,當耗盡事件(drain event)被發射。
? ? 我們現在嘗試在此運行服務器,并且在之后以curl產生一個客戶端請求,這里將會有很大可能將會出現一些背壓back-pressure的情況,因為服務器以一個非常高的速率產生的數據,這個產生數據的速率要快于底層socket所能處理的限度。