Nodejs教程24:Stream流

LeeChen發表於2019-03-19

閱讀更多系列文章請訪問我的GitHub部落格,示例程式碼請訪問這裡

File System的問題

我們通常會使用File System模組對檔案進行讀取,如下:

fs.readFile('./test.txt', (error, buffer) => {
  if (error) {
    console.error(error)
  } else {
    // 讀取檔案成功
    res.write(buffer)
  }
})
複製程式碼

這樣操作簡單有效,但這也存在一些問題:

  1. 佔用記憶體 使用fs讀取檔案,它是一次性將檔案的所有內容讀取到記憶體中,再一次性傳送到客戶端,因此會佔用大量記憶體。
  2. 資源使用效率低 從磁碟讀取檔案期間,磁碟處於忙碌狀態,而網路處於空閒狀態。 磁碟讀取完成後,開始傳送檔案時,情況正相反,網路處於忙碌狀態,此時磁碟卻處於空閒狀態。

Stream流

相比File System,Stream流讀取檔案是讀一份,發一份,Stream流的寫入操作也有同樣特點,因此可以解決File System在上面提到的2個問題。

接下來實現一個簡單的流,將1.txt檔案的內容寫入到2.txt中:

示例程式碼:/lesson24/stream.js

const fs = require('fs')

// 建立一個可讀流。
const readStream = fs.createReadStream('./1.txt')

// 建立一個可寫流。
const writeStream = fs.createWriteStream('./2.txt')

// 將可讀流讀取的資料,通過管道pipe推送到寫入流中,即可將1.txt的內容,寫入到2.txt中。
readStream.pipe(writeStream)

// 讀取出現錯誤時會觸發error事件。
readStream.on('error', (error) => {
  console.error(error)
})

// 寫入完成時,觸發finish事件。
writeStream.on('finish', () => {
  console.log('finish')
})
複製程式碼

使用Zlib壓縮檔案

可以使用Zlib模組,配合Stream流,實現檔案壓縮功能,如下:

示例程式碼:/lesson24/gzip.js

const fs = require('fs')
// 引入zlib模組,用於實現壓縮功能
const zlib = require('zlib')

// 建立一個可讀流。
const readStream = fs.createReadStream('./google.jpg')

// 建立一個可寫流。
const writeStream = fs.createWriteStream('./google.jpg.gz')

// 建立一個Gzip物件,用於將檔案壓縮成.gz檔案
const gzip = zlib.createGzip()

// 將可讀流讀取的資料,先通過管道pipe推送到gzip中,再推送到寫入流中。
// 也就是先將可讀流的資料壓縮,再推送到可寫流中。
readStream.pipe(gzip).pipe(writeStream)

// 讀取出現錯誤時會觸發error事件。
readStream.on('error', (error) => {
  console.error(error)
})

// 寫入完成時,觸發finish事件。
writeStream.on('finish', () => {
  console.log('finish')
})
複製程式碼

使用流傳輸檔案到前臺

學習了流,我們就可以更加高效地將檔案傳輸到前臺:

示例程式碼:/lesson24/server.js

const http = require('http')
const zlib = require('zlib')
const url = require('url')
const fs = require('fs')

const server = http.createServer((req, res) => {
  const {
    pathname
  } = url.parse(req.url, true)

// 建立一個可讀流。
  const readStream = fs.createReadStream(`./${pathname}`)

  // 建立一個Gzip物件,用於將檔案壓縮成.gz檔案
  const gzip = zlib.createGzip()

  // 將讀取的內容,在通過管道推送到res中,該方法不經過壓縮
  readStream.pipe(res)

  // 處理可讀流報錯,防止請求不存在的檔案
  readStream.on('error', (error) => {
    console.error(error);
    res.writeHead(404)
    res.write('Not Found')
    res.end()
  })
})

server.listen(8080)
複製程式碼

但可以看到,在這個例子裡,雖然實現了使用流傳輸檔案,但並沒有用到gzip壓縮,在傳輸時還是更多地消耗網路資源,接下來可以引入gzip壓縮。

檔案經過gzip壓縮後傳輸到前臺

但此時如果只是簡單的用readStream.pipe(gzip).pipe(res)傳輸檔案,瀏覽器在訪問時無法直接開啟,而是會觸發檔案下載。

這是因為未設定請求頭屬性content-encoding的值,導致瀏覽器無法識別用gzip壓縮過的檔案,這就需要修改請求頭res.setHeader('content-encoding', 'gzip'),讓瀏覽器可以識別。

這樣瀏覽器就可以正常開啟檔案了,但若瀏覽器訪問的是不存在的檔案,瀏覽器會報錯“無法訪問此網站”,這是因為請求頭屬性content-encoding已被設定為gzip,但服務端傳給瀏覽器的是Not Found字串,瀏覽器無法識別。

此時可以使用fs.stat方法,先檢查檔案是否存在,若不存在則返回Not Found,若存在則繼續傳輸。

示例程式碼:/lesson24/server_gzip.js

const http = require('http')
const zlib = require('zlib')
const url = require('url')
const fs = require('fs')

const server = http.createServer((req, res) => {
  const {
    pathname
  } = url.parse(req.url, true)

  // 檔案的相對路徑
  const filepath = `./${pathname}`

  // 檢查檔案是否存在
  fs.stat(filepath, (error, stat) => {
    if (error) {
      console.error(error);
      res.setHeader('content-encoding', 'identity')
      res.writeHead(404)
      res.write('Not Found')
      res.end()
    } else {
      // 建立一個可讀流。
      const readStream = fs.createReadStream(filepath)

      // 建立一個Gzip物件,用於將檔案壓縮成.gz檔案
      const gzip = zlib.createGzip()

      // 向瀏覽器傳送經過gzip壓縮的檔案,設定響應頭,否則瀏覽器無法識別,會自動進行下載。
      res.setHeader('content-encoding', 'gzip')
      // 將讀取的內容,通過gzip壓縮之後,在通過管道推送到res中,由於res繼承自Stream流,因此也可以接收管道的推送。
      readStream.pipe(gzip).pipe(res)

      // 處理可讀流報錯,防止檔案中途被刪除或出錯,導致報錯。
      readStream.on('error', (error) => {
        console.error(error);
        res.setHeader('content-encoding', 'identity')
        res.writeHead(404)
        res.write('Not Found')
        res.end()
      })
    }
  })
})

server.listen(8080)
複製程式碼

相關文章