node stream

emilyTian發表於2018-04-07

Stream 是一個抽象介面,對http 伺服器發起請求的request 物件就是一個 Stream,還有stdout(標準輸出)。 Node.js,Stream 有四種流型別:

Readable - 可讀操作。

Writable - 可寫操作。

Duplex - 可讀可寫操作.

Transform - 操作被寫入資料,然後讀出結果。

所有的 Stream 物件都是 EventEmitter 的例項。常用的事件有:

data - 當有資料可讀時觸發。

end - 沒有更多的資料可讀時觸發。

error - 在接收和寫入過程中發生錯誤時觸發。

finish - 所有資料已被寫入到底層系統時觸發。

let fs = require('fs');
let path = require('path');

// 返回的是一個可讀流物件
let rs = fs.createReadStream(path.join(__dirname, '1.txt'), {
    flags: 'r', // 檔案的操作是讀取操作
    encoding: 'utf8', // 預設是null null代表的是buffer
    autoClose: true, // 讀取完畢後自動關閉
    highWaterMark: 3, // 預設是64k  64*1024b
    start: 0,
    end: 3 // 包前又包後
});

rs.setEncoding('utf8');

rs.on('open', function() {
    console.log('檔案開啟了');
});

rs.on('close', function() {
    console.log('關閉');
});

rs.on('error',function (err) {
    console.log(err);
});

rs.on('data',function(data) { // 暫停模式 -> 流動模式
    console.log(data);
    rs.pause(); // 暫停方法 表示暫停讀取,暫停data事件觸發
});

setInterval(function() {
   rs.resume(); //恢復data時間的觸發
}, 3000);

rs.on('end',function() {
    console.log('end')
});


複製程式碼

