流的概念
- 流是一組有序的,有起點和終點的位元組資料傳輸手段
- 它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理
- 流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。
Node.js 中有四種基本的流型別
- Readable – 可讀的流 (例如 fs.createReadStream())。
- Writable – 可寫的流 (例如 fs.createWriteStream()).
- Duplex – 可讀寫的流(雙工流) (例如 net.Socket).
- Transform – 轉換流 在讀寫過程中可以修改和變換資料的 Duplex 流 (例如 zlib.createDeflate())
為什麼使用流
如果讀取一個檔案,使用fs.readFileSync同步讀取,程式會被阻塞,然後所有資料被寫到記憶體中。使用fs.readFile讀取,程式不會阻塞,但是所有資料依舊會一次性全被寫到記憶體,然後再讓消費者去讀取。如果檔案很大,記憶體使用便會成為問題。
這種情況下流就比較有優勢。流相比一次性寫到記憶體中,它會先寫到到一個緩衝區,然後再由消費者去讀取,不用將整個檔案寫進記憶體,節省了記憶體空間。
1.不使用流時檔案會全部寫入記憶體,再又記憶體寫入目標檔案
2.使用流時可以控制流的讀取及寫入速率
流的使用及實現
可讀流createReadStream
可讀流的使用
-
建立可讀流
var rs = fs.createReadStream(path,[options]); 複製程式碼
1.)path讀取檔案的路徑
2.)options
- flags開啟檔案要做的操作,預設為`r`
- encoding預設為null
- start開始讀取的索引位置
- end結束讀取的索引位置(包括結束位置)
- highWaterMark讀取快取區預設的大小64kb
如果指定utf8編碼highWaterMark要大於3個位元組
-
監聽data事件
流切換到流動模式,資料會被儘可能快的讀出
rs.on(`data`, function (data) { console.log(data); }); 複製程式碼
-
監聽end事件
該事件會在讀完資料後被觸發
rs.on(`end`, function () { console.log(`讀取完成`); }); 複製程式碼
-
監聽error事件
rs.on(`error`, function (err) { console.log(err); }); 複製程式碼
-
監聽close事件
與指定{encoding:`utf8`}效果相同,設定編碼
rs.setEncoding(`utf8`); 複製程式碼
-
暫停和恢復觸發data
通過pause()方法和resume()方法
rs.on(`data`, function (data) { rs.pause(); console.log(data); }); setTimeout(function () { rs.resume(); },2000); 複製程式碼
可讀流的簡單實現
- 仿寫可讀流
let fs = require(`fs`); let EventEmitter = require(`events`); class ReadStream extends EventEmitter { 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; this.flags = options.flags || `r`; // 引數的問題 this.flowing = null; // 非流動模式 // 弄一個buffer讀出來的數 this.buffer = Buffer.alloc(this.highWaterMark); this.open(); // {newListener:[fn]} // 次方法預設同步呼叫的 this.on(`newListener`, (type) => { // 等待著 它監聽data事件 if (type === `data`) { this.flowing = true; this.read();// 開始讀取 客戶已經監聽了data事件 } }) } pause(){ this.flowing = false; } resume(){ this.flowing =true; this.read(); } read(){ // 預設第一次呼叫read方法時還沒有獲取fd,所以不能直接讀 if(typeof this.fd !== `number`){ return this.once(`open`,() => this.read()); // 等待著觸發open事件後fd肯定拿到了,拿到以後再去執行read方法 } // 當獲取到fd時 開始讀取檔案了 // 第一次應該讀2個 第二次應該讀2個 // 第二次pos的值是4 end是4 // 一共4個數 123 4 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, (error, byteRead) => { // byteRead真實的讀到了幾個 // 讀取完畢 this.pos += byteRead; // 都出來兩個位置就往後搓兩位 // this.buffer預設就是三個 let b = this.encoding ? this.buffer.slice(0, byteRead).toString(this.encoding) : this.buffer.slice(0, byteRead); this.emit(`data`, b); if ((byteRead === this.highWaterMark)&&this.flowing){ return this.read(); // 繼續讀 } // 這裡就是沒有更多的邏輯了 if (byteRead < this.highWaterMark){ // 沒有更多了 this.emit(`end`); // 讀取完畢 this.destroy(); // 銷燬即可 } }); } // 開啟檔案用的 destroy() { if (typeof this.fd != `number`) { return this.emit(`close`); } fs.close(this.fd, () => { // 如果檔案開啟過了 那就關閉檔案並且觸發close事件 this.emit(`close`); }); } open() { fs.open(this.path, this.flags, (err, fd) => { //fd標識的就是當前this.path這個檔案,從3開始(number型別) if (err) { if (this.autoClose) { // 如果需要自動關閉我在去銷燬fd this.destroy(); // 銷燬(關閉檔案,觸發關閉事件) } this.emit(`error`, err); // 如果有錯誤觸發error事件 return; } this.fd = fd; // 儲存檔案描述符 this.emit(`open`, this.fd); // 觸發檔案的開啟的方法 }); } } module.exports = ReadStream; 複製程式碼
- 驗證
let ReadStream = require(`./ReadStream`); let rs = new ReadStream(`./2.txt`, { highWaterMark: 3, // 位元組 flags:`r`, autoClose:true, // 預設讀取完畢後自動關閉 start:0, //end:3,// 流是閉合區間 包start也包end encoding:`utf8` }); // 預設建立一個流 是非流動模式,預設不會讀取資料 // 我們需要接收資料 我們要監聽data事件,資料會總動的流出來 rs.on(`error`,function (err) { console.log(err) }); rs.on(`open`,function () { console.log(`檔案開啟了`); }); // 內部會自動的觸發這個事件 rs.emit(`data`); rs.on(`data`,function (data) { console.log(data); rs.pause(); // 暫停觸發on(`data`)事件,將流動模式又轉化成了非流動模式 }); setTimeout(()=>{rs.resume()},5000) rs.on(`end`,function () { console.log(`讀取完畢了`); }); rs.on(`close`,function () { console.log(`關閉`) }); 複製程式碼
可寫流createWriteStream
可寫流的使用
-
建立可寫流
var ws = fs.createWriteStream(path,[options]); 複製程式碼
1.)path寫入的檔案路徑
2.)options
- flags開啟檔案要做的操作,預設為`w`
- encoding預設為utf8
- highWaterMark寫入快取區的預設大小16kb
-
write方法
ws.write(chunk,[encoding],[callback]); 複製程式碼
1.)chunk寫入的資料buffer/string
2.)encoding編碼格式chunk為字串時有用,可選
3.)callback 寫入成功後的回撥
返回值為布林值,系統快取區滿時為false,未滿時為true
-
end方法
ws.end(chunk,[encoding],[callback]); 複製程式碼
表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料 如果傳入了可選的 callback 函式,它將作為 `finish` 事件的回撥函式
-
drain方法
-
當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 `drain` 事件就會被觸發
-
建議, 一旦 write() 返回 false, 在 `drain` 事件觸發前, 不能寫入任何資料塊
let fs = require(`fs`); let ws = fs.createWriteStream(`./2.txt`,{ flags:`w`, encoding:`utf8`, highWaterMark:3 }); let i = 10; function write(){ let flag = true; while(i&&flag){ flag = ws.write("1"); i--; console.log(flag); } } write(); ws.on(`drain`,()=>{ console.log("drain"); write(); }); 複製程式碼
-
-
finish方法
在呼叫了 stream.end() 方法,且緩衝區資料都已經傳給底層系統之後, `finish` 事件將被觸發
var writer = fs.createWriteStream(`./2.txt`); for (let i = 0; i < 100; i++) { writer.write(`hello, ${i}! `); } writer.end(`結束 `); writer.on(`finish`, () => { console.error(`所有的寫入已經完成!`); }); 複製程式碼
可寫流的簡單實現
- 仿寫可寫流
let fs = require(`fs`); let EventEmitter = require(`events`); class WriteStream extends EventEmitter { constructor(path, options = {}) { super(); this.path = path; this.flags = options.flags || `w`; this.encoding = options.encoding || `utf8`; this.start = options.start || 0; this.pos = this.start; this.mode = options.mode || 0o666; this.autoClose = options.autoClose || true; this.highWaterMark = options.highWaterMark || 16 * 1024; this.open(); // fd 非同步的 觸發一個open事件當觸發open事件後fd肯定就存在了 // 寫檔案的時候 需要的引數有哪些 // 第一次寫入是真的往檔案裡寫 this.writing = false; // 預設第一次就不是正在寫入 // 快取我用簡單的陣列來模擬一下 this.cache = []; // 維護一個變數 表示快取的長度 this.len = 0; // 是否觸發drain事件 this.needDrain = false; } clearBuffer() { let buffer = this.cache.shift(); if (buffer) { // 快取裡有 this._write(buffer.chunk, buffer.encoding, () => this.clearBuffer()); } else {// 快取裡沒有了 if (this.needDrain) { // 需要觸發drain事件 this.writing = false; // 告訴下次直接寫就可以了 不需要寫到記憶體中了 this.needDrain = false; this.emit(`drain`); } } } _write(chunk, encoding, clearBuffer) { // 因為write方法是同步呼叫的此時fd還沒有獲取到,所以等待獲取到再執行write操作 if (typeof this.fd != `number`) { return this.once(`open`, () => this._write(chunk, encoding, clearBuffer)); } fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, byteWritten) => { this.pos += byteWritten; this.len -= byteWritten; // 每次寫入後就要再記憶體中減少一下 clearBuffer(); // 第一次就寫完了 }) } write(chunk, encoding = this.encoding) { // 客戶呼叫的是write方法去寫入內容 // 要判斷 chunk必須是buffer或者字串 為了統一,如果傳遞的是字串也要轉成buffer chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding); this.len += chunk.length; // 維護快取的長度 3 let ret = this.len < this.highWaterMark; if (!ret) { this.needDrain = true; // 表示需要觸發drain事件 } if (this.writing) { // 正在寫入應該放到記憶體中 this.cache.push({ chunk, encoding, }); } else { // 第一次 this.writing = true; this._write(chunk, encoding, () => this.clearBuffer()); // 專門實現寫的方法 } return ret; // 能不能繼續寫了,false表示下次的寫的時候就要佔用更多記憶體了 } destroy() { if (typeof this.fd != `number`) { this.emit(`close`); } else { 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`, this.fd); }); } } module.exports = WriteStream; 複製程式碼
- 驗證
let WS = require(`./WriteStream`) let ws = new WS(`./2.txt`, { flags: `w`, // 預設檔案不存在會建立 highWaterMark: 1, // 設定當前快取區的大小 encoding: `utf8`, // 檔案裡存放的都是二進位制 start: 0, autoClose: true, // 自動關閉 mode: 0o666, // 可讀可寫 }); // drain的觸發時機,只有當highWaterMark填滿時,才可能觸發drain // 當嘴裡的和地下的都吃完了,就會觸發drain方法 let i = 9; function write() { let flag = true; while (flag && i >= 0) { i--; flag = ws.write(`111`); // 987 // 654 // 321 // 0 console.log(flag) } } write(); ws.on(`drain`, function () { console.log(`幹了`); write(); }); 複製程式碼
pipe方法
pipe方法是管道的意思,可以控制速率
- 會監聽rs的on(`data`),將讀取到的內容呼叫ws.write方法
- 呼叫寫的方法會返回一個boolean型別
- 如果返回了false就呼叫rs.pause()暫停讀取
- 等待可寫流寫入完畢後 on(`drain`)在恢復讀取
pipe方法的使用
let fs = require(`fs`);
let rs = fs.createReadStream(`./2.txt`,{
highWaterMark:1
});
let ws = fs.createWriteStream(`./1.txt`,{
highWaterMark:3
});
rs.pipe(ws); // 會控制速率(防止淹沒可用記憶體)
複製程式碼