正如nodeJS文档所说,流(stream)在Node.js中是处理流数据的抽象接口,它提供了基础的API,我们可以通过这些来实现流接口的对象,在nodeJS中有请求流、响应流、文件流等等,这些流的底层都是使用stream模块封装的,流是可读可写的,并且所有流都是EventEmitter的实例,这篇文章简单介绍下流模块并实现一个可读可写的文件流。
Stream模块简介
流(stream)提供了四种类型的流:
- Stream.Readable(创建可读流)
- Stream.Writable(创建可写流)
- Stream.Duplex(创建可读可写流也就是我们经常说的双工流,我们所熟知的TCP sockets就是Duplex流的实例)
- Stream.Transform(变换流,也是一种Duplex流,可写端写入的数据经变换后会自动添加到可读端)
Readable可读流
所有的 Readable 都实现了 stream.Readable 类定义的接口
可读流有两种模式:流动模式(flowing)和暂停模式(paused)
说一下这两种模式,流动模式就是说将数据源源不断的提供给应用,我们用水龙头来做比喻,水龙头坏掉了,水源源不断的流出来,这就是流动模式,只要在Stream上绑定了ondata方法,流动模式就会自动触发,也就是说当我们监听了data方法,水龙头就坏掉了,触发了流动模式,数据源源不断的流出来。在这里数据并不是直接流向应用,而是先 push 到缓存池,在这个缓存池中有一个阈值highWatermark,当超过这个阈值的时候push就会返回false,两种场景下会出现这样的情况:主动执行了.pause()方法;应用读取数据的速度要比数据流向缓存池的速度慢,也就是说水龙头坏了,水流的太快,而你接的速度太慢。这种情况就叫做背压(最好的情况是应用接收一个数据,数据源就生产一个新数据,这样就能整体保持在一个水平,Readable提供pipe方法,用来实现这个功能) 。
另外一种就是暂停模式,在暂停模式下我们必须显示的调用stream.read()方法来从流中读取数据片段,也就是说我们的水龙头修好了,那么就变成了暂停模式,而stream.read()就是我们的水龙头开关,当调用这个方法的时候就打开了开关,数据流出来了,所有处于暂停模式的可读流,可以通过三种途径切换到流动模式:
- 监听 'data' 事件
- 调用 stream.resume() 方法
- 调用 stream.pipe() 方法将数据发送到 Writable
stream.Readable类提供的事件及方法请见nodeJS官方文档
Writable可写流
可写流是对数据写入'目的地'的一种抽象
可写流的原理其实与可读流类似,当数据过来的时候会写入到资源池,当写入的速度很慢或者写入暂停时候,数据流便会进入到队列池缓存起来,我们把写入数据的源头称之为生产者,而当生产者写入的速度太快了以至于把队列池装满,就会出现背压 ,告诉生产者暂停写入数据,当队列池中的空间释放之后,可写流会发送一个drain消息告诉生产者恢复写入。
pipe(管道) 了解了可读流以及可写流,这里穿插一下pipe()方法,它绑定一个 Writable类到可读流上,将可写流自动切换到 flowing 模式并将所有数据传给绑定的Writable。数据流将被自动管理。这样,即使是可读流较快,目标可写流也不会超负荷。
Duplex双工流 和 Transform交换流
双工流是同时实现了Readable和Writable接口的流,所以说双工流是可读可写的。而交换流集成了双工流,所以说它同样适合可读可写的。
Duplex双工流的实现很简单,继承了可读流,并拥有可写流的方法有兴趣的可以看下nodeJS源码,这里不做过多介绍,我们还可以通过options参数来配置它为只可读、只可写或者半工模式。Transform交换流的输入输出是相互关联的,并在在中间做了一次转换处理,常见的有Gzip压缩、解压等,在交换流中还有两个缓存:可读端的缓存和可写端的缓存 ,还有就是在Transform交换流的内部不存在背压 ,因为它是将双工流的 Readable 连接到 Writable,由于 Readable 的生产效率与 Writable 的消费效率是一样的,所以说不存在背压问题。
文件可读流及可写流的实现
可写流WriteStream的实现
let EventEmitter = require('events');
let util = require('util');
let fs = require('fs');
util.inherits(WriteStream, EventEmitter);//继承EventEmitter类
function WriteStream(path, options) {
EventEmitter.call(this);
if (!(this instanceof WriteStream)) {
return new WriteStream(path, options);
}
this.path = path;
this.fd = options.fd;
this.encoding = options.encoding||'utf8';
this.flags = options.flags || 'w';
this.mode = options.mode || 0o666;
this.autoClose = options.autoClose || true;
this.start = options.start || 0;
this.pos = this.start;//开始写入的索引位置
this.open();//打开文件进行操作
this.writing = false;//没有在写入过程 中
this.buffers = [];
this.highWaterMark = options.highWaterMark||16*1024;
//如果监听到end事件,而且要求自动关闭的话则关闭文件
this.on('end', function () {
if (this.autoClose) {
this.destroy()
}
});
}
WriteStream.prototype.close = function(){
//close方法
fs.close(this.fd,(err)=>{
if(err)
this.emit('error',err);
});
}
WriteStream.prototype.open = function () {
//open方法
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err)
return this.emit('error', err);
this.fd = fd;//把文件描述符赋给当前实例的fd属性
//发射open事件
this.emit('open', fd);
});
}
/**
* 会判断当前是后台是否在写入过程中,如果在写入过程中,
则把这个数据放在待处理的缓存中,
如果不在写入过程中,可以直接写。
*/
WriteStream.prototype.write = function (chunk, encoding, cb) {
chunk= Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,this.encoding);
//先把数据放在缓存里
this.buffers.push({
chunk,
encoding,
cb
});
let isFull = this.buffers.reduce((len, item) => len + item.chunk.length, 0)>=this.highWaterMark;
//只有当缓存区写满了,那么清空缓存区的时候才会发射drain事件,否则 不发放
this.needDrain = isFull;
//如果说文件还没有打开,则把写入的方法压入open事件的监听函数。等文件一旦打开,立刻执行写入操作
if (typeof this.fd !== 'number') {
this.once('open', () => {
this._write();
});
return !isFull;
}else{
if(!this.writing){
setImmediate(()=>{
this._write();
this.writing = true;
});
}
return !isFull;
}
}
WriteStream.prototype._write = function () {
let part = this.buffers.shift();
if (part) {
fs.write(this.fd,part.chunk,0,part.chunk.length,null,(err,bytesWritten)=>{
if(err)return this.emit('error',err);
part.cb && part.cb();
this._write();
});
}else{
//发射一个缓存区清空的事件
this.emit('drain');
this.writing = false;
}
}
module.exports = WriteStream;
测试可写流代码:
let fs = require('fs');
let WriteStream = require('./WriteStream');
let ws = WriteStream('./1.txt',{
flags:'w',
mode:0o666,
autoClose:true,//是否自动关闭文件
encoding:'utf8',
start:0,//从第个索引开始写
highWaterMark:3
});
let i = 9;
function write(){
let flag = true;
while(flag && i>0){
flag = ws.write((i--)+'');
console.log('flag',flag);
}
}
write();
ws.on('error',function () {
console.error('error');
});
ws.on('drain',function () {
console.log('drain');
write();
});
可读流ReadStream的实现
1、暂停模式
//暂停模式
let fs = require('fs');
let EventEmitter = require('events');
class ReadStream extends EventEmitter {
constructor(path, options) {
super(path, options);
this.path = path;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.buffer = Buffer.alloc(this.highWaterMark);
this.flags = options.flags || 'r';
this.encoding = options.encoding;
this.mode = options.mode || 0o666;
this.start = options.start || 0;
this.end = options.end;
this.pos = this.start;
this.autoClose = options.autoClose || true;
this.bytesRead = 0;
this.closed = false;
this.flowing;//控制流的模式(三种)
this.needReadable = false;
this.length = 0;
this.buffers = [];
this.on('end', function () {
if (this.autoClose) {
this.destroy();
}
});
this.on('newListener', (type) => {
if (type == 'data') {
this.flowing = true;
this.read();
}
if (type == 'readable') {
this.read(0);
}
});
this.open();
}
open() {
//实现open方法
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) {
if (this.autoClose) {
this.destroy();
return this.emit('error', err);
}
}
this.fd = fd;
this.emit('open');
});
}
read(n) {
if (typeof this.fd != 'number') {
//判断文件是否打开
return this.once('open', () => this.read());
}
n = parseInt(n, 10);
if (n != n) {
n = this.length;
}
if (this.length == 0)
this.needReadable = true;
let ret;
if (0 < n < this.length) {
ret = Buffer.alloc(n);
let b;
let index = 0;
while (null != (b = this.buffers.shift())) {
for (let i = 0; i < b.length; i++) {
ret[index++] = b[i];
if (index == ret.length) {
this.length -= n;
b = b.slice(i + 1);
this.buffers.unshift(b);
break;
}
}
}
if (this.encoding) ret = ret.toString(this.encoding);
}
let _read = () => {
let m = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark;
fs.read(this.fd, this.buffer, 0, m, this.pos, (err, bytesRead) => {
if (err) {
return
}
let data;
if (bytesRead > 0) {
data = this.buffer.slice(0, bytesRead);
this.pos += bytesRead;
this.length += bytesRead;
if (this.end && this.pos > this.end) {
if (this.needReadable) {
this.emit('readable');
}
this.emit('end');
} else {
this.buffers.push(data);
if (this.needReadable) {
this.emit('readable');
this.needReadable = false;
}
}
} else {
if (this.needReadable) {
this.emit('readable');
}
return this.emit('end');
}
})
}
if (this.length == 0 || (this.length < this.highWaterMark)) {
_read(0);
}
return ret;
}
destroy() {
fs.close(this.fd, (err) => {
this.emit('close');
});
}
pause() {
this.flowing = false;
}
resume() {
this.flowing = true;
this.read();
}
pipe(dest) {
this.on('data', (data) => {
let flag = dest.write(data);
if (!flag) this.pause();
});
dest.on('drain', () => {
this.resume();
});
this.on('end', () => {
dest.end();
});
}
}
module.exports = ReadStream;
2、流动模式
let EventEmitter = require('events');
let fs = require('fs');
class ReadStream extends EventEmitter {
constructor(path, options) {
super(path, options);
this.path = path;
this.flags = options.flags || 'r';
this.mode = options.mode || 0o666;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.pos = this.start = options.start || 0;
this.end = options.end;
this.encoding = options.encoding;
this.flowing = null;
this.buffer = Buffer.alloc(this.highWaterMark);
this.open();//准备打开文件读取
//当给这个实例添加了任意的监听函数时会触发newListener
this.on('newListener',(type,listener)=>{
//如果监听了data事件,流会自动切换的流动模式
if(type == '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.end - this.pos + 1,this.highWaterMark):this.highWaterMark;
//this.buffer并不是缓存区
fs.read(this.fd,this.buffer,0,howMuchToRead,this.pos,(err,bytes)=>{//bytes是实际读到的字节数
if(err){
if(this.autoClose)
this.destroy();
return this.emit('error',err);
}
if(bytes){
let data = this.buffer.slice(0,bytes);
this.pos += bytes;
data = this.encoding?data.toString(this.encoding):data;
this.emit('data',data);
if(this.end && this.pos > this.end){
return this.endFn();
}else{
if(this.flowing)
this.read();
}
}else{
return this.endFn();
}
})
}
endFn(){
this.emit('end');
this.destroy();
}
open() {
fs.open(this.path,this.flags,this.mode,(err,fd)=>{
if(err){
if(this.autoClose){
this.destroy();
return this.emit('error',err);
}
}
this.fd = fd;
this.emit('open');
})
}
destroy(){
fs.close(this.fd,()=>{
this.emit('close');
});
}
pipe(dest){
this.on('data',data=>{
let flag = dest.write(data);
if(!flag){
this.pause();
}
});
dest.on('drain',()=>{
this.resume();
});
}
pause(){
this.flowing = false;
}
resume(){
this.flowing = true;
this.read();
}
}
module.exports = ReadStream;
fs模块包含stream方法,fs是一个子类,其中的——read方法会调用父类的read方法
对于代码中所提及的Buffer的进制不太了解的同学请参见文章作为前端需要了解的编码知识 。 可读流及可写流方法请见nodeJS中文文档 这里不对方法做过多介绍,欢迎各位交流,发表你对nodeJS中Stream的理解!