流的概念
- 流是一组有序的,有起点和终点的字节数据传输手段
- 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
- 流是一个抽象接口,被 Node 中的很多对象所实现。比如HTTP 服务器request和response对象都是流。
Node.js 中有四种基本的流类型
- Readable - 可读的流 (例如 fs.createReadStream())。
- Writable - 可写的流 (例如 fs.createWriteStream()).
- Duplex - 可读写的流(双工流) (例如 net.Socket).
- Transform - 转换流 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate())
为什么使用流
如果读取一个文件,使用fs.readFileSync同步读取,程序会被阻塞,然后所有数据被写到内存中。使用fs.readFile读取,程序不会阻塞,但是所有数据依旧会一次性全被写到内存,然后再让消费者去读取。如果文件很大,内存使用便会成为问题。 这种情况下流就比较有优势。流相比一次性写到内存中,它会先写到到一个缓冲区,然后再由消费者去读取,不用将整个文件写进内存,节省了内存空间。
1.不使用流时文件会全部写入内存,再又内存写入目标文件


流的使用及实现
可读流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}!\n`); } writer.end('结束\n'); 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); // 会控制速率(防止淹没可用内存)