解讀Node核心模組Stream系列一(Writable和pipe)

夢想攻城獅發表於2018-09-17

node中的流

  • node中stream模組是非常,非常,非常重要的一個模組,因為很多模組都是這個模組封裝的:
  • Readable:可讀流,用來讀取資料,比如 fs.createReadStream()。
  • Writable:可寫流,用來寫資料,比如 fs.createWriteStream()。
  • Duplex:雙工流,可讀+可寫,比如 net.Socket()。
  • Transform:轉換流,在讀寫的過程中,可以對資料進行修改,比如 zlib.createDeflate()(資料壓縮/解壓)。

系列連結

Writable

Writable的例子

  • 客戶端上的 HTTP 請求
  • 伺服器上的 HTTP 響應
  • fs 寫入的流
  • zlib 流
  • crypto 流
  • TCP socket
  • 子程式 stdin
  • process.stdout、process.stderr

Writable的特點和簡化實現

特點

  1. Writable擁有一個快取資料的buffer,同時有一個length來記錄buffer的長度
  2. Writable擁有一個highWaterMark來標明buffer的最大容量,如果length小於highWaterMark,則返回 true,否則返回 false
  3. Writable擁有writing來標識生產者正在增加length
  4. Writable擁有write()從寫入快取區資料的同時也會根據標誌判斷是否呼叫消費者消耗快取區
  5. Writable通過clearBuffer來消費快取區
  6. Writable訂閱'drain'事件當一旦所有當前被緩衝的資料塊都被排空了(被作業系統接受來進行輸出)觸發

建構函式

  1. Writable擁有一個快取資料的buffer,同時有一個length來記錄buffer的長度
  2. Writable擁有一個highWaterMark來標明buffer的最大容量,如果length小於highWaterMark,則返回 true,否則返回 false
  3. Writable擁有writing來標識生產者正在增加length
const EE = require('events');
const util = require('util');
const fs = require('fs');

function Writable(path,options) {//這個引數是原始碼沒有的,這裡主要是為了讀取fs為案例加的
    EE.call(this);//建構函式繼承EventEmiter
    
    this.path = path;
    this.autoClose = options.autoClose || true;
    this.highWaterMark = options.highWaterMark || 64 * 1024;//64k
    this.encoding = options.encoding || null;
    this.flags = options.flags || 'w';//// 這個原始碼沒有的,這裡主要是為了fs讀取案例加的
    this.needEmitDrain = false;// 需要觸發drain事件,預設不需要
    this.position = 0;// 偏移量
    this.cache = []; // 快取區
    this.writing = false;// 是否正在從快取中讀取,生產者增加
    this.length = 0; // 快取區大小,控制長度
    this.open(); // 這個原始碼沒有的,這裡主要是為了fs讀取案例加的
}
util.inherits(Writable, EE);//原型繼承EventEmiter
複製程式碼

write和_write

  1. Writable擁有write()從寫入快取區資料的同時也會根據標誌判斷是否呼叫消費者消耗快取區
  2. Writable通過clearBuffer來消費快取區
  3. Writable訂閱'drain'事件當一旦所有當前被緩衝的資料塊都被排空了(被作業系統接受來進行輸出)觸發
Writable.prototype.write = function (chunk, encoding=this.encoding, callback=()=>{}) {
    chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
    //第一次雖然資料沒有放入到快取,但是由於後面會呼叫_write會將這個長度減去,所以先加上,保證length的正確性
    this.length += chunk.length;
    if (this.length >= this.highWaterMark ) {//消耗快取的長度大於快取的最大容量觸發drain
        this.needDrain = true; 
    }
    if (this.writing) {//如果正在執行寫操作,則後面要寫入目標的資料先存入快取
        this.cache.push({
            chunk, encoding, callback
        })
    } else {// 沒有執行寫操作則執行寫操作
        this.writing = true; 
        //原始碼中在這裡呼叫dowrite()然後呼叫_write()和__writev()
        this._write(chunk, encoding, () => {callback();this.clearBuffer()});
    }
    return this.length < this.highWaterMark //如果快取區的內容大於了highWaterMark 那就返回false
  }
  
