深刻學習 Node.js stream 系列(一)

前言

原本想寫一篇 Node.js stream 完整的深刻學習的文章。卻發現,一篇文章難以透徹講解完整個 stream,而後分享的概念太多,怕是一篇下去,可能長達十幾萬字,不只本身一兩個月都沒寫完博客,估計也鮮有讀者會願意仔細讀完。javascript

所以最好仍是寫成一個系列,不只能夠有點章法,並且還能夠慢慢地,細細地雕琢每個微小卻值得分享的點。於寫的人,於看的人,都是件好事。java

所以,系列的第一篇誕生了。node

世間的「流」

古人向來崇尚和喜好流水,在道家學說裏,也有「上善若水」的說法。水善利萬物而不爭,也是爲人處世的最高境界。npm

嗯,流水潺潺。編程

不管是古代的先賢哲人,抑或是近現代的創造者們,都善於從現實生活中學習,抽象,攫取創意或精煉原理。好比,從貓咪的伸懶腰姿式中,人們抽象出了拱橋、瑜伽姿式;從十個手指的計數,衍生出來十進制;以及模仿魚類的形體造船,以木槳仿鰭;研究鳥的內部構造和飛翔姿式,從而造出飛機等等。redux

天然界,孕育的不只僅是充滿智慧的人類,也給予了這羣聰明的猴子們 -- 人類,一大堆科學發明、工程技術實現原理的思想源泉。數組

固然說到這裏,咱們的主角「流」應該也不例外。一樣衍生出了許多概念。bash

RxJS 的異步事件流

好久之前。筆者聽過流的概念,那是來自 RxJS 社區裏的名言:「一切皆是流」。那時候想,個人天,還真 tm 酷,從某個角度理解,彷彿蘊含着哲學意味。服務器

在 RxJS 的世界裏,流是一個基本的概念,各類異步事件行成了一個又一個的流。經過操做符對這些流進行處理,組合、運算,以此知足應用程序的交互邏輯。這種編程方式至關抽象,也被稱爲響應式編程(或反應式編程)。網絡

好比連續的點擊事件 click 是一個流:

image.png

在表單輸入框裏敲字符「Hello!」也是一個流:

image.png

還有,一個 XHR 請求發送也是一個流:

image.png

諸如此類,不勝枚舉。

而後 map、filter、repeat、first、debounce、takeLast 等許多操做符,就能夠操做流。

好比 map 操做符來將 input 事件的數據流大寫:

image.png

是否是很像咱們代碼裏直接寫 Array.prototype.map()。可是咱們能夠理解爲「值」的投射,若是這些值一直在不斷生產,那麼就變成了流。概念很類似,可是 RxJS 在上面附加了推、拉模型等概念,使得處理異步事件的序列組合等邏輯更加友好。

Unix 系統中的流

後來瞭解到 bash,在 unix 系統中能夠用 | 符號來實現流,好比筆者想要計數本身的博客《淺談 TypeScript 下的 IoC 容器原理》裏出現了幾回的 IoC 這個縮寫。

$ cat 淺談TypeScript下的IoC容器原理.md | grep -o "IoC" | wc -w
15
複製代碼

用 cat 程序序列化讀取整個文件(cat -- concatenate and print files),而後以標準輸出流(standard output)發送到 grep 程序,grep 經過 | 接收標準輸入流(standard input),匹配過濾出 IoC,而後將標準輸出流發送給 wc 程序,wc 同理經過 | 接收標準輸入流,-w 參數計數單詞數量。

(基本上在 Unix 系統中,每一個程序若是運行成功,都會返回 0,若是錯誤通常會返回大於 0)

Unix 中的管道符,能夠將第一個進程的標準輸出文件描述符鏈接到第二個進程的標準輸入。什麼意思呢,請看示意圖以下:

image.png

經過管道符 「|」 組合了 cat、grep、wc 程序,unix 系統裏存在大量命令,每一個命令又有大量的參數,當使用流的概念組合使用這些命令時,不須要圖形化界面、軟件的協助,卻能夠完成不少事情。

若是換成 node.js 的 stream 方式來理解的話,有點像:

cat.pipe(grep).pipe(wc)
複製代碼

或者等效於:

cat.pipe(grep)
grep.pipe(wc)
複製代碼

函數組合的流

在函數式編程裏的 compose,pipe 來組合單一職責的函數,也隱隱約約像一個流。

如咱們組合 a b c 三個函數:

compose(a, b, c)
複製代碼

示意圖以下:

image.png

上面調用順序 a(b(c())),也便是 c -> b -> a,它像不像一個流?假如值的生產過程,是一個流,此時函數至關於在對流在不斷的修改、映射。

以及在 koa、redux 裏組合中間件,也是和流有殊途同歸之妙。關於中間件,這裏不詳細介紹,若有不瞭解的同窗,你們能夠看筆者以前寫的博客《深刻理解洋蔥模型中間件機制》瞭解學習。

因此大家看,這個世界處處都是流。

固然 Node.js 裏流也舉足輕重。上面都是筆者的遐想。想闡述的是,許多技術概念有時候是來源於生活的,將現實抽象後,功能分化後,才分叉產生了不一樣領域。咱們能夠尋找一個心智模型(mental model),進行學習這些或許晦澀難懂的概念,有時候說不定能達到觸類旁通,融會貫通的效果。

好了,正式介紹 Node.js 流!

淺談 Node.js 流

流(stream)是 Node.js 中處理流式數據的抽象接口。stream 模塊用於構建實現了流接口的對象。

流是可讀的,也是可寫的,或者可讀又可寫的。
或者可讀可寫的。 全部的流都是 EventEmitter 的實例。
在 Node.js 中有許多流,可讀流(Writable)、可寫流(Readable)、雙工流(Duplex),還有轉換流(Transform)。

雙工流是可讀又可寫的流,而轉換流是能夠在讀寫過程當中修改數據的雙工流。

秉承着飯一口一口吃,路一步一步走的精神,本系列一,咱們能夠先簡單瞭解一下可寫流和可讀流。

可寫流

可寫流是對數據要被寫入的目的地的一種抽象,好比可寫流,在 Node.js 中就有客戶端的 HTTP 請求、服務器的 HTTP 響應、fs 寫入流、process.stdout 等等。

fs.createWritableStream

咱們先來看 fs 寫入流,fs.createWritableStream 示例(fs.js):

const fs = require("fs");
const ws = fs.createWriteStream("./dest.txt");

"Hi!".split("").forEach(char => {
  console.log("write char", char);
  ws.write(`The char: ${char} char code is ${char.charCodeAt()}`);
  ws.write("\n");
});

ws.end(":)");
複製代碼

咱們將 「Hi!"的每一個字符的 charCode 打印在 dest.txt 文件中,文件內容以下:

The char: H char code is 72
The char: i char code is 105
The char: ! char code is 33
:)
複製代碼

咱們調用 fs.createWritableStream 傳入目標寫入路徑後,Node.js 給咱們返回了可寫流的實例,這個實例不只繼承可寫流,也繼承 EventEmitter。

不相信?咱們看:

const stream = require('stream');
const events = require('events');

console.log(ws instanceof stream.Writable); // true
console.log(ws instanceof events.EventEmitter); // true
複製代碼

所以,Writable 和 EventEmitter 擁有的方法,它也有,一個也很多。咱們調用 writable.write 寫入數據,調用 writable.end 通知流對象,咱們已經沒有任何其餘寫入數據。

process.stdout

進程 I/O 一樣也是 Writable 和 EventEmitter 的實例,耳聽爲虛眼見爲實,請同窗們能夠打印:

const stream = require("stream");
const events = require("events");

console.log(process.stdout instanceof stream.Writable); // true
console.log(process.stdout instanceof events.EventEmitter); // true
複製代碼

簡單使用,經過 write 方法寫入數據便可。代碼示例以下(process.js):

process.stdout.write('Hi!');
複製代碼

運行後,控制檯就會輸出友好的問候~

$ node process.js
Hi!
複製代碼

在 node.js 中 console.log 內部就是由 process.stdout 實現的。對應 console.error 內部就是由 process.stderr 實現的。(沒錯 process.stderr 也是可寫流)。

可讀流

而與之對應的可讀流,好比客戶端的 HTTP 響應,服務器的 HTTP 請求,fs 的讀取流,process.stdin。咱們清楚的看到,與可寫流恰好造成鏡像對照。

fs.createReadStream

運行代碼示例以下(fs.js):

const fs = require("fs");
const rs = fs.createReadStream("./src.txt");

let sentence = "";

rs.on("data", chunk => {
  sentence += chunk;
});

rs.on("end", () => {
  console.log(sentence);
});
複製代碼

控制檯成功打印了一句《楚門的世界》的臺詞:

$ node fs.js 
Good morning, and in case I don't see ya, good afternoon, good evening, a nd good night! 複製代碼

很簡單是否是?

process.stdin

咱們在可寫流中瞭解了 process.stdout。而 process.stdin 是可讀流,所以咱們能夠結合二者。代碼示例以下(process.js):

process.stdin.pipe(process.stdout);
複製代碼

運行此行代碼,咱們的好朋友控制檯,就變成了一臺復讀機。

http

上文提到了,客戶端的 HTTP 響應,服務器的 HTTP 請求是可讀流。而後客戶端的 HTTP 請求、服務器的 HTTP 響應是可寫流

同窗們千萬不要被繞暈。其實咱們細細思考琢磨,恰好很天然。不信?請看如下代碼!(請務必留意代碼註釋)

如下是客戶端(client.js):

const http = require("http");
const options = {
  hostname: "127.0.0.1",
  port: 8000,
  path: "/upload",
  method: "POST"
};
const req = http.request(options, res => {
  process.stdout.write("Client get response: ");
  // res 客戶端的 HTTP 響應(可讀流)
  res.pipe(process.stdout);
});

// req 客戶端的 HTTP 請求(可寫流)
req.write("Hi!");
req.end();
複製代碼

如下是服務端(server.js):

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.method === "POST" && req.url.includes("/upload")) {
    process.stdout.write("Server get request: ");
    // req 服務器的 HTTP 請求(可讀流)
    req.pipe(process.stdout);
    // res 服務器的 HTTP 響應(可寫流)
    res.write("Hey!");
    res.end();
  } else {
    res.writeHead(404);
    res.end("Not Found!");
  }
});

server.listen(8000);
複製代碼

咱們先運行 server.js 代碼,再運行 client.js 代碼。Node.js 分別在控制檯會輸出:

$ node server.js 
Server get request: Hi!
複製代碼
$ node client.js 
Client get response: Hey!
複製代碼

總結:可寫流有 write、end 方法用來寫入數據。可讀流有 pipe 方法用來消費數據。

咱們能夠記住如下這個簡單公式:

readableStreamSrc.pipe(writableStreamDest);
複製代碼

固然,Node.js 中還有不少這裏沒有提到的其餘可讀流、可寫流(不過,不用擔憂,之後的系列會慢慢分享到。)

但到此,至少,怎麼使用常見的流,咱們成功掌握了。

爲何使用流

但同窗們確定會問,爲何使用流?流的優點又在哪裏?

首先,咱們要知道,在 Node.js 中,I/O都是異步的,因此在和硬盤以及網絡的交互過程當中會涉及到傳遞迴調函數的過程。好比咱們在服務器端,響應請求並讀取返回文件,咱們頗有可能使用 fs.readFile(path, callback) 方式。可是在大量高併發請求到來時,尤爲是讀完的文件目標體積很大時,此時將會消耗大量的內存,從而形成用戶鏈接緩慢的問題。

既然如上文所介紹,req、res 都是流對象,咱們就可使用 fs.createReadStream(path) 獲得一個文件可讀流對象,而後 rs.pipe(res) 便可。

這樣,文件數據就能夠一小塊一小塊的傳輸過去,客戶端鏈接也會更快,服務器壓力也會更小。固然使用 pipe,還有不少不少優點,好比流的背壓自動控制,組合其餘流模塊等等。

本系列,第一篇,到此爲止。以上只是稍微窺探了 Node.js 流的一點蹤跡。但咱們必須知道,在 Node.js 中流的意義與價值,重視它,並且真正掌握它。

系列計劃

這個系列,計劃會深刻講解如下這些方向:

  1. 每一個流 API 的原理、實踐方式
  2. 流的對象模式(Object Mode)
  3. 流動模式(flowing)與暫停模式(paused)
  4. 流的背壓的原理,以及具體實踐
  5. 社區裏流的實踐(好比與流相關的 npm 包)
  6. 流形成內存泄漏問題
  7. Node.js 流的將來趨勢

除此以外,以及一些筆者忽然想寫的,與流相關的話題、技術探討,都會劃分在這個系列裏。

在 Node.js 裏,流扮演了十分重要的角色,若是你和筆者同樣,都對流的哲學、技術實踐都很感興趣,能夠對此係列保持關注。謝謝~

備註:若有筆者表述不穩當,或者理解錯誤的地方,極其歡迎你們指正,互相學習。

相關文章
相關標籤/搜索