好程式設計師web前端分享Nodejs學習筆記之Stream模組

好程式設計師IT發表於2019-05-17

好程式設計師 web前端分享 Nodejs學習筆記之Stream模組
  一,開篇分析

  流是一個抽象介面,被 Node 中的很多物件所實現。比如對一個 HTTP 伺服器的請求是一個流,stdout 也是一個流。流是可讀,可寫或兼具兩者的。

  最早接觸 Stream是從早期的unix開始的, 數十年的實踐證明Stream 思想可以很簡單的開發出一些龐大的系統。

  在 unix裡,Stream是透過 "|" 實現的。在node中,作為內建的stream模組,很多核心模組和三方模組都使用到。

  和 unix一樣,node stream主要的操作也是.pipe(),使用者可以使用反壓力機制來控制讀和寫的平衡。

  Stream 可以為開發者提供可以重複使用統一的介面,透過抽象的Stream介面來控制Stream之間的讀寫平衡。

  一個 TCP連線既是可讀流,又是可寫流,而Http連線則不同,一個http request物件是可讀流,而http response物件則是可寫流。

  流的傳輸過程預設是以 buffer的形式傳輸的,除非你給他設定其他編碼形式,以下是一個例子:

1.  <p><font size="3">

2.  </font></p>

3.  <p><font size="3">  var http = require('http') ;</font></p>

4.  <p><font size="3">  var server = http.createServer(function(req,res){</font></p>

5.  <p><font size="3">  res.writeHeader(200, {'Content-Type': 'text/plain'}) ;</font></p>

6.  <p><font size="3">  res.end("Hello,大熊!") ;</font></p>

7.  <p><font size="3">  }) ;</font></p>

8.  <p><font size="3">  server.listen(8888) ;</font></p>

9.  <p><font size="3">  console.log("http server running on port 8888 ...") ;</font></p>


  執行後會有亂碼出現,原因就是沒有設定指定的字符集,比如: “utf-8” 。

  修改一下就好:

   var http = require('http') ;

1. 

2.  <p><font size="3">  var server = http.createServer(function(req,res){</font></p>

3.  <p><font size="3">  res.writeHeader(200,{</font></p>

4.  <p><font size="3">  'Content-Type' : 'text/plain;charset=utf-8' // 新增charset=utf-8</font></p>

5.  <p><font size="3">  }) ;</font></p>

6.  <p><font size="3">  res.end("Hello,大熊!") ;</font></p>

7.  <p><font size="3">  }) ;</font></p>

8.  <p><font size="3">  server.listen(8888) ;</font></p>

9.  <p><font size="3">  console.log("http server running on port 8888 ...") ;</font></p>


  執行結果:

  為什麼使用 Stream

  node中的I/O是非同步的,因此對磁碟和網路的讀寫需要透過回撥函式來讀取資料,下面是一個檔案下載例子

   上程式碼:

1. 

2.  <p><font size="3">  var http = require('http') ;</font></p>

3.  <p><font size="3">  var fs = require('fs') ;</font></p>

4.  <p><font size="3">  var server = http.createServer(function (req, res) {</font></p>

5.  <p><font size="3">  fs.readFile(__dirname + '/data.txt', function (err, data) {</font></p>

6.  <p><font size="3">  res.end(data);</font></p>

7.  <p><font size="3">  }) ;</font></p>

8.  <p><font size="3">  }) ;</font></p>

9.  <p><font size="3">  server.listen(8888) ;</font></p>


  程式碼可以實現需要的功能,但是服務在傳送檔案資料之前需要快取整個檔案資料到記憶體,如果 "data.txt"檔案很

  大並且併發量很大的話,會浪費很多記憶體。因為使用者需要等到整個檔案快取到記憶體才能接受的檔案資料,這樣導致

  使用者體驗相當不好。不過還好 (req,res)兩個引數都是Stream,這樣我們可以用fs.createReadStream()代替fs.readFile()。如下:

   var http = require('http') ;

1. 

2.  <p><font size="3">  var fs = require('fs') ;</font></p>

3.  <p><font size="3">  var server = http.createServer(function (req, res) {</font></p>

4.  <p><font size="3">  var stream = fs.createReadStream(__dirname + '/data.txt') ;</font></p>

5.  <p><font size="3">  stream.pipe(res) ;</font></p>

6.  <p><font size="3">  }) ;</font></p>