// 原始碼中在write()中呼叫dowrite()然後呼叫_write()和__writev()來進行讀操作
Writable.prototype._write = function (chunk, encoding, callback) {
    if (typeof this.fd !== 'number') {//這裡是非同步開啟的操作,要保證有fd,沒有則繫結once等檔案open再觸發
        return this.once('open', () => this._write(chunk, encoding, callback));
    }
    
    // 原始碼中clearBuffer()呼叫dowrite()來消耗快取
    // 原始碼中dowrite()再呼叫onwriteStateUpdate()對length進行更新
    // 所以原始碼中這裡不需要呼叫clearBuffer
    {
        this.position += bytesWritten // 位置增加便宜
        this.length -= bytesWritten;// 快取長度更新
        callback();//裡面包含了clearBuffer()    
    }
}

//原始碼中clearBuffer()實是在end的時候呼叫的,
//原始碼中clearBuffer()呼叫dowrite()然後呼叫_write()和__writev()來消耗記憶體
//原始碼中dowrite()再呼叫onwriteStateUpdate()對快取length進行更新
//這裡只是為了簡化
function clearBuffer(){ 
    let obj = this.cache.shift(); 
    if(obj){
        this._write(obj.chunk,obj.encoding,()=>{obj.callback();this.clearBuffer()});
    }else{
        if(this.needDrain){
            this.writing = false;
            this.needDrain = false;
            this.emit('drain'); // 觸發drain事件
        }
    }
 }
複製程式碼

WriteStream

WriteStream和writable的關係

WriteStream其實是writabl的子類,它繼承了writabl,以fs.createWriteStream為例(node/lib/internal/fs/streams.js)

fs/streams
然後對上面的_write方法進行了覆蓋:
fs/streams._write
以及對_writev方法進行了覆蓋:
fs/streams._writev
並且在其上擴充套件了open和close:
fs/streams.read
fs/streams.read

WriteStream簡化實現

只需要對上面的Writable進行showier的修改

const EE = require('events');
const util = require('util');
const fs = require('fs');

function Writable(path,options) {
    EE.call(this);
    
    this.path = path;
    this.autoClose = options.autoClose || true;
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    this.encoding = options.encoding || null;
    this.flags = options.flags || 'w';
    this.needEmitDrain = false;
    this.position = 0;
    this.cache = []; 
    this.writing = false;
    this.length = 0; 
    this.open(); 
}
util.inherits(Writable, EE);

Writable.prototype.write = function (chunk, encoding=this.encoding, callback=()=>{}) {
    chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
    this.length += chunk.length;
    if (this.length >= this.highWaterMark ) {
        this.needDrain = true; 
    }
    if (this.writing) {
        this.cache.push({
            chunk, encoding, callback
        })
    } else {
        this.writing = true; 
        this._write(chunk, encoding, () => {callback();this.clearBuffer()});
    }
    return this.length < this.highWaterMark 
  }
  
Writable.prototype._write = function (chunk, encoding, callback) {
    if (typeof this.fd !== 'number') {//這裡是非同步開啟的操作,要保證有fd,沒有則繫結once等檔案open再觸發
        return this.once('open', () => this._write(chunk, encoding, callback));
    }
    
    //將_write和fs.write結合
    //原始碼中是覆蓋_write和_writev
    fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, bytesWritten) => {
        this.pos += bytesWritten 
        this.len -= bytesWritten;
        callback();
    });
}

Writable.prototype.destroy = function () {
    if (typeof this.fd != 'number') {
        this.emit('close');
    } else {
        fs.close(this.fd, () => {
            this.emit('close');
        })
    }
}
Writable.prototype.open = function () {
    fs.open(this.path, this.flags, (err, fd) => { // fd檔案描述符 只要檔案開啟了就是number
        if (err) { // 銷燬檔案
            if (this.autoClose) { // 如果需要自動關閉 觸發一下銷燬事件
            this.destroy(); 
            }
            return this.emit('error', err);
        }
        this.fd = fd;
        this.emit('open', fd);
    });
};
function clearBuffer(){ 
    let obj = this.cache.shift(); 
    if(obj){
        this._write(obj.chunk,obj.encoding,()=>{obj.callback();this.clearBuffer()});
    }else{
        if(this.needDrain){
            this.writing = false;
            this.needDrain = false;
            this.emit('drain'); // 觸發drain事件
        }
    }
 }
複製程式碼

pipe

pipe的使用

const fs = require('fs');
const ReadStream = require('./ReadStream');
const WriteStream = require('./WriteStream');
let rs = new ReadStream('./1.txt',{
    highWaterMark:4
});
let ws = new WriteStream('./3.txt',{
    highWaterMark:1
});
rs.pipe(ws);
複製程式碼

pipe的實現

由於pipe方法是在ReadStream上呼叫的,所以我們可以修改上篇的ReadStream來實現,原始碼中Readable和Writable都有pipe的實現

const EE = require('events');
const util = require('util');
const fs = require('fs');
function ReadStream (path,options) {
    this.path = path;
    this.flags = options.flags || 'r'; //用來標識開啟檔案的模式
    this.encoding = options.encoding || null;
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    this.start = options.start || 0; //讀取(檔案)的開始位置
    this.end = options.end || null; //讀取(檔案)的結束位置
    this.autoClose = options.autoClose || true;
    this.flowing = null; // 預設非流動模式
    this.position = this.start // 記錄讀取資料的位置
    this.open(); // 開啟文夾
    this.on('newListener', function (type) {
        if (type === 'data') { // 使用者監聽了data事件
            this.flowing = true;
            this.read();
        }
    })
}
ReadStream.prototype.read = function (){
    if (typeof this.fd !== 'number') {// open操作是非同步的,所以必須等待檔案開啟this.fd存在說明開啟檔案
        return this.once('open', () => this.read());
    }
    let buffer = Buffer.alloc(this.highWaterMark); // 把資料讀取到這個buffer中
    //判斷每次讀取的資料是多少exp:資料來源1234567890 highWaterMark=3
    //最後一次讀取長度為1
    let howMuchToRead = Math.min(this.end - this.pos + 1, this.highWaterMark);
    fs.read(this.fd, buffer, 0, howMuchToRead, this.position, (err, byteRead) => {
    if (byteRead > 0) {
        this.emit('data', buffer.slice(0, byteRead));
        this.position += byteRead;//更新讀取的起點
        if (this.flowing) {//處在flowing模式中就一直讀
            this.read();
        }
    }else{//讀取完畢
        this.flowing = null;
        this.emit('end');
        if(this.autoClose){
            this.destroy();
        }
    }
}
//通過flowing控制暫停還是繼續讀取
ReadStream.prototype.pause = function(){
    this.flowing = false;
}
ReadStream.prototype.resume = function(){
    this.flowing = true;
    this.read();
}
ReadStream.prototype.pipe = function (ws){
    this.on('data', (data)=> {
        let flag = ws.write(data);//讀完之後寫,根據flag判斷不需要讀操作來增加快取的長度
        if (!flag) {
            this.pause();
        }
    });
    ws.on('drain',()=> {//當寫完快取之後,lenght=0,發射drain來恢復讀取往快取中新增內容
        this.resume();
    })
  }
ReadStream.prototype.destroy = function () {
    if (typeof this.fd != 'number') {
        this.emit('close');
    } else {
        fs.close(this.fd, () => {
        this.emit('close');
        })
    }
};

ReadStream.prototype.open = function() {
    fs.open(this.path, this.flags, (err, fd) => {// fd檔案描述符 只要檔案開啟了就是number
        if (err) {
            if (this.autoClose) { // 如果需要自動關閉 觸發一下銷燬事件
            this.destroy(); // 銷燬檔案
        }
        return this.emit('error', err);
    }
    this.fd = fd;
    this.emit('open', fd);
    });
};
複製程式碼

結語:

希望這篇文章能夠讓各位看官對Stream熟悉,因為這個模組是node中的核心,很多模組都是繼承這個模組實現的,如果熟悉了這個模組,對node的使用以及koa等框架的使用將大有好處,接下來會逐步介紹其他流模式本文參考:

  1. 深入理解Node Stream內部機制
  2. node API
  3. node 原始碼

相關文章