Node.js是單線程、異步非阻塞IO,但凡對Node.js有點了解的人都會說出這是Node的最大特點之一。但是怎么理解這個特點,或者說怎么能搞說服大家拋棄傳統的Web應用架構而使用Node的架構呢?我想理解這所謂的單線程、異步非阻塞IO就顯得異常重要。
今天我們就看一個Node.js中一個非常重要的概念Stream來簡單的理解一下上述的幾個特點。Stream,翻譯成中文只有簡單的一個“流”字,如果沒有信息系統或者計算機技術背景的人根本不會有什么概念,但是你只要你對任何一種現代編程語言有所了解,都會對輸入輸出的“流”這個概念會有自己的理解,比如Java中的java.io.InputStream和java.io.OutputStream。我個人理解這個Node.js中的Stream與這些IO流沒有什么本質的不同,只不過在Node.js這個架構中更加體現出流的特點,那么流的特點是什么呢?
流最大的特點就是我可以在它沒有完全出現的時候能夠使用它已經出現的部分。比如想我這個年紀,都有在中小學時代值日為班級打白開水的經歷,我們要為全班打一大桶水,開水桶很大本來就需要等待很長時間,而你又很苦逼,打水的水龍頭不是很靈水“流”很小,而更郁悶的是你剛剛下了體育課本來想假公濟私一把自己可以馬上喝到水。這個時候有兩個選擇,第一個是等把整個開水壺打滿后和全班同學同樂,另外一個選擇就是先用自己的茶缸不等開會桶滿了的情況下自己先樂。我想百分之八十的像我這樣非二三道杠的人都會選擇第二個方案。這種情況與我們現在信息系統中的數據IO的情況很像,大部分情況或者說大部分系統架構只能采取第一種方式等待所有的數據(水)都准備好了之后才能處理這些數據,而Node.js就不同可以在數據沒有完全准備好或者是說沒有完全出現的情況下就開始異步處理數據。而這種需要異步處理數據的情況在大數據、實時響應和大量網絡IO情況下尤為顯的重要。
下面就讓我們看一個具體點的例子:
var http = require('http'), fs = require('fs'); var server = http.createServer(function(req, res) { fs.readFile(__dirname + '/data.txt', function(err, data) { res.end(data); }); }); server.listen(8000);
這個Node.js應用很簡單,估計所有學習過Node的人都做過這樣的練習,可以說是Node的Hello World了。這段代碼沒有任何問題,你使用node可以正常的運行起來,使用瀏覽器或者其他的http客戶端都可以正常的訪問運行程序主機的8000端口讀取主機上的data.txt文件。但是這種方式隱含了一個潛在的問題,node會把整個data.txt文件都緩存到內存中以便響應客戶端的請求(request),隨着客戶端請求的增加內存的消耗將是非常驚人的,而且客戶端需要等待很長傳輸時間才能得到結果。讓我們再看一看另外一種方式:
var http = require('http'), fs = require('fs'); var server = http.createServer(function(req, res) { var stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(res); }); server.listen(8000);
這里面有一個非常大的變化就是使用createReadStream這個fs的方法創建了stream這個變量,並由這個變量的pip方法來響應客戶端的請求。使用stream這個變量就可以讓node讀取data.txt一定量的時候就開始向客戶端發送響應的內容,而無需服務緩存以及客戶端的等待。
這樣就使我們對Node.js的stream有了點感覺了。流(Stream)可以定義為一個連續的數據流,並且可以在數據流入(出)的時候對這些數據進行異步操作。在Node.js中流既可以是可讀的,也可以是可寫的。一個可讀的stream還是一個EventEmitter對象,當每一次接收到一定量的數據都會觸發data事件。我們上邊的例子中使用pipe方法將一個文件的內容發送給一個HTTP客戶端。當stream到達文件的末尾時會觸發一個end的事件,表明不會再有任何的data事件發生了。同時一個可讀stream可以暫停並且繼續。可寫stream是接收數據流,這種類型的stream也繼承了EventEmitter對象,並且實現了兩個方法:write()和end()。第一個方法在寫入緩存數據的時候觸發,而且如果數據寫入准確則返回值true,如果緩存滿了(這種情況下數據會再次發送)。方法end()就會在stream結束的時候觸發。
根據Stream的特點我們來做一個Steam類型的應用,這是一個簡單的上傳文件的應用。首先我們將創建一個客戶端使用可讀的stream讀取一個文件並且將其pipe到一個特定的目標,另外在pipe結束的時候使用一個可寫的stream實現一個服務將上傳的數據進行保存。
首先我們先看一下客戶端的代碼,注釋中有比較詳細的解釋。
// 引入http和文件系統模塊 var http = require('http'), fs = require('fs'); // 定義http請求 var options = { host : 'localhost', port : 8000, path : '/', method : 'POST' }; var req = http.request(options, function(res) { console.log(res.statusCode); }); // 創建一個可讀的stream讀取文件並把讀取的內容通過pipe發送給請求 var readStream = fs.ReadStream(__dirname + "/in.txt"); readStream.pipe(req); // 當stream停止讀取的時候通過對request調用end()方法斷開鏈接 readStream.on('close', function() { req.end(); console.log("I finished."); });
我們再看一下服務端的代碼。
// 與客戶端一樣引入http和文件系統兩個模塊,並且使用一個可寫的stream var http = require('http'), fs = require('fs'); var writeStream = fs.createWriteStream(__dirname + "/out.txt"); // 為了響應客戶端上傳文件的請求,我們創建了一個server,數據請求通過request對象進入 // 服務將使用stream並將緩存寫入輸出文件 var server = http.createServer(function(req, res) { req.on('data', function(data) { writeStream.write(data); }); req.on('end', function() { writeStream.end(); res.statusCode = 200; res.end("OK"); }); }); server.listen(8000);
通過上面的列子,大家應該對Node.js的流(stream)會更有感覺了。后面的時間里如果有機會我將繼續深入的探討一下stream的使用,特別是在請求的數據量巨大時,對數據分頁或者分批處理的方法。
本文參考了以下內容: