流(stream)是一种在 Node.js 中处理流式数据的抽象接口。 stream 模块提供了一些基础的 API,用于构建实现了流接口的对象。
Node.js 提供了多种流对象。 例如,发送到 HTTP 服务器的请求和 process.stdout 都是流的实例。
流可以是可读的、可写的、或是可读写的。 所有的流都是 EventEmitter 的实例。
流的类型
Node.js 中有四种基本的流类型(本篇主要说前两种):
- Writable - 可写入数据的流(例如 fs.createWriteStream())
- Readable - 可读取数据的流(例如 fs.createReadStream())
- Duplex - 可读又可写的流(例如 net.Socket)
- Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate())
缓冲
流中一个相当重要的概念,无论读写流都是通过缓冲来实现的。 可写流和可读流都会在一个内部的缓冲器中存储数据,可以分别使用的 writable.writableBuffer 或 readable.readableBuffer 来获取,可缓冲的数据的数量取决于传入流构造函数的 highWaterMark 选项,默认情况下highWaterMark 64*1024个字节 读写的过程都是将数据读取写入缓冲,然后在将数据读出或者写入文件。
几个重要的底层方法
- writable.write(chunk[, encoding][, callback]) writable.write() 方法向流中写入数据,并在数据处理完成后调用 callback 。如果有错误发生, callback 不一定 以这个错误作为第一个参数并被调用。要确保可靠地检测到写入错误,应该监听 'error' 事件。 在确认了 chunk 后,如果内部缓冲区的大小小于创建流时设定的 highWaterMark 阈值,函数将返回 true 。 如果返回值为 false ,应该停止向流中写入数据,直到 'drain' 事件被触发。 当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false。 一旦所有当前所有缓存的数据块都排空了(被操作系统接受来进行输出), 那么 'drain' 事件就会被触发。
- readable.read([size])
来一个小例子,有助于理解
// pipe
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
highWaterMark:1
})
let ws = fs.createWriteStream('./5.txt',{
highWaterMark:2
})
let index = 1;
rs.on('data', (data) => {
console.log(index++)
let flag = ws.write(data); // 当内部的可写缓冲的总大小小于 highWaterMark 设置的阈值时,
//调用 writable.write() 会返回 true。 一旦内部缓冲的大小达到或超过 highWaterMark 时,则会返回 false。
if (!flag) { //内部缓冲超过highWaterMark
rs.pause()
}
})
let wsIndex = 1;
ws.on('drain', () => {
console.log('ws'+wsIndex++)
rs.resume()
})
// 1 2 ws1 3 4 ws2 5 6 ws3
复制代码
几个重要的事件监听
前面已经说了所有的流都是 EventEmitter 的实例,那么就可以on,可以emit等等
- rs.on('data',()) //读入缓冲
- ws.on('drain',()) //写的缓冲被清空
上面的例子中 当写缓冲大于highWaterMark时 我们就要暂停读取,等待监听到drain事件,然后重新启动rs.resume()读取
其实啊,在工作中也是很少直接这用到的,我们可以直接用pipe rs.pipe(ws)即可 这样就给一个可读流写入到一个可写流当中
自己实现的可读流
let EventEmitter = require('events'); //所有的流都是 EventEmitter 的实例,流继承EventEmitter
let fs = require('fs');
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.autoClose = options.autoClose || true;
this.flags = options.flags || 'r';
this.encoding = options.encoding || null;
this.start = options.start || 0;
this.end = options.end || null;
this.highWaterMark = options.highWaterMark || 64 * 1024;
// 应该有一个读取文件的位置 可变的(可变的位置)
this.pos = this.start;
// 控制当前是否是流动模式
this.flowing = null;
// 构建读取到的内容的buffer
this.buffer = Buffer.alloc(this.highWaterMark);
// 当创建可读流 要将文件打开
this.open(); // 异步执行
this.on('newListener', (type) => {
if(type === 'data'){ // 用户监听了data事件,就开始读取吧
this.flowing = true;
this.read();// 开始读取文件
}
});
}
read(){
// 这时候文件还没有打开呢,等待着文件打开后再去读取
if(typeof this.fd !== 'number'){
// 等待着文件打开,再次调用read方法
return this.once('open',()=>this.read());
}
// 开始读取了
// 文件可能有10个字符串
// start 0 end 4
// 每次读三个 3
// 0-2
// 34
let howMuchToRead = this.end ? Math.min(this.highWaterMark,this.end - this.pos+1) :this.highWaterMark
// 文件描述符 读到哪个buffer里 读取到buffer的哪个位置
// 往buffer里读取几个,读取的位置
fs.read(this.fd, this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
if (bytesRead>0){ // 读到内容了
this.pos += bytesRead;
// 保留有用的
let r = this.buffer.slice(0, bytesRead);
r = this.encoding ? r.toString(this.encoding) : r;
// 第一次读取
this.emit('data', r);
if (this.flowing) {
this.read();
}
}else{
this.end = true;
this.emit('end');
this.destroy();
}
});
}
destroy() { // 判断文件是否打开 (将文件关闭掉)
if (typeof this.fd === 'number') {
fs.close(this.fd, () => {
this.emit('close');
});
return;
}
this.emit('close');
}
open() { // 打开文件的逻辑
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err);
if (this.autoClose) {
this.destroy(); // 销毁 关闭文件(触发close事件)
} return;
}
this.fd = fd;
this.emit('open'); // 触发文件开启事件
});
}
pause(){
this.flowing = false;
}
resume(){
this.flowing = true;
this.read(); // 继续读取
}
}
module.exports = ReadStream;
复制代码
自己实现的可写流
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.mode = options.mode || 0o666;
this.highWaterMark = options.highWaterMark || 16*1024;
this.start = options.start || 0;
this.autoClose = options.autoClose|| true;
this.encoding = options.encoding || 'utf8';
// 是否需要触发drain事件
this.needDrain = false;
// 是否正在写入
this.writing = false;
// 缓存 正在写入就放到缓存中
this.buffer = [];
// 算一个当前缓存的个数
this.len = 0;
// 写入的时候也有位置关系
this.pos = this.start;
this.open();
}
// 0 [1 2]
write(chunk, encoding = this.encoding,callback){
chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
this.len += chunk.length;// 每次调用write就统计一下长度
this.needDrain = this.highWaterMark <= this.len;
// this.fd
if(this.writing){
this.buffer.push({chunk,encoding,callback});
}else{
// 当文件写入后 清空缓存区的内容
this.writing = true; // 走缓存
this._write(chunk,encoding,()=>this.clearBuffer());
}
return !this.needDrain; // write 的返回值必须是true / false
//这时候可以回头看一下上面的例子,在this.len >= this.higWaterMark的时候,返回了一个fasle,例子中就暂停读取了。等待写入完成
}
_write(chunk,encoding,callback){
if (typeof this.fd !== 'number') {
return this.once('open', () => this._write(chunk, encoding, callback));
}
// fd是文件描述符 chunk是数据 0 写入的位置和 长度 , this.pos偏移量
fs.write(this.fd, chunk,0,chunk.length,this.pos,(err,bytesWritten)=>{
this.pos += bytesWritten;
this.len -= bytesWritten; // 写入的长度会减少
callback();
});
}
clearBuffer(){
let buf = this.buffer.shift();
if(buf){
this._write(buf.chunk, buf.encoding, () => this.clearBuffer());
}else{
this.writing = false;
this.needDrain = false; // 触发一次drain 再置回false 方便下次继续判断
this.emit('drain');
}
}
destroy(){
if(typeof this.fd === 'number'){
fs.close(this.fd,()=>{
this.emit('close');
});
return
}
this.emit('close');
}
open(){
fs.open(this.path,this.flags,this.mode,(err,fd)=>{
if(err){
this.emit('error');
this.destroy();
return
}
this.fd = fd;
this.emit('open');
});
}
}
module.exports = WriteStream;
复制代码
以上就是流的一些基础知识,流的简单应用以及自己实现的可读流可写流。当然有很多不足之处,希望朋友们提出指正。也希望和各位朋友一起学习分享!