流的英文stream
,流(Stream)是一個抽象的數據接口,Node.js
中不少對象都實現了流,流是EventEmitter
對象的一個實例,總之它是會冒數據(以 Buffer
爲單位),或者可以吸取數據的東西,它的本質就是讓數據流動起來。 可能看一張圖會更直觀:javascript
注意:stream
不是node.js獨有的概念,而是一個操做系統最基本的操做方式,只不過node.js有API支持這種操做方式。linux命令的|就是stream
。html
做者簡介:koala,專一完整的 Node.js 技術棧分享,從 JavaScript 到 Node.js,再到後端數據庫,祝您成爲優秀的高級 Node.js 工程師。【程序員成長指北】做者,Github 博客開源項目 github.com/koala-codin…前端
小夥伴們確定都在線看過電影,對比定義中的圖-水桶管道流轉圖
,source
就是服務器端的視頻,dest
就是你本身的播放器(或者瀏覽器中的flash和h5 video)。你們想一下,看電影的方式就如同上面的圖管道換水同樣,一點點從服務端將視頻流動到本地播放器,一邊流動一邊播放,最後流動完了也就播放完了。java
說明:視頻播放的這個例子,若是咱們不使用管道和流動的方式,直接先從服務端加載完視頻文件,而後再播放。會形成不少問題node
有一個這樣的需求,想要讀取大文件data的例子linux
使用文件讀取git
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer(function (req, res) {
const fileName = path.resolve(__dirname, 'data.txt');
fs.readFile(fileName, function (err, data) {
res.end(data);
});
});
server.listen(8000);
複製代碼
使用文件讀取這段代碼語法上並無什麼問題,可是若是data.txt文件很是大的話,到了幾百M,在響應大量用戶併發請求的時候,程序可能會消耗大量的內存,這樣可能形成用戶鏈接緩慢的問題。並且併發請求過大的話,服務器內存開銷也會很大。這時候咱們來看一下用stream
實現。程序員
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer(function (req, res) {
const fileName = path.resolve(__dirname, 'data.txt');
let stream = fs.createReadStream(fileName); // 這一行有改動
stream.pipe(res); // 這一行有改動
});
server.listen(8000);
複製代碼
使用stream就能夠不須要把文件所有讀取了再返回,而是一邊讀取一邊返回,數據經過管道流動給客戶端,真的減輕了服務器的壓力。github
看了兩個例子我想小夥伴們應該知道爲什麼要使用stream
了吧!由於一次性讀取,操做大文件,內存和網絡是吃不消的,所以要讓數據流動起來,一點點的進行操做。數據庫
再次看這張水桶管道流轉圖
stream
整個流轉過程包括source,dest,還有鏈接兩者的管道pipe(stream的核心),分別介紹三者來帶領你們搞懂stream流轉過程。
stream
的常見來源方式有三種:
http
請求中的request
這裏先說一下從控制檯輸入
這種方式,2和3兩種方式stream應用場景
章節會有詳細的講解。
看一段process.stdin
的代碼
process.stdin.on('data', function (chunk) {
console.log('stream by stdin', chunk)
console.log('stream by stdin', chunk.toString())
})
//控制檯輸入koalakoala後輸出結果
stream by stdin <Buffer 6b 6f 61 6c 61 6b 6f 61 6c 61 0a>
stream by stdin koalakoala
複製代碼
運行上面代碼:而後從控制檯輸入任何內容都會被data
事件監聽到,process.stdin
就是一個stream
對象,data 是stream
對象用來監聽數據傳入的一個自定義函數,經過輸出結果可看出process.stdin
是一個stream對象。
說明: stream
對象能夠監聽"data"
,"end"
,"opne"
,"close"
,"error"
等事件。node.js
中監聽自定義事件使用.on
方法,例如process.stdin.on(‘data’,…)
, req.on(‘data’,…)
,經過這種方式,能很直觀的監聽到stream
數據的傳入和結束
從水桶管道流轉圖中能夠看到,在source
和dest
之間有一個鏈接的管道pipe
,它的基本語法是source.pipe(dest)
,source
和dest
就是經過pipe鏈接,讓數據從source
流向了dest
。
stream的常見輸出方式有三種:
http
請求中的response
stream
的應用場景主要就是處理IO
操做,而http請求
和文件操做
都屬於IO
操做。這裏再提一下stream
的本質——因爲一次性IO
操做過大,硬件開銷太多,影響軟件運行效率,所以將IO
分批分段進行操做,讓數據像水管同樣流動起來,直到流動完成,也就是操做完成。下面對幾個經常使用的應用場景分別進行介紹
一個對網絡請求作壓力測試的工具ab
,ab
全稱 Apache bench
,是 Apache
自帶的一個工具,所以使用 ab
必需要安裝 Apache
。mac os 系統自帶 Apache
,windows
用戶視本身的狀況進行安裝。運行ab
以前先啓動 Apache
,mac os
啓動方式是 sudo apachectl start
。
Apache bench對應參數的詳細學習地址,有興趣的能夠看一下 Apache bench對應參數的詳細學習地址
介紹這個小工具的目的是對下面幾個場景能夠進行直觀的測試,看出使用stream帶來了哪些性能的提高。
這樣一個需求:
使用node.js實現一個http請求,讀取data.txt文件,建立一個服務,監聽8000端口,讀取文件後返回給客戶端,講get請求的時候用一個常規文件讀取與其作對比,請看下面的例子。
getTest1.js
// getTest.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer(function (req, res) {
const method = req.method; // 獲取請求方法
if (method === 'GET') { // get 請求方法判斷
const fileName = path.resolve(__dirname, 'data.txt');
fs.readFile(fileName, function (err, data) {
res.end(data);
});
}
});
server.listen(8000);
複製代碼
getTest2.js
// getTest2.js
// 主要展現改動的部分
const server = http.createServer(function (req, res) {
const method = req.method; // 獲取請求方法
if (method === 'GET') { // get 請求
const fileName = path.resolve(__dirname, 'data.txt');
let stream = fs.createReadStream(fileName);
stream.pipe(res); // 將 res 做爲 stream 的 dest
}
});
server.listen(8000);
複製代碼
對於下面get請求中使用stream的例子,會不會有些小夥伴提出質疑,難道response也是一個stream對象,是的沒錯,對於那張水桶管道流轉圖
,response就是一個dest。
雖然get請求中可使用stream,可是相比直接file文件讀取·res.end(data)
有什麼好處呢?這時候咱們剛纔推薦的壓力測試小工具就用到了。getTest1
和getTest2
兩段代碼,將data.txt
內容增長大一些,使用ab
工具進行測試,運行命令ab -n 100 -c 100 http://localhost:8000/
,其中-n 100
表示前後發送100次請求,-c 100
表示一次性發送的請求數目爲100個。對比結果分析使用stream後,有很是大的性能提高,小夥伴們能夠本身實際操做看一下。
一個經過post請求微信小程序的地址生成二維碼的需求。
/* * 微信生成二維碼接口 * params src 微信url / 其餘圖片請求連接 * params localFilePath: 本地路徑 * params data: 微信請求參數 * */
const downloadFile=async (src, localFilePath, data)=> {
try{
const ws = fs.createWriteStream(localFilePath);
return new Promise((resolve, reject) => {
ws.on('finish', () => {
resolve(localFilePath);
});
if (data) {
request({
method: 'POST',
uri: src,
json: true,
body: data
}).pipe(ws);
} else {
request(src).pipe(ws);
}
});
}catch (e){
logger.error('wxdownloadFile error: ',e);
throw e;
}
}
複製代碼
看這段使用了stream的代碼,爲本地文件對應的路徑建立一個stream對象,而後直接.pipe(ws)
,將post請求的數據流轉到這個本地文件中,這種stream的應用在node後端開發過程當中仍是比較經常使用的。
request和reponse同樣,都是stream對象,可使用stream的特性,兩者的區別在於,咱們再看一下水桶管道流轉圖
,
一個文件拷貝的例子
const fs = require('fs')
const path = require('path')
// 兩個文件名
const fileName1 = path.resolve(__dirname, 'data.txt')
const fileName2 = path.resolve(__dirname, 'data-bak.txt')
// 讀取文件的 stream 對象
const readStream = fs.createReadStream(fileName1)
// 寫入文件的 stream 對象
const writeStream = fs.createWriteStream(fileName2)
// 經過 pipe執行拷貝,數據流轉
readStream.pipe(writeStream)
// 數據讀取完成監聽,即拷貝完成
readStream.on('end', function () {
console.log('拷貝完成')
})
複製代碼
看了這段代碼,發現是否是拷貝好像很簡單,建立一個可讀數據流readStream
,一個可寫數據流writeStream
,而後直接經過pipe
管道把數據流轉過去。這種使用stream的拷貝相比存文件的讀寫實現拷貝,性能要增長不少,因此小夥伴們在遇到文件操做的需求的時候,儘可能先評估一下是否須要使用stream
實現。
目前一些比較火的前端打包構建工具
,都是經過node.js
編寫的,打包和構建的過程確定是文件頻繁操做的過程,離不來stream
,例如如今比較火的gulp
,有興趣的小夥伴能夠去看一下源碼。
Readable Stream
可讀數據流Writeable Stream
可寫數據流Duplex Stream
雙向數據流,能夠同時讀和寫Transform Stream
轉換數據流,可讀可寫,同時能夠轉換(處理)數據(不經常使用)以前的文章都是圍繞前兩種可讀數據流和可寫數據流,第四種流不太經常使用,須要的小夥伴網上搜索一下,接下來對第三種數據流Duplex Stream 說明一下。
Duplex Stream
雙向的,既可讀,又可寫。 Duplex streams
同時實現了 Readable
和Writable
接口。 Duplex streams
的例子包括
tcp sockets
zlib streams
crypto streams
我在項目中還未使用過雙工流,一些Duplex Stream的內容能夠參考這篇文章NodeJS Stream 雙工流rs.pipe(ws)
的方式來寫文件並非把 rs 的內容 append
到 ws 後面,而是直接用 rs 的內容覆蓋 ws 原有的內容pipe
方法返回的是目標數據流,如 a.pipe(b)
返回的是 b,所以監聽事件的時候請注意你監聽的對象是否正確pipe
方法來串聯數據流的話,你就要寫成: 代碼實例:data
.on('end', function() {
console.log('data end');
})
.pipe(a)
.on('end', function() {
console.log('a end');
})
.pipe(b)
.on('end', function() {
console.log('b end');
});
複製代碼
event-stream 用起來有函數式編程的感受
awesome-nodejs#streams也是一個不錯的第三方stream庫,有興趣的小夥伴能夠github看一下
看完了這篇文章是否是對stream有了必定的瞭解,而且知道了node對於文件處理仍是有完美的解決方案的。本文中三次展現了水桶管道流轉圖
,總要的事情說三遍但願小夥伴們記住它,除了以上內容小夥伴們會不會有一些思考,好比
string
類型仍是其餘類型,該類型爲stream帶來了什麼好處?pipe
函數何時觸發的呢?在什麼狀況下觸流轉發?底層機制是什麼? 上面的疑問(因爲篇幅過長拆分爲兩篇)會在我stream
的第二篇文章爲你們詳細講解。今天就分享這麼多,若是對分享的內容感興趣,能夠關注公衆號「程序員成長指北」,或者加入技術交流羣,你們一塊兒討論。
加入咱們一塊兒學習吧!
node學習交流羣
交流羣滿100人不能自動進羣, 請添加羣助手微信號:【coder_qi】備註node,自動拉你入羣。