7.  <p><font size="3">  server.listen(8888) ;</font></p>


  .pipe()方法監聽fs.createReadStream()的'data' 和'end'事件,這樣"data.txt"檔案就不需要快取整

  個檔案,當客戶端連線完成之後馬上可以傳送一個資料塊到客戶端。使用 .pipe()另一個好處是可以解決當客戶

  端延遲非常大時導致的讀寫不平衡問題。

  有五種基本的 Stream:readable,writable,transform,duplex,and "classic” 。(具體使用請自己查閱api)

  二,例項引入

  當記憶體中無法一次裝下需要處理的資料時,或者一邊讀取一邊處理更加高效時,我們就需要用到資料流。 NodeJS中透過各種Stream來提供對資料流的操作。

  以大檔案複製程式為例,我們可以為資料來源建立一個只讀資料流,示例如下:

   var rs = fs.createReadStream(pathname);

1. 

2.  <p><font size="3">  rs.on('data', function (chunk) {</font></p>

3.  <p><font size="3">  doSomething(chunk) ; // 具體細節自己任意發揮</font></p>

4.  <p><font size="3">  });</font></p>

5.  <p><font size="3">  rs.on('end', function () {</font></p>

6.  <p><font size="3">  cleanUp() ;</font></p>

7.  <p><font size="3">  }) ;</font></p>


  程式碼中 data事件會源源不斷地被觸發,不管doSomething函式是否處理得過來。程式碼可以繼續做如下改造,以解決這個問題。

1.  <p><font size="3">

2.  </font></p>

3.  <p><font size="3">  var rs = fs.createReadStream(src) ;</font></p>

4.  <p><font size="3">  rs.on('data', function (chunk) {</font></p>

5.  <p><font size="3">  rs.pause() ;</font></p>

6.  <p><font size="3">  doSomething(chunk, function () {</font></p>

7.  <p><font size="3">  rs.resume() ;</font></p>

8.  <p><font size="3">  }) ;</font></p>

9.  <p><font size="3">  }) ;</font></p>

10.  <p><font size="3">  rs.on('end', function () {</font></p>

11.  <p><font size="3">  cleanUp();</font></p>

12.  <p><font size="3">  }) ;</font></p>


  給 doSomething函式加上了回撥,因此我們可以在處理資料前暫停資料讀取,並在處理資料後繼續讀取資料。

  此外,我們也可以為資料目標建立一個只寫資料流,如下:
   var rs = fs.createReadStream(src) ;

1. 

2.  <p><font size="3">  var ws = fs.createWriteStream(dst) ;</font></p>

3.  <p><font size="3">  rs.on('data', function (chunk) {</font></p>

4.  <p><font size="3">  ws.write(chunk);</font></p>

5.  <p><font size="3">  }) ;</font></p>

6.  <p><font size="3">  rs.on('end', function () {</font></p>

7.  <p><font size="3">  ws.end();</font></p>

8.  <p><font size="3">  }) ;</font></p>


  doSomething換成了往只寫資料流裡寫入資料後,以上程式碼看起來就像是一個檔案複製程式了。但是以上程式碼存在上邊提到的問題,如果寫入速度跟不上讀取速度的話,只寫資料流內部的快取會爆倉。我們可以根據.write方法的返回值來判斷傳入的資料是寫入目標了,還是臨時放在了快取了,並根據drain事件來判斷什麼時候只寫資料流已經將快取中的資料寫入目標,可以傳入下一個待寫資料了。因此程式碼如下:

   var rs = fs.createReadStream(src) ;

1. 

2.  <p><font size="3">  var ws = fs.createWriteStream(dst) ;</font></p>

3.  <p><font size="3">  rs.on('data', function (chunk) {</font></p>

4.  <p><font size="3">  if (ws.write(chunk) === false) {</font></p>

5.  <p><font size="3">  rs.pause() ;</font></p>

6.  <p><font size="3">  }</font></p>

7.  <p><font size="3">  }) ;</font></p>

8.  <p><font size="3">  rs.on('end', function () {</font></p>

9.  <p><font size="3">  ws.end();</font></p>

10.  <p><font size="3">  });</font></p>

