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);
複製程式碼