實現了stream.Readable介面的物件,將物件資料讀取為流資料,當監聽data事件後,開始發射資料

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.open();//開啟檔案 fd

        this.flowing = null; // null就是暫停模式
    
        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(); // 去讀內容了
            }
        })
    }
    
    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) => {
            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 和 Readable 流都會將資料儲存到內部的緩衝器(buffer)中。這些緩衝器可以 通過相應的 writable._writableState.getBuffer() 或 readable._readableState.buffer 來獲取。

緩衝器的大小取決於傳遞給流建構函式的 highWaterMark 選項。 對於普通的流, highWaterMark 選項指定了總共的位元組數。對於工作在物件模式的流, highWaterMark 指定了物件的總數。

當可讀流的實現呼叫stream.push(chunk)方法時,資料被放到緩衝器中。如果流的消費者沒有呼叫stream.read()方法, 這些資料會始終存在於內部佇列中,直到被消費。

當內部可讀緩衝器的大小達到 highWaterMark 指定的閾值時,流會暫停從底層資源讀取資料,直到當前 緩衝器的資料被消費 (也就是說, 流會在內部停止呼叫 readable._read() 來填充可讀緩衝器)。

可寫流通過反覆呼叫 writable.write(chunk) 方法將資料放到緩衝器。 當內部可寫緩衝器的總大小小於 highWaterMark 指定的閾值時, 呼叫 writable.write() 將返回true。 一旦內部緩衝器的大小達到或超過 highWaterMark ,呼叫 writable.write() 將返回 false 。

stream API 的關鍵目標, 尤其對於 stream.pipe() 方法, 就是限制緩衝器資料大小,以達到可接受的程度。這樣,對於讀寫速度不匹配的源頭和目標,就不會超出可用的記憶體大小。

Duplex 和 Transform 都是可讀寫的。 在內部,它們都維護了 兩個 相互獨立的緩衝器用於讀和寫。 在維持了合理高效的資料流的同時,也使得對於讀和寫可以獨立進行而互不影響。

function computeNewHighWaterMark(n) {
  n--;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  n++;
 return n;
}

read(n) { // 想取1個
    if (n > this.length) {
        // 更改快取區大小  讀取五個就找 2的幾次放最近的
        this.highWaterMark = computeNewHighWaterMark(n)
        this.emittedReadable = true;
        this._read();
    }

    // 如果n>0 去快取區中取吧
    let buffer = null;
    let index = 0; // 維護buffer的索引的
    let flag = true;
    if (n > 0 && n <= this.length) { // 讀的內容 快取區中有這麼多
        // 在快取區中取 [[2,3],[4,5,6]]
        buffer = Buffer.alloc(n); // 這是要返回的buffer
        let buf;
        while (flag && (buf = this.buffers.shift())) {
            for (let i = 0; i < buf.length; i++) {
                buffer[index++] = buf[i];
                if (index === n) { // 拷貝夠了 不需要拷貝了
                    flag = false;
                    this.length -= n;
                    let bufferArr = buf.slice(i+1); // 取出留下的部分
                    // 如果有剩下的內容 在放入到快取中
                    if (bufferArr.length > 0) {
                        this.buffers.unshift(bufferArr);
                    }
                    break;
                }
            }
        }
    }
    // 當前快取區 小於highWaterMark時在去讀取
    if (this.length == 0) {
        this.emittedReadable = true;
    }
    if (this.length < this.highWaterMark) {
        if (!this.reading) {
            this.reading = true;
            this._read(); // 非同步的
        }
    }
    return buffer;
}
複製程式碼

完整的程式碼

let fs = require('fs');
let EventEmitter = require('events');

function computeNewHighWaterMark(n) {
      n--;
      n |= n >>> 1;
      n |= n >>> 2;
      n |= n >>> 4;
      n |= n >>> 8;
      n |= n >>> 16;
      n++;
     return n;
}
  
class ReadStream extends EventEmitter {
    constructor(path, options) {
        super();
        this.path = path;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.autoClose = options.autoClose || true;
        this.start = 0;
        this.end = options.end;
        this.flags = options.flags || 'r';

        this.buffers = []; // 快取區 
        this.pos = this.start;
        this.length = 0; // 快取區大小
        this.emittedReadable = false;
        this.reading = false; // 不是正在讀取的
        this.open();
        this.on('newListener', (eventName) => {
            if (eventName === 'readable') {
                this.read();
            }
        })
    }
    
    read(n) { 
        if (n > this.length){
            // 更改快取區大小  讀取五個就找 2的幾次放最近的
            this.highWaterMark = computeNewHighWaterMark(n)
            this.emittedReadable = true;
            this._read();
        }

        // 如果n>0 去快取區中取吧
        let buffer = null;
        let index = 0; // 維護buffer的索引的
        let flag = true;
        if (n > 0 && n <= this.length) { // 讀的內容 快取區中有這麼多
            // 在快取區中取 [[2,3],[4,5,6]]
            buffer = Buffer.alloc(n); // 這是要返回的buffer
            let buf;
            while (flag && (buf = this.buffers.shift())) {
                for (let i = 0; i < buf.length; i++) {
                    buffer[index++] = buf[i];
                    if(index === n){ // 拷貝夠了 不需要拷貝了
                        flag = false;
                        this.length -= n;
                        let bufferArr = buf.slice(i+1); // 取出留下的部分
                        // 如果有剩下的內容 在放入到快取中
                        if(bufferArr.length > 0) {
                            this.buffers.unshift(bufferArr);
                        }
                        break;
                    }
                }
            }
        }
        // 當前快取區 小於highWaterMark時在去讀取
        if (this.length == 0) {
            this.emittedReadable = true;
        }
        if (this.length < this.highWaterMark) {
            if(!this.reading){
                this.reading = true;
                this._read(); // 非同步的
            }
        }
        return buffer;
    }
    
    // 封裝的讀取的方法
    _read() {
        // 當檔案開啟後在去讀取
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this._read());
        }
        // 上來我要喝水 先倒三升水 []
        let buffer = Buffer.alloc(this.highWaterMark);
        fs.read(this.fd, buffer, 0, buffer.length, this.pos, (err, bytesRead) => {
            if (bytesRead > 0) {
                // 預設讀取的內容放到快取區中
                this.buffers.push(buffer.slice(0, bytesRead));
                this.pos += bytesRead; // 維護讀取的索引
                this.length += bytesRead;// 維護快取區的大小
                this.reading = false;
                // 是否需要觸發readable事件
                if (this.emittedReadable) {
                    this.emittedReadable = false; // 下次預設不觸發
                    this.emit('readable');
                }
            } else {
                this.emit('end');
                this.destroy();
            }
        })
    }
    destroy() {
        if (typeof this.fd !== 'number') {
            return this.emit('close')
        }
        fs.close(this.fd, () => {
            this.emit('close')
        })
    }
    open() {
        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;
複製程式碼

LineReader 最後,結合上面所說的暫停模式readable,我們來實現一個行讀取器的例子,我們先定義好一個行讀取器類和它的測試程式碼,它實現的功能就是我們通過建立一個LineReader物件並傳入要讀取的檔案,然後監聽line事件,在每次讀取到一行資料時就會觸發line的回撥函式。

// LineReader 行讀取器
let fs = require('fs');
let EventEmitter = require('events');
let path = require('path');

class LineReader extends EventEmitter {

}

let lineReader = new LineReader(path.join(__dirname, './2.txt'));
lineReader.on('line', function (data) {
    console.log(data); // abc , 123 , 456 ,678
})
複製程式碼

可寫流

var stream = require('stream');
var util = require('util');
util.inherits(Writer, stream.Writable);
let stock = [];
function Writer(opt) {
    stream.Writable.call(this, opt);
}
Writer.prototype._write = function(chunk, encoding, callback) {
    setTimeout(()=>{
        stock.push(chunk.toString('utf8'));
        console.log("增加: " + chunk);
        callback();
    },500)
};
var w = new Writer();
for (var i=1; i<=5; i++){
    w.write("專案:" + i, 'utf8');
}
w.end("結束寫入",function(){
    console.log(stock);
});
管道流 管道提供了一個輸出流到輸入流的機制。通常我們用於從一個流中獲取資料並將資料傳遞到另外一個流中。

const stream = require('stream')

var index = 0;
const readable = stream.Readable({
    highWaterMark: 2,
    read: function () {
        process.nextTick(() => {
            console.log('push', ++index)
            this.push(index+'');
        })
    }
})

const writable = stream.Writable({
    highWaterMark: 2,
    write: function (chunk, encoding, next) {
        console.log('寫入:', chunk.toString())
    }
})

readable.pipe(writable);
複製程式碼

雙工流 可讀可寫

const {Duplex} = require('stream');
const inoutStream = new Duplex({
    write(chunk, encoding, callback) {
        console.log(chunk.toString());
        callback();
    },
    read(size) {
        this.push((++this.index)+'');
        if (this.index > 3) {
            this.push(null);
        }
    }
});

inoutStream.index = 0;
process.stdin.pipe(inoutStream).pipe(process.stdout);
複製程式碼

相關文章