11.  <p><font size="3">  ws.on('drain', function () {</font></p>

12.  <p><font size="3">  rs.resume();</font></p>

13.  <p><font size="3">  }) ;</font></p>


  最終實現了資料從只讀資料流到只寫資料流的搬運,幷包括了防爆倉控制。因為這種使用場景很多,例如上邊的大檔案複製程式, NodeJS直接提供了.pipe方法來做這件事情,其內部實現方式與上邊的程式碼類似。

  下面是一個更加完整的複製檔案的過程:

   var fs = require('fs'),

1. 

2.  <p><font size="3">  path = require('path'),</font></p>

3.  <p><font size="3">  out = process.stdout;</font></p>

4.  <p><font size="3">  var filePath = '/bb/bigbear.mkv';</font></p>

5.  <p><font size="3">  var readStream = fs.createReadStream(filePath);</font></p>

6.  <p><font size="3">  var writeStream = fs.createWriteStream('file.mkv');</font></p>

7.  <p><font size="3">  var stat = fs.statSync(filePath);</font></p>

8.  <p><font size="3">  var totalSize = stat.size;</font></p>

9.  <p><font size="3">  var passedLength = 0;</font></p>

10.  <p><font size="3">  var lastSize = 0;</font></p>

11.  <p><font size="3">  var startTime = Date.now();</font></p>

12.  <p><font size="3">  readStream.on('data', function(chunk) {</font></p>

13.  <p><font size="3">  passedLength += chunk.length;</font></p>

14.  <p><font size="3">  if (writeStream.write(chunk) === false) {</font></p>

15.  <p><font size="3">  readStream.pause();</font></p>

16.  <p><font size="3">  }</font></p>

17.  <p><font size="3">  });</font></p>

18.  <p><font size="3">  readStream.on('end', function() {</font></p>

19.  <p><font size="3">  writeStream.end();</font></p>

20.  <p><font size="3">  });</font></p>

21.  <p><font size="3">  writeStream.on('drain', function() {</font></p>

22.  <p><font size="3">  readStream.resume();</font></p>

23.  <p><font size="3">  });</font></p>

24.  <p><font size="3">  setTimeout(function show() {</font></p>

25.  <p><font size="3">  var percent = Math.ceil((passedLength / totalSize) * 100);</font></p>

26.  <p><font size="3">  var size = Math.ceil(passedLength / 1000000);</font></p>

27.  <p><font size="3">  var diff = size - lastSize;</font></p>

28.  <p><font size="3">  lastSize = size;</font></p>

29.  <p><font size="3">  out.clearLine();</font></p>

30.  <p><font size="3">  out.cursorTo(0);</font></p>

31.  <p><font size="3">  out.write('已完成' + size + 'MB, ' + percent + '%, 速度:' + diff * 2 + 

32.  'MB/s');</font></p>

33.  <p><font size="3">  if (passedLength < totalSize) {</font></p>

34.  <p><font size="3">  setTimeout(show, 500);</font></p>

35.  <p><font size="3">  } else {</font></p>

36.  <p><font size="3">  var endTime = Date.now();</font></p>

37.  <p><font size="3">  console.log();</font></p>

38.  <p><font size="3">  console.log('共用時:' + (endTime - startTime) / 1000 + '秒。');</font></p>

39.  <p><font size="3">  }</font></p>

40.  <p><font size="3">  }, 500);</font></p>


  可以把上面的程式碼儲存為 "copy.js" 試驗一下我們新增了一個遞迴的 setTimeout (或者直接使用setInterval)來做一個旁觀者,

  每 500ms觀察一次完成進度,並把已完成的大小、百分比和複製速度一併寫到控制檯上,當複製完成時,計算總的耗費時間。

  三,總結一下
  (1),理解Stream概念。
  (2),熟練使用相關Stream的api
  (3),注意細節的把控,比如:大檔案的複製,採用的使用 “chunk data” 的形式進行分片處理。
   (4),pipe的使用
  (5),再次強調一個概念:一個TCP連線既是可讀流,又是可寫流,而Http連線則不同,一個http request物件是可讀流,而http response物件則是可寫流。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69913892/viewspace-2644730/,如需轉載,請註明出處,否則將追究法律責任。

相關文章