Node.js流(一) 可讀流及可讀流的實現

wannawanna發表於2018-06-02

流(Stream)到底是什麼

流(Stream)是資料的集合,就跟陣列和字串一樣。不同點就在於Streams可能不是立刻就全部可用,並且不會全部載入記憶體。這使得他非常適合處理大量資料,或者處理每隔一段時間有一個資料片段傳入的情況。

Node.js 提供了多種流物件。 例如, HTTP 請求 和 process.stdout就都是流的例項。

流可以是可讀的、可寫的,或是可讀寫的。所有的流都是 EventEmitter 的例項。

流的型別

Node.js 中有四種基本的流型別:

  • Readable - 可讀流 (例如 fs.createReadStream())
  • Writable - 可寫流 (例如 fs.createWriteStream())
  • Duplex - 可讀寫的流(雙弓流) (例如 net.Socket)
  • Transform - 在讀寫過程中可以修改和變換資料的 Duplex 流 (例如 zlib.createDeflate())

本次介紹可讀流及其原始碼實現

fs.createReadStream()可讀流介紹

可讀流的建立

let fs = require('fs')

let rs = fs.createReadStream('./1.txt', {
  highWaterMark: 3, // 位元組
  flags:'r',
  autoClose:true, // 預設讀取完畢後自動關閉
  start:0,
  end:3,// 流是閉合區間 包start也包end
  encoding:'utf8'
})
複製程式碼
  • highWaterMark每次讀取的位元組數,預設為64kb
  • autoClose 讀取結束是否關閉檔案
  • startend 讀取檔案結束(包含)和開始(包含)的位置,
  • encoding 讀取流之後的編碼

通過fs.createReadStream()就可以建立一個可讀流。

可讀流的事件

可讀流有如下事件

rs.on('open', function () {
    console.log('檔案開啟了')
})
rs.on('data', function (data) {
    console.log('輸出資料', data.toString())
})
rs.on('end', function () {
    console.log('讀取完畢')
})
rs.on('close', function () {
    console.log('檔案關閉了')
})
rs.on('error', function (err) {
    console.log('出錯了', err)
})
複製程式碼
  • open 檔案開啟時,會被觸發
  • data 讀取流的資料時觸發
  • end 流讀取結束觸發
  • close 檔案關閉時觸發
  • error 流讀取失敗觸發

可讀流的方法

可讀流有兩個很重要的模式(flawing)影響了我們使用的方式。

  • 暫停模式
  • 流動模式

所有的可讀流開始的時候都是預設暫停模式,但是它們可以輕易的被切換成流動模式,當我們需要的時候又可以切換成暫停模式。有時候這個切換是自動的。

可以使用resume()pause()方法在這兩種模式之間切換。

當一個流是流動模式的時候,資料是持續的流動,我們需要使用事件去監聽資料的變化。

在流動模式中,如果可讀流沒有監聽者,可讀流的資料會丟失。這就是為什麼當可讀流逝流動模式的時候,我們必須使用data事件去監聽資料的變化。事實上,只需新增一個data事件處理程式即可將暫停的流轉換為流模式

可讀流的實現

可以根據上面介紹的可讀流特性,實現一個可讀流的類

引入Node.js 模組

顯然可讀流是需要 fsevents 這兩個模組的

let fs = require('fs')
let EventEmit = require('events')
複製程式碼

構造方法

class ReadStream extends EventEmit {
    constructor(path, options = {}) {
        super()
        this.path = path
        this.highWaterMark = options.highWaterMark || 64 * 1024
        this.autoClose = options.autoClose || true
        this.start = options.start || 0
        this.end = options.end || null
        this.encoding = options.encoding || null
        this.flags = options.flags || 'r'

        // 引數處理
        this.flawing = false // 預設暫停模式
    }
}    
module.exports = ReadStream    
複製程式碼

ReadStream類是繼承events模組的,預設是暫停模式(this.flawing = false),其他是一些引數的處理。

讀取資料之前的一些處理

讀資料之前,需要開啟檔案,open方法實現

class ReadStream extends EventEmit {
    constructor(path, options = {}) {
        ...
        // 引數處理
        this.flawing = false
        // 開啟檔案 非同步
        this.open()
    }
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) {
                this.emit('error', err)
                this.destroy()
                return
            }
            this.fd = fd
            this.emit('open')
        })
    }
}  
複製程式碼

需要用到fs.open(),這個方法是非同步的。開啟檔案失敗則發射error事件,並銷燬(destroy)可讀流的例項。成功則儲存檔案描述符(fd),發射open事件

destroy()方法實現

destroy() {
    if (typeof this.fd !== 'number') {
        // 檔案沒有開啟
        return this.emit('close')
    }
    fs.close(this.fd, () => {
        this.emit('close')
    })
}
複製程式碼

分兩種情況

  • 檔案開啟失敗,直接發射close事件
  • 流讀取結束需要關閉,使用fs.close()關閉檔案,回撥中觸發close事件

開始讀取資料

read()方法之後(這是非同步的,檔案描述符並沒拿到)就需要讀取資料了。流建立時,預設是暫停模式,只有新增了data事件,才會轉換為流動模式。

構造方法中新增:

//同步執行
class ReadStream extends EventEmit {
    constructor(path, options = {}) {
        ...
        // 開啟檔案 非同步
        this.open()
        //同步執行
        this.on('newListener', (type) => {
            if (type === 'data') {
                this.flawing = true
                this.read()
            }
        })
    }
}    
複製程式碼

當例項上新增有data事件,就呼叫read()方法讀取資料。事件監聽這裡是同步的,通俗的說: read()要比open先執行。明白這點很關鍵,後面的read要處理fd沒有拿到。

read方法實現:

read() {
    // 檔案沒有開啟,可能就開始讀取
    if(typeof this.fd !== 'number') {
        return this.once('open', this.read)
    }
    let howMuchToRead = this.end ?Math.min(this.highWaterMark, this.end - this.pos + 1) : this.highWaterMark 
    fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, (err, bytesRead) => {
        // 讀取完畢,位置向後移動
        this.pos += bytesRead
        // 發射資料
        let result = this.buffer.slice(0, bytesRead)
        let encodedResult = this.encoding ? result.toString(this.encoding) : result
        this.emit('data', encodedResult);
        // 如果還沒結束,繼續讀
        if(bytesRead === this.highWaterMark && this.flawing) {
            this.read()
        }
        // 沒有讀滿,說明結束了
        if(bytesRead < this.highWaterMark) {
            this.emit('end')
            this.destroy()
        }
    })
}
複製程式碼

1、可讀流新增data事件時,會成流動模式,開始執行read()方法讀取資料,此時檔案並沒有開啟,因此需要open事件觸發後,執行read方法。

2、fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, callback)方法介紹

  • this.fd 開啟檔案拿到的檔案描述符
  • this.buffer 讀取檔案buffer存放。建構函式中初始化this.buffer = Buffer.alloc(this.highWaterMark)
  • 0 存放到this.buffer中的偏移量
  • howMuchToRead 每次從檔案中讀取的長度
  • this.pos 每次從檔案中讀取的位置,需要自己累加維護
  • callback 讀取之後的回撥,其中bytesRead是buffer的長度

要點提醒:

1、每次讀取長度howMuchToRead的計算;

2、發射資料時需要從this.buffer中擷取bytesRead位數;

3、暫停模式下不能讀取(this.flawing=== false)

4、剩下的就是正常流程:this.pos維護累加、沒讀完繼續讀取、讀完之後發射end事件,並銷燬。

pause()和resume()的事件

直接上程式碼了,一看就明白

pause() {
    this.flawing = false
}

resume() {
    this.flawing = true
    this.read()
}
複製程式碼

結語

以上就是全部了,謝謝閱讀!如有紕漏,多多指正。

相關文章