Node.js 流的使用及實現

GetFullStack發表於2018-04-09

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

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

流的特點:

  1. 流的工作模式是典型的 生產消費者模式
  2. Readable和Writable都有緩衝區,在讀寫操作時都是先將資料放入緩衝區,緩衝區的大小由構造引數 highWaterMark 決定。
  3. 緩衝區滿了以後,就形成了「背壓」,返回 false 給消費者,待緩衝區被消費小於 highWaterMark 後,返回 true
  4. 流一般只對strings 和 Buffer進行操作。
  5. Duplex 和 Transform 都是可讀寫的。 在內部,它們都維護了 兩個 相互獨立的緩衝器用於讀和寫。 在維持了合理高效的資料流的同時,也使得對於讀和寫可以獨立進行而互不影響。
  6. 所有流都是 EventEmitter 的子類。

Readable Stream

可讀流事實上工作在下面兩種模式之一:flowing 和 paused 。

模式簡述

  1. 在flowing狀態下,會不停的傳送data事件,給消費者使用資料。
  2. 在paused狀態下,只能通過三種方法重新回來flowing狀態。
    • 監聽 data 事件。
    • 呼叫 resume() 方法。
    • 呼叫 pipe() 方法將資料傳送到 Writable。
  3. 可以呼叫下面二種方法進用paused狀態。
    • 不存在管道目標,呼叫 pause() 方法。
    • 存在管道目標,取消 'data' 事件監聽,並呼叫 unpipe() 方法。

狀態簡述

若 readable._readableState.flowing 為 null,由於不存在資料消費者,可讀流將不會產生資料。

readable._readableState.flowing = null

呼叫 readable.pause() 方法, readable.unpipe() 方法,或者接收 “背壓”(back pressure), 將導致 readable._readableState.flowing 值變為 false。

readable._readableState.flowing = false

在null狀態下,監聽 'data' 事件,呼叫 readable.pipe() 方法,或者呼叫 readable.resume() 方法,readable._readableState.flowing 的值將會變為 true 。

readable._readableState.flowing = true

一般使用程式碼:

let fs = require('fs');
let path = require('path');
let ReadStream = require('./ReadStream')

// 返回的是一個可讀流物件
let rs = new ReadStream(path.join(__dirname, '1.txt'), {
    flags: 'r', // 檔案的操作是讀取操作
    encoding: 'utf8', // 預設是null null代表的是buffer
    autoClose: true, // 讀取完畢後自動關閉
    highWaterMark: 3, // 預設是64k  64*1024b
    start: 0, 
    //end:3 // 包前又包後
});
rs.on('open', function () {
    console.log('檔案開啟了')
});
rs.on('end', function () {
    console.log('讀取結束了')
});
rs.on('close', function () {
    console.log('檔案關閉')
});
rs.on('error', function (err) {
    console.log(err);
});
// flowing模式會一直觸發data事件,直到讀取完畢
// rs.setEncoding('utf8');
rs.on('data', function (data) { // 暫停模式 -> 流動模式
    console.log(data);
    // rs.pause(); // 暫停方法 切換至paused 模式
});
// 輸出----
// 檔案開啟了
// 檔案內容
// 讀取結束了
// 檔案關閉
複製程式碼

模擬實現原碼

let EventEmitter = require('events');
let fs = require('fs');
class ReadStream extends EventEmitter {
    constructor(path, options) {
        super();
        this.path = path;
        this.flags = options.flags || 'r';
        this.autoClose = options.autoClose || true;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.start = options.start || 0;
        this.end = options.end;
        this.encoding = options.encoding || null

        this.flowing = null; // null就是暫停模式
        // 看是否監聽了data事件,如果監聽了 就要變成流動模式

        // 要建立一個buffer 這個buffer就是要一次讀多少
        this.buffer = Buffer.alloc(this.highWaterMark);

        this.pos = this.start; // pos 讀取的位置 可變 start不變的
        this.on('newListener', (eventName, callback) => {
            if (eventName === 'data') {
                // 相當於使用者監聽了data事件
                this.flowing = true;
                // 監聽了 就去讀
                this.read(); // 去讀內容了
            }
        });
        
        this.open(); //開啟檔案 fd
    }
    read() {
        // 此時檔案還沒開啟呢
        if (typeof this.fd !== 'number') {
            // 當檔案真正開啟的時候 會觸發open事件,觸發事件後再執行read,此時fd肯定有了
            return this.once('open', () => this.read())
        }
        // 此時有fd了
        // 應該填highWaterMark?
        // 想讀4個 寫的是3  每次讀3個
        // 123 4
        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) => {
            // 讀到了多少個 累加
            if (bytesRead > 0) {
                this.pos += bytesRead;
                let data = this.encoding ? this.buffer.slice(0, bytesRead).toString(this.encoding) : this.buffer.slice(0, bytesRead);
                this.emit('data', data);
                // 當讀取的位置 大於了末尾 就是讀取完畢了
                if (this.pos > this.end) {
                    this.emit('end');
                    this.destroy();
                }
                if (this.flowing) { // 流動模式繼續觸發
                    this.read();
                }
            } else {
                this.emit('end');
                this.destroy();
            }
        });
    }
    resume() {
        this.flowing = true;
        this.read();
    }
    pause() {
        this.flowing = false;
    }
    destroy() {
        // 先判斷有沒有fd 有關閉檔案 觸發close事件
        if (typeof this.fd === 'number') {
            fs.close(this.fd, () => {
                this.emit('close');
            });
            return;
        }
        this.emit('close'); // 銷燬
    };
    open() {
        // copy 先開啟檔案
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) {
                this.emit('error', err);
                if (this.autoClose) { // 是否自動關閉
                    this.destroy();
                }
                return;
            }
            this.fd = fd; // 儲存檔案描述符
            this.emit('open'); // 檔案開啟了
        });
    }
}
module.exports = ReadStream;
複製程式碼

Writable Stream

寫入流的原理和讀取流一樣,這裡簡單敘述一下:

  1. 呼叫write()傳入寫入內容。
  2. 當達到highWaterMark時,再次呼叫會返回false。
  3. 當寫入完成,並清空buffer時,會出發“drain"事件,這時可以再次寫入。

一般使用方法:

let fs = require('fs');
let path = require('path');
let WriteStream = require('./WriteStream')
let ws = new WriteStream(path.join(__dirname, '1.txt'), {
    highWaterMark: 3,
    autoClose: true,
    flags: 'w',
    encoding: 'utf8',
    mode: 0o666,
    start: 0,
});
let i = 9;

function write() {
    let flag = true;
    while (i > 0 && flag) {
        flag = ws.write(--i + '', 'utf8', () => {
            console.log('ok')
        });
        console.log(flag)
    }
}
write();
// drain只有當快取區充滿後 並且被消費後觸發
ws.on('drain', function () {
    console.log('抽乾')
    write();
});
複製程式碼

模擬實現原碼

let EventEmitter = require('events');
let fs = require('fs');
class WriteStream extends EventEmitter {
    constructor(path, options) {
        super();
        this.path = path;
        this.highWaterMark = options.highWaterMark || 16 * 1024;
        this.autoClose = options.autoClose || true;
        this.mode = options.mode;
        this.start = options.start || 0;
        this.flags = options.flags || 'w';
        this.encoding = options.encoding || 'utf8';

        // 可寫流 要有一個快取區,當正在寫入檔案是,內容要寫入到快取區中
        // 在原始碼中是一個連結串列 => []

        this.buffers = [];

        // 標識 是否正在寫入
        this.writing = false;

        // 是否滿足觸發drain事件
        this.needDrain = false;

        // 記錄寫入的位置
        this.pos = 0;

        // 記錄快取區的大小
        this.length = 0;
        this.open();
    }
    destroy() {
        if (typeof this.fd !== 'number') {
            return this.emit('close');
        }
        fs.close(this.fd, () => {
            this.emit('close')
        })
    }
    open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) {
                this.emit('error', err);
                if (this.autoClose) {
                    this.destroy();
                }
                return
            }
            this.fd = fd;
            this.emit('open');
        })
    }
    write(chunk, encoding = this.encoding, callback = () => {}) {
        chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
        // write 返回一個boolean型別 
        this.length += chunk.length;
        let ret = this.length < this.highWaterMark; // 比較是否達到了快取區的大小
        this.needDrain = !ret; // 是否需要觸發needDrain
        // 判斷是否正在寫入 如果是正在寫入 就寫入到快取區中
        if (this.writing) {
            this.buffers.push({
                encoding,
                chunk,
                callback
            }); // []
        } else {
            // 專門用來將內容 寫入到檔案內
            this.writing = true;
            this._write(chunk, encoding, () => {
                callback();
                this.clearBuffer();
            }); // 8
        }
        return ret;
    }
    clearBuffer() {
        let buffer = this.buffers.shift();
        if (buffer) {
            this._write(buffer.chunk, buffer.encoding, () => {
                buffer.callback();
                this.clearBuffer()
            });
        } else {
            this.writing = false;
            if (this.needDrain) { // 是否需要觸發drain 需要就發射drain事件
                this.needDrain = false;
                this.emit('drain');
            }
        }
    }
    _write(chunk, encoding, callback) {
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this._write(chunk, encoding, callback));
        }
        fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, byteWritten) => {
            this.length -= byteWritten;
            this.pos += byteWritten;

            callback(); // 清空快取區的內容
        });
    }
}

module.exports = WriteStream;
複製程式碼

Duplex Stream

雙工流,又能讀,又能寫

一般使用方法:

let {Duplex} = require('stream');
let d = Duplex({
    read() {
        this.push('hello');
        this.push(null)
    },
    write(chunk, encoding, callback) {
        console.log("write:" + chunk.toString());
        callback();
    }
});
d.on('data', function (data) {
    console.log("read:" + data.toString());
});
d.write('hello');
複製程式碼

Transform

一般使用方法:

let {Transform} =  require('stream');

// 他的引數和可寫流一樣
let tranform1 = Transform({
    transform(chunk,encoding,callback){
        let a = chunk.toString().toUpperCase();
        this.push(a); // 將輸入的內容放入到可讀流中
        //console.log(a)
        callback();
    }
});
let tranform2 = Transform({
    transform(chunk,encoding,callback){
        console.log(chunk.toString());
        callback();
    }
});

// 希望將輸入的內容轉化成大寫在輸出出來
process.stdin.pipe(tranform1).pipe(tranform2);
複製程式碼

相關文章