手寫node可讀流之流動模式

tenor發表於2018-05-31

node的可讀流基於事件

可讀流之流動模式,這種流動模式會有一個"開關",每次當"開關"開啟的時候,流動模式起作用,如果將這個"開關"設定成暫停的話,那麼,這個可讀流將不會去讀取檔案,直到將這個"開關"重新置為流動。

讀取檔案流程

讀取檔案內容的流程,主要為:

  1. 開啟檔案,開啟檔案成功,將觸發open事件,如果開啟失敗,觸發error事件和close事件,將檔案關閉。
  2. 開始讀取檔案中的內容,監聽data事件,資料處於流動狀態,可通過修改開關的狀態來暫停讀取。
  3. 每次讀取到的內容放入快取中,並通過data事件將資料釋出出去。
  4. 當檔案中的內容讀取完畢之後,將檔案關閉。

這一系列動作都是基於事件來進行操作的,而node中的事件我們都知道是一種釋出訂閱模式來實現的。

下面我們來看一看,node是如何使用可讀流來讀取檔案中的內容?

node 可讀流引數

首先我們通過fs模組來建立一個可讀流,可讀流接受兩個引數:

  • 第一個引數是要讀取的檔案地址,在這裡指明你要讀取哪個檔案。
  • 第二個引數是可選項,這個引數是一個物件,用來指定可讀流的一些具體的引數。

如下幾個引數我們來一一說明:

  • highWaterMark:設定高水位線,這個引數主要用於在讀取檔案時,可讀流會將檔案中的內容讀取到快取當中,而這裡我們需要建立一個buffer來快取這些資料,所以這個引數是用來設定buffer的大小,如果不對這個引數進行設定的話,可讀流預設的配置64k。

  • flags:這個引數主要用於設定檔案的執行模式,比如說我們具體的操作適用於讀取檔案還是寫入檔案等這些操作。如果是寫入檔案的話那我們,使用的是w。如果是讀取檔案的話那這個操作符就應該是r。

下面這張表格就說明了不同的符號代表不同含義:

符號 含義
r 讀檔案,檔案不存在報錯
r+ 讀取並寫入,檔案不存在報錯
rs 同步讀取檔案並忽略快取
w 寫入檔案,不存在則建立,存在則清空
wx 排它寫入檔案
w+ 讀取並寫入檔案,不存在則建立,存在則清空
wx+ 和w+類似,排他方式開啟
a 追加寫入
ax 與a類似,排他方式寫入
a+ 讀取並追加寫入,不存在則建立
ax+ 作用與a+類似,但是以排他方式開啟檔案
  • autoClose:這個引數主要用於,對檔案的關閉的一些控制。如果檔案再開啟的過程或者其他操作的過程中出現了錯誤的情況下,需要將檔案進行關閉。那這個引數是設定檔案是否自動關閉的功能。

  • encoding:node中用buffer來讀取檔案操作的東西二進位制資料。這些資料展現出來的話我們是一堆亂碼,所以需要,要我們對這個資料指定一個具體的編碼格式。然後將會對這些資料進行編碼轉化,這樣轉化出來的資料就是我們能看懂的資料。

  • starts:這個引數主要用於指定從什麼位置開始讀取檔案中的內容,預設的話是從零開始。

  • ends:這個引數主要用於指定定具體要讀取檔案多長的資料,這裡需要說明一下,這個引數是包括本身的位置,也就是所謂的包前和包後。

下面我們來看看可讀流具體例子:

let fs = require("fs");
let rs = fs.createReadStream("./a.js", {
    highWaterMark: 3,
    encoding: "utf8",
    autoClose: true,
    start: 0,
    end: 9
});
rs.on("open", () => {console.log("open");});
rs.on("close", () => {console.log("close");});
rs.on("data", data => {
    console.log(data);
    rs.pause();//暫停讀取 此時流動模式為暫停模式
});
setInterval(() => {
    rs.resume();//重新設定為流動模式,開始讀取資料
}, 1000);
rs.on("end", () => { console.log("end"); });
rs.on("error", err => { console.log(err); });
複製程式碼

