Node.js 中的 stream 模塊詳解

echa攻城獅 發佈 2020-05-17T20:47:32+00:00

流的英文stream,流是一個抽象的數據接口,Node.js中很多對象都實現了流,流是EventEmitter對象的一個實例,總之它是會冒數據,或者能夠吸收數據的東西,它的本質就是讓數據流動起來。


作者: koala

轉發連結:https://mp.weixin.qq.com/s/HQmualyyEV4t7lG0PZf8zQ

什麼是stream

定義

流的英文stream,流(Stream)是一個抽象的數據接口,Node.js中很多對象都實現了流,流是EventEmitter對象的一個實例,總之它是會冒數據(以 Buffer 為單位),或者能夠吸收數據的東西,它的本質就是讓數據流動起來。可能看一張圖會更直觀:

水桶管道流轉圖

注意:stream不是node.js獨有的概念,而是一個作業系統最基本的操作方式,只不過node.js有API支持這種操作方式。linux命令的|就是stream。

為什麼要學習stream

視頻播放例子

小夥伴們肯定都在線看過電影,對比定義中的圖-水桶管道流轉圖,source就是伺服器端的視頻,dest就是你自己的播放器(或者瀏覽器中的flash和h5 video)。大家想一下,看電影的方式就如同上面的圖管道換水一樣,一點點從服務端將視頻流動到本地播放器,一邊流動一邊播放,最後流動完了也就播放完了。

說明:視頻播放的這個例子,如果我們不使用管道和流動的方式,直接先從服務端加載完視頻文件,然後再播放。會造成很多問題

  1. 因內存占有太多而導致系統卡頓或者崩潰
  2. 因為我們的網速 內存 cpu運算速度都是有限的,而且還要有多個程序共享使用,一個視頻文件加載完可能有幾個g那麼大。

讀取大文件data的例子

有一個這樣的需求,想要讀取大文件data的例子

使用文件讀取

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就可以不需要把文件全部讀取了再返回,而是一邊讀取一邊返回,數據通過管道流動給客戶端,真的減輕了伺服器的壓力。

看了兩個例子我想小夥伴們應該知道為什麼要使用stream了吧!因為一次性讀取,操作大文件,內存和網絡是吃不消的,因此要讓數據流動起來,一點點的進行操作。

stream流轉過程

再次看這張水桶管道流轉圖

圖中可以看出,stream整個流轉過程包括source,dest,還有連接二者的管道pipe(stream的核心),分別介紹三者來帶領大家搞懂stream流轉過程。

stream從哪裡來-soucre

stream的常見來源方式有三種:

  1. 從控制台輸入
  2. http請求中的request
  3. 讀取文件

這裡先說一下從控制台輸入這種方式,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數據的傳入和結束

連接水桶的管道-pipe

從水桶管道流轉圖中可以看到,在source和dest之間有一個連接的管道pipe,它的基本語法是source.pipe(dest),source和dest就是通過pipe連接,讓數據從source流向了dest。

stream到哪裡去-dest

stream的常見輸出方式有三種:

  1. 輸出控制台
  2. http請求中的response
  3. 寫入文件

stream應用場景

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帶來了哪些性能的提升。

get請求中應用stream

這樣一個需求:

使用node.js實現一個http請求,讀取data.txt文件,創建一個服務,監聽8000埠,讀取文件後返回給客戶端,講get請求的時候用一個常規文件讀取與其做對比,請看下面的例子。

  • 常規使用文件讀取返回給客戶端response例子 ,文件命名為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);
  • 使用stream返回給客戶端response 將上面代碼做部分修改,文件命名為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中使用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後端開發過程中還是比較常用的。

post與get使用stream總結

request和reponse一樣,都是stream對象,可以使用stream的特性,二者的區別在於,我們再看一下水桶管道流轉圖,

request是source類型,是圖中的源頭,而response是dest類型,是圖中的目的地。

在文件操作中使用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,有興趣的小夥伴可以去看一下源碼。

stream的種類

  • 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 雙工流

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');
        });

stream的常見類庫

  • event-stream 用起來有函數式編程的感覺
  • awesome-nodejs#streams也是一個不錯的第三方stream庫,有興趣的小夥伴可以github看一下

總結

本篇文章屬於進階路線【Node必知必會系列】,看完了這篇文章是不是對stream有了一定的了解,並且知道了node對於文件處理還是有完美的解決方案的。本文中三次展示了水桶管道流轉圖,重要的事情說三遍希望小夥伴們記住它,除了以上內容小夥伴們會不會有一些思考,比如

  1. stream數據流轉具體內容是什麼呢?二進位還是string類型還是其他類型,該類型為stream帶來了什麼好處?
  2. 水桶管道流轉圖中的水管,也就是pipe函數什麼時候觸發的呢?在什麼情況下觸流轉發?底層機制是什麼?上面的疑問(由於篇幅過長拆分為兩篇)會在我stream的第二篇文章為大家詳細講解

推薦Vue學習資料文章:

《「乾貨」了不起的 Deno 實戰教程》

《「乾貨」通俗易懂的Deno 入門教程》

《Deno 正式發布,徹底弄明白和 node 的區別》

《「實踐」基於Apify+node+react/vue搭建一個有點意思的爬蟲平台》

《「實踐」深入對比 Vue 3.0 Composition API 和 React Hooks》

《前端網紅框架的插件機制全梳理(axios、koa、redux、vuex)》

《深入Vue 必學高階組件 HOC「進階篇」》

《深入學習Vue的data、computed、watch來實現最精簡響應式系統》

《10個實例小練習,快速入門熟練 Vue3 核心新特性(一)》

《10個實例小練習,快速入門熟練 Vue3 核心新特性(二)》

《教你部署搭建一個Vue-cli4+Webpack移動端框架「實踐」》

《2020前端就業Vue框架篇「實踐」》

《詳解Vue3中 router 帶來了哪些變化?》

《Vue項目部署及性能優化指導篇「實踐」》

《Vue高性能渲染大數據Tree組件「實踐」》

《尤大大細品VuePress搭建技術網站與個人博客「實踐」》

《10個Vue開發技巧「實踐」》

《是什麼導致尤大大選擇放棄Webpack?【vite 原理解析】》

《帶你了解 vue-next(Vue 3.0)之 小試牛刀【實踐】》

《帶你了解 vue-next(Vue 3.0)之 初入茅廬【實踐】》

《實踐Vue 3.0做JSX(TSX)風格的組件開發》

《一篇文章教你並列比較React.js和Vue.js的語法【實踐】》

《手拉手帶你開啟Vue3世界的鬼斧神工【實踐】》

《深入淺出通過vue-cli3構建一個SSR應用程式【實踐】》

《怎樣為你的 Vue.js 單頁應用提速》

《聊聊昨晚尤雨溪現場針對Vue3.0 Beta版本新特性知識點匯總》

《【新消息】Vue 3.0 Beta 版本發布,你還學的動麼?》

《Vue真是太好了 壹萬多字的Vue知識點 超詳細!》

《Vue + Koa從零打造一個H5頁面可視化編輯器——Quark-h5》

《深入淺出Vue3 跟著尤雨溪學 TypeScript 之 Ref 【實踐】》

《手把手教你深入淺出vue-cli3升級vue-cli4的方法》

《Vue 3.0 Beta 和React 開發者分別槓上了》

《手把手教你用vue drag chart 實現一個可以拖動 / 縮放的圖表組件》

《Vue3 嘗鮮》

《總結Vue組件的通信》

《手把手讓你成為更好的Vue.js開發人員的12個技巧和竅門【實踐】》

《Vue 開源項目 TOP45》

《2020 年,Vue 受歡迎程度是否會超過 React?》

《尤雨溪:Vue 3.0的設計原則》

《使用vue實現HTML頁面生成圖片》

《實現全棧收銀系統(Node+Vue)(上)》

《實現全棧收銀系統(Node+Vue)(下)》

《vue引入原生高德地圖》

《Vue合理配置WebSocket並實現群聊》

《多年vue項目實戰經驗匯總》

《vue之將echart封裝為組件》

《基於 Vue 的兩層吸頂踩坑總結》

《Vue插件總結【前端開發必備】》

《Vue 開發必須知道的 36 個技巧【近1W字】》

《構建大型 Vue.js 項目的10條建議》

《深入理解vue中的slot與slot-scope》

《手把手教你Vue解析pdf(base64)轉圖片【實踐】》

《使用vue+node搭建前端異常監控系統》

《推薦 8 個漂亮的 vue.js 進度條組件》

《基於Vue實現拖拽升級(九宮格拖拽)》

《手摸手,帶你用vue擼後台 系列二(登錄權限篇)》

《手摸手,帶你用vue擼後台 系列三(實戰篇)》

《前端框架用vue還是react?清晰對比兩者差異》

《Vue組件間通信幾種方式,你用哪種?【實踐】》

《淺析 React / Vue 跨端渲染原理與實現》

《10個Vue開發技巧助力成為更好的工程師》

《手把手教你Vue之父子組件間通信實踐講解【props、$ref 、$emit】》

《1W字長文+多圖,帶你了解vue的雙向數據綁定源碼實現》

《深入淺出Vue3 的響應式和以前的區別到底在哪裡?【實踐】》

《乾貨滿滿!如何優雅簡潔地實現時鐘翻牌器(支持JS/Vue/React)》

《基於Vue/VueRouter/Vuex/Axios登錄路由和接口級攔截原理與實現》

《手把手教你D3.js 實現數據可視化極速上手到Vue應用》

《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【上】》

《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【中】》

《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【下】》

《Vue3.0權限管理實現流程【實踐】》

《後台管理系統,前端Vue根據角色動態設置菜單欄和路由》

作者: koala

轉發連結:https://mp.weixin.qq.com/s/HQmualyyEV4t7lG0PZf8zQ

關鍵字: