模擬實現和深入理解Node Stream內部機制

whynotgonow發表於2019-03-04

一 模擬實現 stream.Readable & stream.Writable

1 模擬實現 stream.Readable

1) flowing模式的實現

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

class ReadStream extends EventEmitter {
    constructor(path, options) {
        super(path, options);
        
        // 初始化引數
        this.path = path;
        this.flags = options.flags || 'r';
        this.mode = options.mode || 0o666;
        this.pos = this.start = options.start || 0;
        this.end = options.end;
        this.encoding = options.encoding;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        
        
        this.flowing = null;
        this.buffer = Buffer.alloc(this.highWaterMark);
        this.open()
        this.on('newListener', (type, listener) => {
            if (type == 'data') {//on('data')觸發read操作
                this.flowing = true;
                this.read();
            }
        });
    }
    
    read() {
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this.read());
        }
        let howMuchToRead = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark;
        
        fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, (err, bytes) => {
            if (err) {
                if (this.autoClose) {
                    this.destroy();
                }
                return this.emit('error', err);
            }
            if (bytes) {
                let data = this.buffer.slice(0, bytes);
                data = this.encoding ? data.toString(this.encoding) : data;
                this.emit('data', data);
                
                this.pos += bytes;
                
                if (this.end && this.pos > this.end) {
                    return this.endFn();
                } else {
                    if (this.flowing) {
                        this.read();
                    }
                }
            } else {
                return this.endFn();
            }
            
        })
    }
    
    endFn() {
        this.emit('end');
        this.destroy();
    }
    
    open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) {
                if (this.autoClose) {
                    this.destroy();
                    return this.emit('error', err);
                }
            }
            this.fd = fd;
            this.emit('open');// 容錯非同步操作
        })
    }
    
    destroy() {
        fs.close(this.fd, () => {
            this.emit('close');
        });
    }
    
    pipe(dest) {
        this.on('data', data => {
            let flag = dest.write(data);
            if (!flag) {
                this.pause();
            }
        });
        dest.on('drain', () => {
            this.resume();
        });
    }
    
    pause() {
        this.flowing = false;
    }
    
    resume() {
        this.flowing = true;
        this.read();
    }
}

module.exports = ReadStream;

複製程式碼

flowing模式的實現邏輯比價簡單,flowing模式的測試程式碼如下:

let fs = require('fs');
fs.createReadStream();
require('stream');
let ReadStream = require('./ReadStream');
let rs = new ReadStream('1.txt',{
   highWaterMark:3,
    encoding:'utf8'
});

rs.on('readable',function () {
    console.log(rs.length);//3
    console.log(rs.read(1));//讀了1個位元組
    console.log(rs.length);//2
    setTimeout(()=>{
        console.log(rs.length);//又向快取區里加入了highWaterMark個位元組
    },500)
});
複製程式碼

2) paused模式的實現

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

class ReadStream extends EventEmitter {
    constructor(path, options) {
        super(path, options);
        // 初始化引數
        this.path = path;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.buffer = Buffer.alloc(this.highWaterMark);
        this.flags = options.flags || 'r';
        this.encoding = options.encoding;
        this.mode = options.mode || 0o666;
        this.start = options.start || 0;
        this.end = options.end;
        this.pos = this.start;
        this.autoClose = options.autoClose || true;
        this.bytesRead = 0;
        this.closed = false;
        this.flowing;
        this.needReadable = false;
        this.length = 0;
        this.buffers = [];
        this.on('end', function () {
            if (this.autoClose) {
                this.destroy();
            }
        });
        this.on('newListener', (type) => {
            if (type == 'data') {
                this.flowing = true;
                this.read();
            }
            if (type == 'readable') {
                this.read(0);
            }
        });
        this.open();
    }
    
    open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) {
                if (this.autoClose) {
                    this.destroy();
                    return this.emit('error', err);
                }
            }
            this.fd = fd;
            this.emit('open');// 容錯非同步
        });
    }
    
    read(n) {
        if (typeof this.fd != 'number') {
            return this.once('open', () => this.read());
        }
        n = parseInt(n, 10);
        if (n != n) {
            n = this.length;
        }
        if (this.length == 0)
            this.needReadable = true;
        let ret;
        if (0 < n < this.length) {// 從快取區中讀取(shift)資料
            ret = Buffer.alloc(n);
            let b;
            let index = 0;
            while (null != (b = this.buffers.shift())) {
                for (let i = 0; i < b.length; i++) {
                    ret[index++] = b[i];
                    if (index == ret.length) {
                        this.length -= n;
                        b = b.slice(i + 1);
                        this.buffers.unshift(b);//把沒有取完的Buffer再放回快取區
                        break;
                    }
                }
            }
            ret = ret.toString(this.encoding);
        }
        
        let _read = () => {// 把讀取到的資料push到快取區中
            let m = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark;
            fs.read(this.fd, this.buffer, 0, m, this.pos, (err, bytesRead) => {
                if (err) {
                    return
                }
                let data;
                if (bytesRead > 0) {
                    data = this.buffer.slice(0, bytesRead);
                    this.pos += bytesRead;
                    this.length += bytesRead;
                    if (this.end && this.pos > this.end) {
                        if (this.needReadable) {
                            this.emit('readable');
                        }
                        
                        this.emit('end');
                    } else {
                        this.buffers.push(data);
                        if (this.needReadable) {
                            this.emit('readable');
                            this.needReadable = false;
                        }
                        
                    }
                } else {
                    if (this.needReadable) {
                        this.emit('readable');
                    }
                    return this.emit('end');
                }
            })
        }
        if (this.length == 0 || (this.length < this.highWaterMark)) {
            _read();
        }
        return ret;
    }
    
    destroy() {
        fs.close(this.fd, (err) => {
            this.emit('close');
        });
    }
    
    pause() {
        this.flowing = false;
    }
    
    resume() {
        this.flowing = true;
        this.read();
    }
    
    pipe(dest) {
        this.on('data', (data) => {
            let flag = dest.write(data);
            if (!flag) this.pause();
        });
        dest.on('drain', () => {
            this.resume();
        });
        this.on('end', () => {
            dest.end();
        });
    }
}

module.exports = ReadStream;
複製程式碼

read方法

  • 在呼叫完_read()後,read(n)會試著從快取中取資料。

  • 如果_read()是非同步呼叫push方法的,則此時快取中的資料量不會增多,容易出現資料量不夠的現象。

  • 如果read(n)的返回值為null,說明這次未能從快取中取出所需量的資料。此時,消耗方需要等待新的資料到達後再次嘗試呼叫read方法。

  • 在資料到達後,流是通過readable事件來通知消耗方的。

  • 在此種情況下,push方法如果立即輸出資料,接收方直接監聽data事件即可,否則資料被新增到快取中,需要觸發readable事件,消耗方必須監聽這個readable事件,再呼叫read方法取得資料。

  • 另外,流中維護了一個快取,當快取中的資料足夠多時,呼叫read()不會引起_read()的呼叫,即不需要向底層請求資料。state.highWaterMark是給快取大小設定的一個上限閾值。如果取走n個資料後,快取中保有的資料不足這個量,便會從底層取一次資料。

paused模式實現的邏輯相對比較複雜,下圖為read方法的邏輯圖,可以參考一下:

paused模式Readable

paused模式的測試程式碼如下:

let fs = require('fs');
let ReadStream = require('./ReadStream');
let rs = new ReadStream('1.txt', {
    highWaterMark: 3,
    encoding: 'utf8'
});

rs.on('readable', function () {
    console.log(rs.length);// 3 當前快取區的長度
    console.log('char', rs.read(1));
    console.log(rs.length);// 2 當你消耗掉一個位元組之後,快取區變成2個位元組了
    
    //一旦發現緩衝區的位元組數小於最高水位線了,則會現再讀到最高水位線個位元組填充到快取區裡
    setTimeout(() => {
        console.log(rs.length);//5
    }, 500)
});
複製程式碼

2 模擬實現 stream.Writable

先來張Writable的內部機制模擬圖

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

class WriteStream extends EventEmitter {
    constructor(path, options) {
        super(path, options);
        this.path = path;
        this.flags = options.flags || 'w';
        this.mode = options.mode || 0o666;
        this.start = options.start || 0;
        this.pos = this.start;
        this.encoding = options.encoding || 'utf8';
        this.autoClose = options.autoClose;
        this.highWaterMark = options.highWaterMark || 16 * 1024;
        
        this.buffers = [];//快取區
        this.writing = false;//表示內部正在寫入資料
        this.length = 0;//表示快取區位元組的長度
        this.open();
    }
    
    open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) {
                if (this.autoClose) {
                    this.destroy();
                }
                return this.emit('error', err);
            }
            this.fd = fd;
            this.emit('open');
        });
    }
    
    
    write(chunk, encoding, cb) {
        chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, this.encoding);
        let len = chunk.length;
        
        this.length += len;//快取區的長度加上當前寫入的長度
        
        let ret = this.length < this.highWaterMark; //判斷當前最新的快取區是否小於最高水位線
        if (this.writing) {//表示正在向底層寫資料,當前資料必須放在快取區裡
            this.buffers.push({
                chunk,
                encoding,
                cb
            });
        } else {
            this.writing = true;
            this._write(chunk, encoding, () => this.clearBuffer()); //在底層寫完當前資料後要清空快取區
        }
        return ret;
    }
    
    clearBuffer() {
        let data = this.buffers.shift();
        if (data) {
            this._write(data.chunk, data.encoding, () => this.clearBuffer())
        } else {
            //快取區清空的時候,發射'drain'事件
            this.writing = false;
            this.emit('drain');
        }
    }
    
    _write(chunk, encoding, cb) {
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this._write(chunk, encoding, cb));
        }
        fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, bytesWritten) => {
            if (err) {
                if (this.autoClose) {
                    this.destroy();
                    this.emit('error', err);
                }
            }
            this.pos += bytesWritten;
            
            this.length -= bytesWritten;
            
            cb && cb();
        })
    }
    
    destroy() {
        fs.close(this.fd, () => {
            this.emit('close');
        })
    }
}

module.exports = WriteStream;
複製程式碼

Writable實現的邏輯圖如下:

Writable
第一次請求源的資料時首先會先呼叫底層的寫入方法,再次請求源資料時如果此時底層正在寫資料的時候會把請求到的資料放到快取區裡面,底層的寫入方法寫完之後會從快取區里拉取資料寫入。另外資料放到快取區時,如果快取區裡的大小大於或等於highWaterMark時,會觸發'drain'事件停止繼續寫入。

Writable的測試程式碼,如下:

let fs = require('fs');
let WriteStream = require('./WriteStream');
let ws = new WriteStream('./1.txt', {
    flags: 'w',
    mode: 0o666,
    start: 0,
    encoding: 'utf8',
    autoClose: true,
    highWaterMark: 3
});
let n = 9;
ws.on('error', (err) => {
    console.log(err);
});

function write() {
    let flag = true;
    while (flag && n > 0) {
        flag = ws.write(n + "", 'utf8', () => {
            console.log('ok')
        });
        n--;
        console.log('flag=', flag);
    }
}

ws.on('drain', () => {
    console.log('drain');
    write();
});
write();

複製程式碼

二 深入理解NodeStream的內部機制

2.1 stream.Readable

我們先來理清一下通過Readable讀取資料的機制,如下圖中:

模擬實現和深入理解Node Stream內部機制
先來分析一下:

  • 用Readable建立物件readable後,便得到了一個可讀流。
  • 如果實現_read方法,就將流連線到一個底層資料來源。
  • 流通過呼叫_read向底層請求資料,底層再呼叫流的push方法將需要的資料傳遞過來。
  • 當readable連線了資料來源後,下游便可以呼叫readable.read(n)向流請求資料,同時監聽readable的data事件來接收取到的資料。

來段程式碼感受一下:

let {Readable} = require('stream');
let index = 3;
let rs = new Readable({
    read() {//實現_read方法(原始碼裡會將read處理為_read)
        if (index > 0) {
            this.push(index-- + '');
        } else {
            this.push(null);
        }
    }
});

rs.on('data', (data) => {
    console.log(data.toString());//3 2 1
});
複製程式碼

push()的作用:

  • 消耗方呼叫read(n)促使流輸出資料,而流通過_read()使底層呼叫push方法將資料傳給流。
  • 如果流在流動模式下(state.flowing為true)輸出資料,資料會自發地通過data事件輸出,不需要消耗方反覆呼叫read(n)。
  • 如果呼叫push方法時快取為空,則當前資料即為下一個需要的資料。這個資料可能先新增到快取中,也可能直接輸出。
  • 執行read方法時,在呼叫_read後,如果從快取中取到了資料,就以data事件輸出。
  • 所以,如果_read非同步呼叫push時發現快取為空,則意味著當前資料是下一個需要的資料,且不會被read方法輸出,應當在push方法中立即以data事件輸出。