手寫可讀流第一步

上面我們說過,node可讀流是基於node的核心模組事件來完成的,所以在實現我們自己的可讀流時需要繼承events模組,程式碼如下:

let fs = require('fs');
let EventEmitter = require('events');
class ReadStream extends EventEmitter {

}
複製程式碼

繼承了EventEmitter類,我們就可以使用EventEmitter類中的各個方法,並且同樣是採用釋出訂閱的模式了處理事件。

第二步:處理可讀流配置的引數

上面我們提到,node中建立可讀流時可以對這個流配置具體的引數,比如

let rs = fs.createReadStream("./a.js", {
    highWaterMark: 3,
    encoding: "utf8",
    autoClose: true,
    start: 0,
    end: 9
});
複製程式碼

那麼對於這些引數,我們自己實現的可讀流類也需要對這些引數進行處理,那麼這些引數該如何進行處理呢?

constructor(path, options = {}) {
    super();
    this.path = path; //指定要讀取的檔案地址
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    this.autoClose = options.autoClose || true; //是否自動關閉檔案
    this.start = options.start || 0; //從檔案哪個位置開始讀取
    this.pos = this.start; // pos會隨著讀取的位置改變
    this.end = options.end || null; // null表示沒傳遞
    this.encoding = options.encoding || null;// buffer編碼
    this.flags = options.flags || 'r';

    this.flowing = null; // 模式開關
    this.buffer = Buffer.alloc(this.highWaterMark);// 根據設定建立一個buffer儲存讀出來的數
    this.open();
}
複製程式碼

通常配置的原則是以使用者配置的引數為準,如果使用者沒有對這個引數進行設定的話,就採用預設的配置。

實現可讀流第三步:開啟檔案

這裡原理是使用node模組fs中的open方法。首先我們來回顧下fs.open()方法的使用。

fs.open(filename,flags,[mode],callback);
//例項
fs.open('./1,txt','r',function(err,fd){});
複製程式碼

這裡需要說明下,回撥函式callback中有2個引數:

  • 第一個是error,node中非同步回撥都會返回的一個引數,用來說明具體的錯誤資訊
  • 第二個引數是fd,是檔案描述符,用來標識檔案,等價於open函式的第一個引數

好了,現在我們來看看我們自己的可讀流的open方法該如何實現吧:

open() {
    fs.open(this.path, this.flags, (err, fd) => { 
        //fd標識的就是當前this.path這個檔案,從3開始(number型別)
        if (err) {
            if (this.autoClose) { // 如果需要自動關閉則去關閉檔案
                this.destroy(); // 銷燬(關閉檔案,觸發關閉事件)
            }
            this.emit('error', err); // 如果有錯誤觸發error事件
            return;
        }
        this.fd = fd; // 儲存檔案描述符
        this.emit('open', this.fd); // 觸發檔案的開啟的方法
    });
}
複製程式碼

從程式碼上我們可以看出:

  • fs.open函式是非同步函式,也就是說callback是非同步執行的,在成功開啟檔案的情況下,fd這個屬性也是非同步獲取到的,這點需要注意。

  • 另外重要的一點是,如果在開啟檔案發生錯誤時,則表明開啟檔案失敗,那麼此時就需要將檔案關閉。

實現可讀流第四步:讀取檔案內容

上面我們詳細說過,可讀流自身定義了一個"開關",當我們要讀取檔案中的內容的時候,我們需要將這個"開關"開啟,那麼node可讀流本身是如何來開啟這個"開關"的呢?

監聽data事件

node可讀流通過監聽data事件來實現這個"開關"的開啟:

rs.on("data", data => {
    console.log(data);
});
複製程式碼

當使用者監聽data事件的時候,"開關"開啟,不停的從檔案中讀取內容。那麼node是怎麼監聽data事件的呢? 答案就是 事件模組的newListener

這是因為node可讀流是基於事件的,而事件中,伺服器就可以通過newListener事件監聽到從使用者這邊過來的所有事件,每個事件都有對應的型別,當使用者監聽的是data事件的時候,我們就可以獲取到,然後就可以去讀取檔案中的內容了,那我們自己的可讀流該如何實現呢?

