首先啥是流呢?
- 流是一组有序的,有起点和终点的字节数据传输手段
- 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
- 流是一个抽象接口,被 Node 中的很多对象所实现。比如HTTP 服务器request和response对象都是流。
Node.js 中有四种基本的流类型
- Readable - 可读的流 (例如 fs.createReadStream()).
- Writable - 可写的流 (例如 fs.createWriteStream()).
- Duplex - 可读写的流 (例如 net.Socket).
- Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate()).
流中的数据有两种模式,二进制模式和对象模式
- 二进制模式, 每个分块都是buffer或者string对象.
- 对象模式, 流内部处理的是一系列普通对象.
所有使用 Node.js API 创建的流对象都只能操作 strings 和 Buffer对象。但是,通过一些第三方流的实现,你依然能够处理其它类型的 JavaScript 值 (除了 null,它在流处理中有特殊意义)。 这些流被认为是工作在 “对象模式”(object mode)。 在创建流的实例时,可以通过 objectMode 选项使流的实例切换到对象模式。试图将已经存在的流切换到对象模式是不安全的。
下面就简单的介绍下 首先是可读流
可读流事实上工作在下面两种模式之一:flowing 和 paused
- 在 flowing 模式下, 可读流自动从系统底层读取数据,并通过 EventEmitter 接口的事件尽快将数据提供给应用。
- 在 paused 模式下,必须显式调用 stream.read() 方法来从流中读取数据片段。
- 所有初始工作模式为 paused 的 Readable 流,可以通过下面三种途径切换到 flowing 模式:
- 监听 'data' 事件
- 调用 stream.resume() 方法
- 调用 stream.pipe() 方法将数据发送到 Writable
- 可读流可以通过下面途径切换到 paused 模式:
- 如果不存在管道目标(pipe destination),可以通过调用 stream.pause() 方法实现。
- 如果存在管道目标,可以通过取消 'data' 事件监听,并调用 stream.unpipe() 方法移除所有管道目标来实现。
如果 Readable 切换到 flowing 模式,且没有消费者处理流中的数据,这些数据将会丢失。 比如, 调用了 readable.resume() 方法却没有监听 'data' 事件,或是取消了 'data' 事件监听,就有可能出现这种情况。
可读流的三种状态
在任意时刻,任意可读流应确切处于下面三种状态之一:
-
readable._readableState.flowing = null
-
readable._readableState.flowing = false
-
readable._readableState.flowing = true
-
若 readable._readableState.flowing 为 null,由于不存在数据消费者,可读流将不会产生数据。 在这个状态下,监听 'data' 事件,调用 readable.pipe() 方法,或者调用 readable.resume() 方法, readable._readableState.flowing 的值将会变为 true 。这时,随着数据生成,可读流开始频繁触发事件。
-
调用 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背压”(back pressure), 将导致 readable._readableState.flowing 值变为 false。 这将暂停事件流,但 不会 暂停数据生成。 在这种情况下,为 'data' 事件设置监听函数不会导致 readable._readableState.flowing 变为 true。
-
当 readable._readableState.flowing 值为 false 时, 数据可能堆积到流的内部缓存中。
创建可读流
let rs=fs.createReadStream(path.join(__dirname,'1.txt'),{
flags:'r',//文件的操作 默认r
encoding:'utf8',//编码 默认buffer
autoClose:true,//是否自动关闭 默认true
start:0,//开始读取的位置 默认0
end:10,//结束读取的位置(包含)默认读完
highWaterMark:3,// 一次读取多少字节 默认读是64k 也就是64*1024
//还有fd 文件描述符 默认null ,mode 文件操作权限,默认为 0o666
})
//这里的第一个参数路径;要说下: 是相对于项目根目录的;跟我们平常的相对路径是不一样的;
//返回值是一个读流对象;
// 监听data事件;获取每一次读到的内容;流切换到流动模式,数据会被尽可能快的读出
rs.on('data',function (data) {
console.log(data);
});
//监听end事件 该事件会在读完数据后被触发
rs.on('end', function () {
console.log('读取完成');
});
//还有open close error一些事件 看名字就知道干啥的
//暂停和恢复触发data;通过pause()方法和resume()方法
rs.on('data', function (data) {
rs.pause();
console.log(data);
});
setTimeout(function () {
rs.resume();
},2000);
复制代码
可写流
//和读流参数基本差不多
let ws=fs.createWriteStream('1.txt', {
flags: 'w', //默认是'w'
encoding: 'utf8',//默认是utf8
highWaterMark: 2, // 写入缓存区的默认大小 默认是16k 16*1024
})
let flag=ws.write('aaaa','utf8',()=>{
console.log('写入成功');
})
//参数:1.写入的数据buffer/string 2.编码编码格式chunk为字符串时有用可选, 3.写入成功后的回调
//返回值为布尔值,系统缓存区满时为false,未满时为true
//end方法;不是on('end',cb);在调完end方法后;不能再调用write方法
ws.end(chunk,[encoding],[callback]);
//表明接下来没有数据要被写入 Writable 通过传入可选的 chunk 和 encoding 参数,可以在关闭流之前再写入一段数据
ws.on('finish', () => {
console.error('所有的写入已经完成!');
});
//在调用了 ws.end() 方法,且缓冲区数据都已经传给底层系统之后, 'finish' 事件将被触发。
//drain方法
//当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false。 一旦所有当前所有缓存的数据块都排空了(被操作系统接受来进行输出), 那么 'drain' 事件就会被触发
//建议, 一旦 write() 返回 false, 在 'drain' 事件触发前, 不能写入任何数据块
//感受下
let i = 10;
function write() {
let flag = true;
while (i && flag) {
flag = ws.write("1");
i--;
console.log(flag);
}
}
write(); //true false
ws.on('drain', () => {
console.log("drain");
write();
});
//true false drain 5次
复制代码
pipe 用法
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to);
复制代码
原理
var ws = fs.createWriteStream('./2.txt');
var rs = fs.createReadStream('./1.txt');
rs.on('data', function (data) {
var flag = ws.write(data);
if (!flag)
rs.pause();
});
ws.on('drain', function () {
rs.resume();
});
rs.on('end', function () {
ws.end();
});
//将数据的滞留量限制到一个可接受的水平,以使得不同速度的来源和目标不会淹没可用内存。
复制代码
简单的实现下可读流
let fs = require('fs');
let EventEmitter = require('events');
class ReadStream extends EventEmitter {//可以on就是发布订阅
constructor(path, options) {
super(path, options);
this.path = path;
this.fd = options.fd;
this.flags = options.flags || 'r';
this.encoding = options.encoding;
this.start = options.start || 0;
this.pos = this.start;
this.end = options.end;
this.flowing = false;
this.autoClose = true;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.buffer = Buffer.alloc(this.highWaterMark);
this.length = 0;
this.on('newListener', (type, listener) => {
if (type == 'data') {//检测到有监听data的事件 就变成流动模式
this.flowing = true;
this.read();
}
});
this.on('end', () => {
if (this.autoClose) {
this.destroy();
}
});
this.open();//只要调用这个类 默认打开文件
}
read() {
if (typeof this.fd != 'number') {
return this.once('open', () => this.read());
}
let n = this.end ? Math.min(this.end - this.pos, this.highWaterMark) : this.highWaterMark;
fs.read(this.fd,this.buffer,0,n,this.pos,(err,bytesRead)=>{
//参数: 文件描述符 读到哪里(容器) 从哪开始 读到哪个位置 偏移 回调
if(err){
return;
}
if(bytesRead){
let data = this.buffer.slice(0,bytesRead);
data = this.encoding?data.toString(this.encoding):data;
this.emit('data',data);
this.pos += bytesRead;
if(this.end && this.pos > this.end){
return this.emit('end');
}
if(this.flowing)
this.read();
}else{
this.emit('end');
}
})
}
open() {
//打开文件 (异步)
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) return this.emit('error', err);
this.fd = fd;
this.emit('open', fd);
})
}
end() {
if (this.autoClose) {
this.destroy();
}
}
destroy() {
//关闭文件
fs.close(this.fd, () => {
this.emit('close');
})
}
}
module.exports = ReadStream;
复制代码
简单的实现下可写流
let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends EventEmitter{
constructor(path, options) {
super(path, options);
this.path = path;
this.fd = options.fd;
this.flags = options.flags || 'w';
this.mode = options.mode || 0o666;
this.encoding = options.encoding;
this.start = options.start || 0;
this.pos = this.start;
this.writing = false;
this.autoClose = true;
this.highWaterMark = options.highWaterMark || 16 * 1024;
this.buffers = [];
this.length = 0;
this.open();
}
open() {
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) return this.emit('error', err);
this.fd = fd;
this.emit('open', fd);
})
}
write(chunk, encoding, cb) {
if (typeof encoding == 'function') {
cb = encoding;
encoding = null;
}
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, this.encoding || 'utf8');
let len = chunk.length;
this.length += len;
let ret = this.length < this.highWaterMark;
if (this.writing) {
this.buffers.push({
chunk,
encoding,
cb,
});
} else {
this.writing = true;
this._write(chunk, encoding,this.clearBuffer.bind(this));
}
return ret;
}
_write(chunk, encoding, cb) {
if (typeof this.fd != 'number') {
return this.once('open', () => this._write(chunk, encoding, cb));
}
fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, written) => {
if (err) {
if (this.autoClose) {
this.destroy();
}
return this.emit('error', err);
}
this.length -= written;
this.pos += written;
cb && cb();
});
}
clearBuffer() {
let data = this.buffers.shift();
if (data) {
this._write(data.chunk, data.encoding, this.clearBuffer.bind(this))
} else {
this.writing = false;
this.emit('drain');
}
}
end() {
if (this.autoClose) {
this.emit('end');
this.destroy();
}
}
destroy() {
fs.close(this.fd, () => {
this.emit('close');
})
}
}
module.exports = WriteStream;
复制代码