來段'readable'的列子:

let {Readable} = require('stream');
let index = 9;
let rs = new Readable({
    highWaterMark: 3,
    read() {
        if (index > 0) {
            this.push(index-- + '');
        } else {
            this.push(null);
        }
    }
});

let once = false;
rs.setEncoding('utf8');
rs.on('readable', (chunk) => {
    console.log(rs.read(1));
});
/*
9
8
*/
複製程式碼

觸發'readable'事件的幾種情況:

  • 在流中有資料可讀取時觸發
  • 達到流資料尾部時觸發
  • 當有新資料流到快取區時觸發

2.2 stream.Writable

let {Writable} = require('stream');
let arr = [];
let ws = Writable({
    write(chunk, encoding, cb) {//底層寫入方法
        arr.push(chunk.toString());
        cb();//進入下一次寫入
    }
});

for (i = 0; i < 5; i++) {
    ws.write(i + '', 'utf8', () => {
        console.log('ok');
    });
}
ws.end();
setTimeout(() => {
    console.log(arr);// [ '0', '1', '2', '3', '4' ]
});
複製程式碼

2.3 pipe

當寫入速度過快,把快取區裝滿了之後,就會出現「背壓」,這個時候是需要告訴底層暫停寫入,當快取區佇列釋放之後,Writable Stream 會觸發一個'drain'事件,恢復底層寫入。

let {Writable, Readable} = require('stream');
let i = 0;
let rs = Readable({
    highWaterMark: 2,
    read() {
        if (i < 10) {
            this.push(i++ + '');
        } else {
            this.push(null);
        }
    }
});

let ws = Writable({
    highWaterMark: 2,
    write(chunk, encoding, cb) {
        console.log(chunk.toString());//0
    }
});

rs.pipe(ws);
setTimeout(() => {
    console.log('Readable快取區length:', rs._readableState.length);//2 
    console.log('Writable快取區length:', ws._writableState.length);//2
});
/*
0
Readable快取區length: 2
Writable快取區length: 2
*/
複製程式碼

此時ws的write方法沒有完全執行完成,所以快取區裡並沒有減掉它的長度,所以在這個地方(write函式裡面沒有執行cb)Writable快取區length仍然是2。

2.4 Duplex

Duplex 流是同時實現了 Readable 和 Writable 介面的流,但是read 和 write 之間沒有關係,也就是說可讀流和可寫流之間沒有關係。

let {Duplex} = require('stream');
let index = 0;
let duplex = Duplex({//
    read() {
        if (index++ < 3) {
            this.push('a');
        } else {
            this.push(null);
        }
        
    },
    write(chunk, encoding, cb) {
        console.log(chunk.toString().toUpperCase());
        cb();
    }
});

process.stdin.pipe(duplex).pipe(process.stdout);// 在控制檯的讀和寫互不干擾,沒有關係

複製程式碼

2.5 Transform

變換流(Transform streams)是一種Duplex流。它的輸出與輸入是通過某種方式關聯的。和所有 Duplex 流一樣,變換流同時實現了 Readable 和 Writable 介面。

let {Transform} = require('stream');
    
let t = Transform({
    transform(chunk, encoding, cb) {
        this.push(chunk.toString().toUpperCase());//從可讀流拿到資料,轉換後寫出
        cb();//相當於write()
    }
});
process.stdin.pipe(t).pipe(process.stdout);
複製程式碼

2.6 物件流

前面我們使用的例子都是Buffer,傳入的引數都是字串,但是也可以向可讀流和可寫流放入物件,可讀流把readableObjectMode引數設定為true,可寫流把writableObjectMode設定為true。

let {Transform} = require('stream');
let fs = require('fs');
let rs = fs.createReadStream('./user.json');
let toJson = Transform({
    readableObjectMode: true,//可以向可讀流裡放物件
    transform(chunk, encoding, cb) {//作為可讀流
        this.push(JSON.parse(chunk.toString()))
    }
});

let outJson = Transform({
    writableObjectMode: true,//可以把物件放到可寫流裡
    transform(chunk, encoding, cb) {//作為可寫流
        console.log(chunk);
    }
});
rs.pipe(toJson).pipe(outJson);
// {name: 'Lucy'}
複製程式碼

三 參考文獻

個人能力有限,如果文章有理解不正確的地方,歡迎指正。

相關文章