// 監聽newListener事件,看是否監聽了data事件,如果監聽了data事件的話,就開始啟動流動模式,讀取檔案中的內容
this.on("newListener", type => {
    if (type === "data") {
        //  開啟流動模式,開始讀取檔案中的內容
        this.flowing = true;
        this.read();
    }
});
複製程式碼

好了,知道了這個"開關"是如何開啟的,那麼這個時候就到了真正讀取檔案中內容的關鍵時候了,先上程式碼先:

read() {
    // 第一次讀取檔案的話,有可能檔案是還沒有開啟的,此時this.fd可能還沒有值
    if (typeof this.fd !== "number") {
        // 如果此時檔案還是沒有開啟的話,就觸發一次open事件,這樣檔案就真的開啟了,然後再讀取
        return this.once("open", () => this.read());
    }
    // 具體每次讀取多少個字元,需要進行計算,因為最後一次讀取倒的可能比highWaterMark小
    let howMuchRead = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark;
    fs.read(this.fd, this.buffer, 0, howMuchRead, this.pos, (err, byteRead) => {
        // this.pos 是每次讀取檔案讀取的位置,是一個偏移量,每次讀取會發生變化
        this.pos += byteRead;
        // 將讀取到的內容轉換成字串串,然後通過data事件,將內容釋出出去
        let srr = this.encoding ? this.buffer.slice(0, byteRead).toString(this.encoding) : this.buffer.slice(0, byteRead);
        // 將內容通過data事件釋出出去
        this.emit("data", srr);
        // 當讀取到到內容長度和設定的highWaterMark一致的話,並且還是流動模式的話,就繼續讀取
        if ((byteRead === this.highWaterMark) && this.flowing) {
            return this.read();
        }
        // 沒有更多的內容了,此時表示檔案中的內容已經讀取完畢
        if (byteRead < this.highWaterMark) {
            // 讀取完成,釋出end方法,並關閉檔案
            this.emit("end");
            this.destory();
        }
    });
}
複製程式碼

這裡我們特別要注意的是:

  • 檔案是否已經開啟,是否獲取到fd,如果沒有開啟的話,則再次觸發open方法
  • 分批次讀取檔案內容,每次讀取的內容是變化的,所以位置和偏移量是要動態計算的
  • 控制讀取停止的條件。

手寫node可讀流之流動模式

實現可讀流第五步:關閉檔案

好了,到現在,基礎的讀取工作已經完成,那麼就需要將檔案關閉了,上面的open和read方法裡面都呼叫了一個方法:destory,沒錯,這個就是關閉檔案的方法,好了,那麼我們來看看這個方法該如何實現吧

destory() {
    if (typeof this.fd !== "number") {
        // 釋出close事件
        return this.emit("close");
    }
    // 將檔案關閉,釋出close事件
    fs.close(this.fd, () => {
        this.emit("close");
    });
}
複製程式碼

當然這塊的原理就是呼叫fs模組的close方法啦。

實現可讀流第六步:暫停和恢復

既然都說了,node可讀流有一個神奇的"開關",就像大壩的閥門一樣,可以控制水的流動,同樣也可以控制水的暫停啦。當然在node可讀流中的暫停是停止對檔案的讀取,恢復就是將開關開啟,繼續讀取檔案內容,那麼這兩個分別對應的方法就是pause()和resume()方法。

那麼我們自己的可讀流類裡面該如何實現這兩個方法的功能呢?非常簡單: 我們在定義類的私有屬性的時候,定義了這樣一個屬性flowing,當它的值為true時表示開關開啟,反之關閉。

pause() {
    this.flowing = false;// 將流動模式設定成暫停模式,不會讀取檔案
}
resume() {
    this.flowing = true;//將模式設定成流動模式,可以讀取檔案
    this.read();// 重新開始讀取檔案
}
複製程式碼

好了,關於node可讀流的實現我們就寫到這裡,快快敲起程式碼,動手實現一個你自己的可讀流吧!

相關文章