nodejs-stream部分

参考:

https://blog.csdn.net/eeewwwddd/article/details/81042225

http://nodejs.cn/api/stream.html#stream_writable_write_chunk_encoding_callback

流(stream)是 Node.js 中处理流式数据的抽象接口。 stream 模块提供了一些 API,用于构建实现了流接口的对象。

Node.js 提供了多种流对象。 例如,HTTP 服务器的请求 process.stdout 都是流的实例。

流可以是可读的、可写的、或者可读可写的。 所有的流都是 EventEmitter 的实例,即可以通过事件的监听得以触发事件并执行一定的操作,如:

  req.on('data', (chunk) => {
    body += chunk;
  });

stream 模块可以通过以下方式使用:

const stream = require('stream');

尽管理解流的工作方式很重要,但是 stream 模块本身主要用于开发者创建新类型的流实例。

对于以消费流对象为主的开发者,极少需要直接使用 stream 模块

Node.js 中有四种基本的流类型:

    Writable - 可写入数据的流(例如 fs.createWriteStream())。
    Readable - 可读取数据的流(例如 fs.createReadStream())。
    Duplex - 可读又可写的流(例如 net.Socket)。
    Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate())

两种模式

 二进制模式

每个分块都是buffer、string对象

对象模式

Node.js 创建的流都是运作在字符串和 Buffer(或 Uint8Array)上。 当然,流的实现也可以使用其它类型的 JavaScript 值(除了 null)。 这些流会以“对象模式”进行操作。

创建流时,可以使用 objectMode 选项把流实例切换到对象模式。 将已存在的流切换到对象模式是不安全的。

 比如如果想创建一个的可以压入任意形式数据的可读流,只要在创建流的时候设置参数objectModetrue即可,例如:Readable({ objectMode: true })

如果readable stream写入的是字符串,那么字符串会默认转换为Buffer,如果在创建流的时候设置Writable({ decodeStrings: false })参数,那么不会做转换。

如果readable stream写入的数据是对象,那么需要这样创建writable stream,Writable({ objectMode: true })

⚠️就是如果输入的数据并不是Buffer(或 Uint8Array格式的时候,那么在创建这个流的时候就要将其设置为对象模式,即设置其的objectMode: true,举例:

const DuplexStream = require('readable-stream').Duplex
const inherits = require('util').inherits

module.exports = PostMessageStream

inherits(PostMessageStream, DuplexStream)

function PostMessageStream (opts) {
  DuplexStream.call(this, {
    objectMode: true,
  })
...
}

 

缓冲

可写流可读流都会在内部的缓冲器中存储数据,可以分别使用的 writable.writableBuffer 或 readable.readableBuffer 来获取。

可缓冲的数据大小取决于传入流构造函数的 highWaterMark 选项。 对于普通的流,highWaterMark 指定了字节的总数。 对于对象模式的流,highWaterMark 指定了对象的总数。

当调用 stream.push(chunk) 时,数据会被缓冲在可读流中。 如果流的消费者没有调用 stream.read(),则数据会保留在内部队列中直到被消费。

一旦内部的可读缓冲的总大小达到 highWaterMark 指定的阈值时,流会暂时停止从底层资源读取数据,直到当前缓冲的数据被消费 (也就是说,流会停止调用内部的用于填充可读缓冲的 readable._read())。

当调用 writable.write(chunk) 时,数据会被缓冲在可写流中。 当内部的可写缓冲的总大小小于 highWaterMark 设置的阈值时,调用 writable.write() 会返回 true。 一旦内部缓冲的大小达到或超过 highWaterMark 时,则会返回 false

stream API 的主要目标,特别是 stream.pipe(),是为了限制数据的缓冲到可接受的程度,也就是读写速度不一致的源头与目的地不会压垮内存。

因为 Duplex 和 Transform 都是可读又可写的,所以它们各自维护着两个相互独立的内部缓冲器用于读取和写入, 这使得它们在维护数据流时,读取和写入两边可以各自独立地运作。 例如,net.Socket 实例是 Duplex 流,它的可读端可以消费从 socket 接收的数据,而可写端则可以将数据写入到 socket。 因为数据写入到 socket 的速度可能比接收数据的速度快或者慢,所以在读写两端独立地进行操作(或缓冲)就显得很重要了。

【1】用于消费流的 API(即读取流中数据)

test.js

const http = require('http');

const server = http.createServer((req, res) => {
  // req 是一个 http.IncomingMessage 实例,它是可读流// res 是一个 http.ServerResponse 实例,它是可写流

  let body = '';
  // 接收数据为 utf8 字符串,
  // 如果没有设置字符编码,则会接收到 Buffer 对象。
  req.setEncoding('utf8');

  // 如果添加了监听器,则可读流会触发 'data' 事件。
  req.on('data', (chunk) => {
    body += chunk;
  });

  // 'end' 事件表明整个请求体已被接收。 
  req.on('end', () => {
    try {
      const data = JSON.parse(body);
      // 响应信息给用户。
      res.write(typeof data);
      res.end();//end()表示写结束
    } catch (er) {
      // json 解析失败。
      res.statusCode = 400;
      return res.end(`错误: ${er.message}`);
    }
  });
});

server.listen(1337);

然后在终端使用node test.js运行该服务器

然后在另一个终端使用curl localhost:1337 -d "{}" 连接服务器localhost:1337 ,-d即post数据data为{} ,返回object

curl localhost:1337 -d "{}"       返回object
curl localhost:1337 -d ""foo""  返回string
curl localhost:1337 -d "not json" 返回 错误: Unexpected token o in JSON at position 1

可写流(比如例子中的 res)会暴露了一些方法,比如 write() 和 end() 用于写入数据到流。

当数据可以从流读取时,可读流会使用 EventEmitter API 来通知应用程序。 从流读取数据的方式有很多种。

可写流可读流都通过多种方式使用 EventEmitter API 来通讯流的当前状态。

Duplex 流和 Transform 流都是可写又可读的。

对于只需写入数据到流或从流消费数据的应用程序,并不需要直接实现流的接口,通常也不需要调用 require('stream')

《1》可写流

可写流是对数据要被写入的目的地的一种抽象。

可写流的例子包括:

上面的一些例子事实上是实现了可写流接口的 Duplex 流。

所有可写流都实现了 stream.Writable 类定义的接口。

尽管可写流的具体实例可能略有差别,但所有的可写流都遵循同一基本的使用模式,如以下例子所示:

const myStream = getWritableStreamSomehow();
myStream.write('一些数据');
myStream.write('更多数据');
myStream.end('完成写入数据');//说明完成写入

 

stream.Writable 类

下面介绍几类事件:

'close' 事件

当流或其底层资源(比如文件描述符)被关闭时触发。 表明不会再触发其他事件,也不会再发生操作。

不是所有可写流都会触发 'close' 事件。

'drain' 事件

如果调用 stream.write(chunk) 返回 false,可能缓冲区已满,需要等待,则当有空间可以继续写入数据到流时会触发 'drain' 事件。

// 向可写流中写入数据一百万次。
// 留意背压(back-pressure)。
function writeOneMillionTimes(writer, data, encoding, callback) {
  let i = 1000000;
  write();
  function write() {
    let ok = true;
    do {
      i--;
      if (i === 0) {
        // 最后一次写入。
        writer.write(data, encoding, callback);
      } else {
        // 检查是否可以继续写入。 
        // 不要传入回调,因为写入还没有结束。
        ok = writer.write(data, encoding);
      }
    } while (i > 0 && ok);
    if (i > 0) {
      // 被提前中止。
      // 当触发 'drain' 事件时继续写入,继续运行write()函数。
      writer.once('drain', write);
    }
  }
}
'error' 事件

当写入数据发生错误时触发。

当触发 'error' 事件时,流还未被关闭

'finish' 事件

调用 stream.end() 且缓冲数据都已传给底层系统之后触发。

const http = require('http');

const server = http.createServer((req, res) => {
  // req 是一个 http.IncomingMessage 实例,它是可读流。
  // res 是一个 http.ServerResponse 实例,它是可写流。

  let body = '';
  // 接收数据为 utf8 字符串,
  // 如果没有设置字符编码,则会接收到 Buffer 对象。
  req.setEncoding('utf8');

  // 如果添加了监听器,则可读流会触发 'data' 事件。
  req.on('data', (chunk) => {
    body += chunk;
  });

  // 'end' 事件表明整个请求体已被接收。 
  req.on('end', () => {
    try {
      const data = JSON.parse(body);
      // 响应信息给用户。
      res.write(typeof data);
      res.end();//会触发finish事件
      res.on('finish', () => {
          console.error('写入已完成');
      });
    } catch (er) {
      // json 解析失败。
      res.statusCode = 400;
      return res.end(`错误: ${er.message}`);
    }
  });
});

server.listen(1337);

运行结果:

'pipe' 事件

当在可读流上调用 stream.pipe() 时触发。

var assert = require('assert');
const writer = process.stdout;
const reader = process.stdin;
writer.on('pipe', (src) => {
  console.error('有数据正通过管道流入写入器');
  assert.equal(src,reader);//两者相等
  console.log(src);
});
reader.pipe(writer);

返回:

有数据正通过管道流入写入器
ReadStream {
  connecting: false,
  _hadError: false,
  _handle:
   TTY { owner: [Circular], onread: [Function: onread], reading: false },
  _parent: null,
  _host: null,
  _readableState:
   ReadableState {
     objectMode: false,//非对象模式
     highWaterMark: 0,
     buffer: BufferList { length: 0 },
     length: 0,
     pipes:
      WriteStream {
        connecting: false,
        _hadError: false,
        _handle: [TTY],
        _parent: null,
        _host: null,
        _readableState: [ReadableState],
        readable: false,
        _events: [Object],
        _eventsCount: 7,
        _maxListeners: undefined,
        _writableState: [WritableState],
        writable: true,
        allowHalfOpen: false,
        _sockname: null,
        _writev: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: null,
        _server: null,
        columns: 80,
        rows: 24,
        _type: 'tty',
        fd: 1,
        _isStdio: true,
        destroySoon: [Function: destroy],
        _destroy: [Function],
        [Symbol(asyncId)]: 2,
        [Symbol(lastWriteQueueSize)]: 0,
        [Symbol(timeout)]: null,
        [Symbol(kBytesRead)]: 0,
        [Symbol(kBytesWritten)]: 0 },
     pipesCount: 1,
     flowing: true,
     ended: false,
     endEmitted: false,
     reading: false,
     sync: false,
     needReadable: true,
     emittedReadable: false,
     readableListening: false,
     resumeScheduled: true,
     emitClose: false,
     destroyed: false,
     defaultEncoding: 'utf8',
     awaitDrain: 0,
     readingMore: false,
     decoder: null,
     encoding: null },
  readable: true,
  _events:
   { end: [ [Function: onReadableStreamEnd], [Function] ],
     pause: [Function],
     data: [Function: ondata] },
  _eventsCount: 3,
  _maxListeners: undefined,
  _writableState:
   WritableState {
     objectMode: false,
     highWaterMark: 0,
     finalCalled: false,
     needDrain: false,
     ending: false,
     ended: false,
     finished: false,
     destroyed: false,
     decodeStrings: false,
     defaultEncoding: 'utf8',
     length: 0,
     writing: false,
     corked: 0,
     sync: true,
     bufferProcessing: false,
     onwrite: [Function: bound onwrite],
     writecb: null,
     writelen: 0,
     bufferedRequest: null,
     lastBufferedRequest: null,
     pendingcb: 0,
     prefinished: false,
     errorEmitted: false,
     emitClose: false,
     bufferedRequestCount: 0,
     corkedRequestsFree:
      { next: null,
        entry: null,
        finish: [Function: bound onCorkedFinish] } },
  writable: false,
  allowHalfOpen: false,
  _sockname: null,
  _writev: null,
  _pendingData: null,
  _pendingEncoding: '',
  server: null,
  _server: null,
  isRaw: false,
  isTTY: true,
  fd: 0,
  [Symbol(asyncId)]: 5,
  [Symbol(lastWriteQueueSize)]: 0,
  [Symbol(timeout)]: null,
  [Symbol(kBytesRead)]: 0,
  [Symbol(kBytesWritten)]: 0 }
View Code
'unpipe' 事件

当在可读流上调用 stream.unpipe() 时触发。

当可读流通过管道流向可写流发生错误时,也会触发 'unpipe' 事件。

var assert = require('assert');
const writer = process.stdout;
const reader = process.stdin;
writer.on('pipe', (src) => {
  console.error('有数据正通过管道流入写入器');
  assert.equal(src,reader);
  // console.log(src);
});
writer.on('unpipe', (src) => {
  console.error('已移除可写流管道');
  assert.equal(src, reader);
});
reader.pipe(writer);//触发'pipe'事件
reader.unpipe(writer);//触发'unpipe'事件

返回:

userdeMacBook-Pro:stream-learning user$ node test.js
有数据正通过管道流入写入器
已移除可写流管道

下面是可使用的方法:

writable.write(chunk[, encoding][, callback])
  • chunk <string> | <Buffer> | <Uint8Array> | <any> 要写入的数据。  对于非对象模式的流chunk 必须是字符串、Buffer 或 Uint8Array。 对于对象模式的流,chunk 可以是任何 JavaScript 值,除了 null
  • encoding <string> 如果 chunk 是字符串,则指定字符编码。
  • callback <Function> 当数据块被输出到目标后的回调函数。
  • 返回: <boolean> 如果流需要等待 'drain' 事件触发才能继续写入更多数据,则返回 false,否则返回 true
//wfd文件描述符,一般通过fs.open中获取
//buffer,要取数据的缓存源
//0,从buffer的0位置开始取
//BUFFER_SIZE,每次取BUFFER_SIZE这么长的长度
//index,每次写入文件的index的位置
//bytesRead,真实写入的个数
fs.write(wfd,buffer,0,bytesRead,index,function(err,bytesWrite){

})

writable.write() 写入数据到流,并在数据被完全处理之后调用 callback。 如果发生错误,则 callback 可能被调用也可能不被调用。 为了可靠地检测错误,可以为 'error' 事件添加监听器。

在接收了 chunk 后,如果内部的缓冲小于创建流时配置的 highWaterMark,则返回 true 。 如果返回 false ,则应该停止向流写入数据,直到 'drain' 事件被触发。

当流还未被排空时,调用 write() 会缓冲 chunk,并返回 false。 一旦所有当前缓冲的数据块都被排空了(被操作系统接收并传输),则触发 'drain' 事件。 建议一旦 write() 返回 false,则不再写入任何数据块,直到 'drain' 事件被触发。 当流还未被排空时,也是可以调用 write(),Node.js 会缓冲所有被写入的数据块,直到达到最大内存占用,这时它会无条件中止。 甚至在它中止之前, 高内存占用将会导致垃圾回收器的性能变差和 RSS 变高(即使内存不再需要,通常也不会被释放回系统)。 如果远程的另一端没有读取数据,TCP 的 socket 可能永远也不会排空,所以写入到一个不会排空的 socket 可能会导致远程可利用的漏洞。 

对于 Transform, 写入数据到一个不会排空的流尤其成问题,因为 Transform 流默认会被暂停,直到它们被 pipe 或者添加了 'data' 或 'readable' 事件句柄。 

如果要被写入的数据可以根据需要生成或者取得,建议将逻辑封装为一个可读流并且使用 stream.pipe()。 如果要优先调用 write(),则可以使用 'drain' 事件来防止背压与避免内存问题:

var assert = require('assert');
const writer = process.stdout;
// const reader = process.stdin;

function write(data, cb) {
  if (!writer.write(data)) {
    writer.once('drain', cb);
  } else {
    process.nextTick(cb);
  }
}

// 在回调函数被执行后再进行其他的写入。
write('hello', () => {
  console.log('完成写入,可以进行更多的写入');
});

返回:

node test.js
hello完成写入,可以进行更多的写入

 

举一个例子说明write和drain:

参考https://blog.csdn.net/eeewwwddd/article/details/81042225

  • 如果文件不存在会创建,如果有内容会被清空
  • 读取到highWaterMark的时候就会输出
  • 第一次是真的写到文件 后面就是写入缓存区 再从缓存区里面去取
path:写入的文件的路径
option: 
highWaterMark:水位线,一次可写入缓存中的字节,一般默认是64k
flags:标识,写入文件要做的操作,默认是w
encoding:编码,默认为buffer
start:开始写入的索引位置
end:结束写入的索引位置(包括结束位置)
autoClose:写入完毕是否关闭,默认为true
let fs = require('fs')
let ws = fs.createWriteStream('./foo1.txt',{
  flags: 'w',
  encoding: 'utf8',
  start: 0,
  //write的highWaterMark只是用来触发是不是干了
  highWaterMark: 19 //写是默认16k,当这里设置的长度小于或者等于我一下子要写入的字符串长度时,会触发一次drain,也仅触发一次,然后将剩余部分的所有内容放入缓存,后面将不会再触发drain了
})
//返回boolean 每当write一次都会在ws中吃下一个馒头 当吃下的馒头数量达到highWaterMark时 就会返回false 吃不下了会把其余放入缓存 其余状态返回true
//write只能放string或者buffer
var flag = ws.write('today is a good day','utf8',()=>{
  console.log('write');
});
ws.on('drain', ()=>{
    console.log('drain');
});

返回:

node test.js
drain
write

如果改为highWaterMark: 20,大于输入内容,则不会触发drain

则返回:

node test.js
write
writable.end([chunk][, encoding][, callback])
  • chunk <string> | <Buffer> | <Uint8Array> | <any> 要写入的数据。 对于非对象模式的流chunk 必须是字符串、Buffer、或 Uint8Array。 对于对象模式的流, chunk 可以是任何 JavaScript 值,除了 null
  • encoding <string> 如果 chunk 是字符串,则指定字符编码。
  • callback <Function> 当流结束时的回调函数。
  • 返回: <this>

调用 writable.end() 表明已没有数据要被写入可写流。 可选的 chunk 和 encoding 参数可以在关闭流之前再写入一块数据。 如果传入了 callback 函数,则会做为监听器添加到 'finish' 事件。

调用 stream.end() 之后再调用 stream.write() 会导致错误

writable.cork()

强制把所有写入的数据都缓冲到内存中。 当调用 stream.uncork() 或 stream.end() 时,缓冲的数据才会被输出。

当写入大量小块数据到流时,内部缓冲可能失效,从而导致性能下降,writable.cork() 主要用于避免这种情况。 对于这种情况,实现了 writable._writev() 的流可以用更优的方式对写入的数据进行缓冲。

writable.uncork()

将调用 stream.cork() 后缓冲的所有数据输出到目标。

当使用 writable.cork() 和 writable.uncork() 来管理流的写入缓冲时,建议使用 process.nextTick() 来延迟调用 writable.uncork()。 通过这种方式,可以对单个 Node.js 事件循环中调用的所有 writable.write() 进行批处理。

扩展: process.nextTick()

process.nextTick(callback[, ...args])

  • callback <Function>
  • ...args <any> 调用 callback时传递给它的额外参数

process.nextTick()方法将 callback 添加到"next tick 队列"。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

这种方式不是setTimeout(fn, 0)的别名。它更加有效率。事件轮询随后的ticks 调用,会在任何I/O事件(包括定时器)之前运行。

举例:

console.log('start');
process.nextTick(() => {
  console.log('nextTick callback');
});
console.log('scheduled');
// Output:
// start
// scheduled
// nextTick callback

回到writable.uncork(),举例:

var assert = require('assert');
const writer = process.stdout;

writer.cork();
writer.write('一些 ');
writer.write('数据 ');
process.nextTick(() => writer.uncork());如果没有这一句,运行时没有输出结果的

返回:

node test.js
一些 数据

如果一个流上多次调用 writable.cork(),则必须调用同样次数的 writable.uncork() 才能输出缓冲的数据。

var assert = require('assert');
const writer = process.stdout;

writer.cork();
writer.write('一些 ');
writer.cork();
writer.write('数据 ');
process.nextTick(() => {
  writer.uncork();
  // 数据不会被输出,直到第二次调用 uncork()。
  writer.uncork();//注释掉这一句就不会有输出,正确输出为一些 数据 
});
writable.destroy([error])

销毁流,并触发 'error' 事件且传入 error 参数。 调用该方法后,可写流就结束了,之后再调用 write() 或 end() 都会导致 ERR_STREAM_DESTROYED 错误。 实现流时不应该重写这个方法,而是重写 writable._destroy()

writable.setDefaultEncoding(encoding)

可写流设置默认的 encoding

转自https://blog.csdn.net/eeewwwddd/article/details/81042225

let fs = require('fs')
let EventEmitter = require('events')
//只有第一次write的时候直接用_write写入文件 其余都是放到cache中 但是len超过了highWaterMark就会返回false告知需要drain 很占缓存
//从第一次的_write开始 回去一直通过clearBuffer递归_write写入文件 如果cache中没有了要写入的东西 会根据needDrain来判断是否触发干点
class WriteStream extends EventEmitter{
  constructor(path,options = {}){
    super()
    this.path = path
    this.highWaterMark = options.highWaterMark || 64*1024
    this.flags = options.flags || 'r'
    this.start = options.start || 0
    this.pos = this.start
    this.autoClose = options.autoClose || true
    this.mode = options.mode || 0o666
    //默认null就是buffer
    this.encoding = options.encoding || null

    //打开这个文件
    this.open()

    //写文件的时候需要哪些参数
    //第一次写入的时候 是给highWaterMark个馒头 他会硬着头皮写到文件中 之后才会把多余吃不下的放到缓存中
    this.writing = false
    //缓存数组
    this.cache = []
    this.callbackList = []
    //数组长度
    this.len = 0
    //是否触发drain事件
    this.needDrain = false
  }

  clearBuffer(){
    //取缓存中最上面的一个
    let buffer = this.cache.shift()
    if(buffer){
      //有buffer的情况下
      this._write(buffer.chunk,buffer.encoding,()=>this.clearBuffer(),buffer.callback)
    }else{
      //没有的话 先看看需不需要drain
      if(this.needDrain){
        //触发drain 并初始化所有状态
        this.writing = false
        this.needDrain = false
        this.callbackList.shift()()
        this.emit('drain')

      }
      this.callbackList.map(v=>{
        v()
      })
      this.callbackList.length = 0
    }
  }
  _write(chunk,encoding,clearBuffer,callback){
    //因为write方法是同步调用的 所以可能还没获取到fd
    if(typeof this.fd != 'number'){
      //直接在open的时间对象上注册一个一次性事件 当open被emit的时候会被调用
      return this.once('open',()=>this._write(chunk,encoding,clearBuffer,callback))
    }
    fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWrite)=>{
      this.pos += byteWrite
      //每次写完 相应减少内存中的数量
      this.len -= byteWrite
      if(callback) this.callbackList.push(callback)
      //第一次写完
      clearBuffer()

    })
  }

  //写入方法
  write(chunk,encoding=this.encoding,callback){
    //判断chunk必须是字符串或者buffer 为了统一都变成buffer
    chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding)
    //维护缓存的长度 3
    this.len += chunk.length
    let ret = this.len < this.highWaterMark
    if(!ret){
      //表示要触发drain事件
      this.needDrain = true
    }
    //正在写入的应该放到内存中
    if(this.writing){
      this.cache.push({
        chunk,
        encoding,
        callback
      })
    }else{
      //这里是第一次写的时候
      this.writing = true
      //专门实现写的方法
      this._write(chunk,encoding,()=>this.clearBuffer(),callback)
    }
    // console.log(ret)
    //能不能继续写了 false代表下次写的时候更占内存
    return ret
  }

  destory(){
    if(typeof this.fd != 'number'){
      return this.emit('close')
    }
    //如果文件被打开过 就关闭文件并且触发close事件
    fs.close(this.fd,()=>{
      this.emit('close')
    })
  }
  open(){
    //fd表示的就是当前this.path的这个文件,从3开始(number类型)
    fs.open(this.path,this.flags,(err,fd)=>{
      //有可能fd这个文件不存在 需要做处理
      if(err){
        //如果有自动关闭 则帮他销毁
        if(this.autoClose){
          //销毁(关闭文件,出发关闭文件事件)
          this.destory()
        }
        //如果有错误 就会触发error事件
        this.emit('error',err)
        return
      }
      //保存文件描述符
      this.fd = fd
      //当文件打开成功时触发open事件
      this.emit('open',this.fd)
    })
  }
}

自定义可写流

因为createWriteStream内部调用了WriteStream类,WriteStream又实现了Writable接口,WriteStream实现了_write()方法,所以我们通过自定义一个类继承stream模块的Writable,并在原型上自定义一个_write()就可以自定义自己的可写流

let { Writable } = require('stream');

class MyWrite extends Writable{
  _write(chunk,encoding,callback){
    //write()的第一个参数,写入的数据
    console.log(chunk);
    //这个callback,就相当于我们上面的clearBuffer方法,如果不执行callback就不会继续从缓存中取出写
    callback();
  }
}

let writer = new MyWrite();
writer.write('yes',()=>{
  console.log('ok');
});

返回:

node test.js
<Buffer 79 65 73>
ok

《2》可读流

可读流是对提供数据的来源的一种抽象。

可读流的例子包括:

所有可读流都实现了 stream.Readable 类定义的接口。

两种读取模式

可读流运作于两种模式之一:流动模式(flowing)或暂停模式(paused)。

  • 在流动模式中,数据自动从底层系统读取,并通过 EventEmitter 接口的事件尽可能快地被提供给应用程序。
  • 在暂停模式中,必须显式调用 stream.read() 读取数据块。

所有可读流开始于暂停模式,可以通过以下方式切换到流动模式:

可读流可以通过以下方式切换回暂停模式:

  • 如果没有管道目标,则调用 stream.pause()
  • 如果有管道目标,则移除所有管道目标。调用 stream.unpipe() 可以移除多个管道目标。

只有提供了消费或忽略数据的机制后,可读流才会产生数据。 如果消费的机制被禁用或移除,则可读流会停止产生数据。

为了向后兼容,移除 'data' 事件句柄不会自动地暂停流。 如果有管道目标,一旦目标变为 drain 状态并请求接收数据时,则调用 stream.pause() 也不能保证流会保持暂停模式。

如果可读流切换到流动模式,且没有可用的消费者来处理数据,则数据将会丢失。 例如,当调用 readable.resume() 时,没有监听 'data' 事件或 'data' 事件句柄已移除。

添加 'readable' 事件句柄会使流自动停止流动,并通过 readable.read() 消费数据。 如果 'readable' 事件句柄被移除,且存在 'data' 事件句柄,则流会再次开始流动。

三种状态

可读流的两种模式是对发生在可读流中更加复杂的内部状态管理的一种简化的抽象。

在任意时刻,可读流会处于以下三种状态之一:

  • readable.readableFlowing === null
  • readable.readableFlowing === false
  • readable.readableFlowing === true

 readable.readableFlowing 为 null 时,没有提供消费流数据的机制,所以流不会产生数据。 在这个状态下,监听 'data' 事件、调用 readable.pipe()、或调用 readable.resume() 都会使 readable.readableFlowing 切换到 true,可读流开始主动地产生数据并触发事件。

调用 readable.pause()readable.unpipe()、或接收到背压,则 readable.readableFlowing 会被设为 false暂时停止事件流动但不会停止数据的生成。 在这个状态下, 'data' 事件绑定监听器不会使 readable.readableFlowing 切换到 true

const { PassThrough, Writable } = require('stream');
const pass = new PassThrough();
const writable = new Writable();

pass.pipe(writable);
pass.unpipe(writable);
// readableFlowing 现在为 false。

pass.on('data', (chunk) => { console.log(chunk.toString()); });
pass.write('ok'); // 不会触发 'data' 事件。
pass.resume(); // 必须调用它才会触发 'data' 事件。如果注释掉它则不会返回结果ok

 readable.readableFlowing 为 false 时,数据可能会堆积在流的内部缓冲中。

选择一种接口风格

可读流的 API 贯穿了多个 Node.js 版本,且提供了多种方法来消费流数据。 ⚠️开发者通常应该选择其中一种方法来消费数据,不要在单个流使用多种方法来消费数据。 混合使用 on('data')on('readable')pipe() 或异步迭代器,会导致不明确的行为。

对于大多数用户,建议使用 readable.pipe(),因为它是消费流数据最简单的方式。 如果开发者需要精细地控制数据的传递与产生,可以使用 EventEmitterreadable.on('readable')/readable.read() 或 readable.pause()/readable.resume()

stream.Readable 类

下面是事件的介绍:

'error' 事件

当流因底层内部出错而不能产生数据、或推送无效的数据块时触发。

'close' 事件

当流或其底层资源(比如文件描述符)被关闭时触发。 表明不会再触发其他事件,也不会再发生操作。

不是所有可读流都会触发 'close' 事件。

'data' 事件
  • chunk <Buffer> | <string> | <any> 数据块。 对于非对象模式的流, chunk 可以是字符串或 Buffer。 对于对象模式的流,chunk 可以是任何 JavaScript 值,除了 null

当流将数据块传送给消费者后触发。 当调用 readable.pipe()readable.resume() 或绑定监听器到 'data' 事件时,流会转换到流动模式。 当调用 readable.read() 且有数据块返回时,也会触发 'data' 事件。

如果使用 readable.setEncoding() 为流指定了默认的字符编码,则监听器回调传入的数据为字符串,否则传入的数据为 Buffer

const fs = require('fs');
const rr = fs.createReadStream('data.txt');//hello data

rr.on('data', (chunk) => {//readable不行,报错TypeError: Cannot read property 'length' of undefined
  console.log(`接收到 ${chunk.length} 个字节的数据`); //chunk为undefined
}); 

返回:

node test.js
接收到 10 个字节的数据
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {//readable不行,会闪退???????
  console.log(`接收到 ${chunk.length} 个字节的数据`);
}); 

返回:

node test.js
今天天气好 //自己输入并回车,这个内容就会被process.stdin收到
接收到 6 个字节的数据

之前有试一个例子一直没有成功:

process.stdin.setEncoding('utf8');
// process.stdout.write("请输入用户名:");
process.stdin.on('data', (chunk) => {
  // var chunk = process.stdin.read();
  console.log(chunk);
  if (chunk !== null) {
    process.stdout.write(`data: ${chunk}`);
  }
});
 
process.stdin.on('end', () => {
  process.stdout.write('end');
});
'end' 事件

当流中没有数据可供消费时触发。

'end' 事件只有在数据被完全消费掉后才会触发。 要想触发该事件,可以将流转换到流动模式,或反复调用 stream.read() 直到数据被消费完。

'readable' 事件

当流中有数据可供读取时触发

当到达流数据尾部时, 'readable' 事件也会触发。触发顺序在 'end' 事件之前。

事实上, 'readable' 事件表明流有了新的动态:要么是有了新的数据,要么是到了流的尾部。 对于前者, stream.read() 将返回可用的数据。而对于后者, stream.read() 将返回 null。 例如,下面的例子中的 foo.txt 是一个空文件:

const fs = require('fs');
const rr = fs.createReadStream('foo.txt');
rr.on('readable', () => {
  console.log(`读取的数据: ${rr.read()}`);
});
rr.on('end', () => {
  console.log('结束');
});

返回:

node test.js
读取的数据: null
结束

有问题:

const fs = require('fs');
const rr = fs.createReadStream('data.txt');
  
rr.on('readable', function(){//不能是'data'事件,为什么,如果是data,返回只有null和end,明天好好查查这两者的对比
    var chunk = rr.read(); // 获取到输入的信息 
    console.log(chunk); 

    if(chunk === ''){  
        rr.emit('end'); // 触发end事件  
        return  
    }  
    if (chunk !== null) {  
        process.stdout.write('data: '+ chunk +'
');  
    }
    // rr.emit('end');
});  
  
rr.on('end', function() {  
    process.stdout.write('end'+'
');  //也输出了,只是被挡住了,加上+'
'就看出来了
});

返回:

node test.js
<Buffer 68 65 6c 6c 6f 20 64 61 74 61>
data: hello data
null
end

 上面标明的错误都是因为一开始没能弄清楚data和readable的区别,看了博客https://blog.csdn.net/eeewwwddd/article/details/81042225?utm_source=copy后终于明白


参数

path:读取的文件的路径

option:
highWaterMark:水位线,一次可读的字节,一般默认是64k
flags:标识,打开文件要做的操作,默认是r
encoding:编码,默认为buffer
start:开始读取的索引位置
end:结束读取的索引位置(包括结束位置)
autoClose:读取完毕是否关闭,默认为true

data与readable的区别:

  • readable和读流的data的区别就是,readable可以控制自己从缓存区读多少和控制读的次数,而data是每次读取都清空缓存,读多少输出多少
  • readable是暂停模式,data是流动模式;就是readable需要使用read()来读取数据,data则是从回调中就能够得到数据
const fs = require('fs');
//读取的时候默认读64k
let rs = fs.createReadStream('./data.txt',{//内容为hello data
  highWaterMark: 2,//一次读的字节 默认64k
  flags: 'r',      //标示 r为读 w为写
  autoClose: true, //默认读取完毕后自动关闭
  start: 0,
  end: 5,          //流是闭合区间包start,也包end 默认是读完
  encoding: 'utf8' //默认编码是buffer
});
rs.on('data',(data) => {
    console.log('data');
    console.log(data);
});
//因为上面的data事件把数据读了,清空缓存区。所以导致下面的readable读出为null
rs.on('readable',() => { console.log('readable'); console.log(rs.read()); });

返回:

node test.js
data
he
data
ll
data
o
readable
null

如果把'data'监听去掉,那么返回结果就是:

node test.js
readable
he
readable
ll
readable
o 
readable
null

举例说明readable的使用情况:

(1)

let rs = fs.createReadStream('./foo.txt', {//内容为 Today is a good day.i want to go out for fun.
  //每次读7个
  highWaterMark: 7,
  encoding: 'utf8'
})
//如果读流第一次全部读下来并且小于highWaterMark,就会再读一次(再触发一次readable事件)
rs.on('readable', () => {
  let result = rs.read(2);
  console.log(result)
})

返回:

node test.js
To
da

(2)

//如果rs.read()不加参数,一次性读完,会从缓存区再读一次,为null
rs.on('readable', () => {
  let result = rs.read();
  console.log(result)
})

返回:

node test.js
Today i
s a goo
d day.i
 want t
o go ou
t for f
un.
null

(3)

//如果readable每次都刚好读完(即rs.read()的参数刚好和highWaterMark相等),就会一直触发readable事件,如果最后不足他想喝的数,他就会先触发一次null,最后把剩下的喝完
rs.on('readable', () => {
  let result = rs.read();
  console.log(result)
})

返回:

node test.js
Today i
s a goo
d day.i
 want t
o go ou
t for f
null
un.

(4)

//一开始缓存区为0的时候也会默认调一次readable事件,将foo.txt内容清零
rs.on('readable', () => {
  let result = rs.read();
  console.log(result)
})

返回:

node test.js
null

实战:行读取器(平常我们的文件可能有回车、换行,此时如果要每次想读一行的数据,就得用到readable)

let EventEmitter = require('events')
//如果要将内容全部读出就用on('data'),精确读取就用on('readable')
class LineReader extends EventEmitter {
  constructor(path) {
    super()
    this.rs = fs.createReadStream(path)
    //回车符的十六进制
    let RETURN = 0x0d
    //换行符的十六进制
    let LINE = 0x0a
    let arr = []
    this.on('newListener', (type) => {//每次使用 on 监听事件时触发'newListener'事件
      if (type === 'newLine') {//自定义的一个事件'newLine',触发后就调用'readable',然后自行设定一次读取一行的操作
        this.rs.on('readable', () => {
          let char
          //每次读一个,当读完的时候会返回null,终止循环
          while (char = this.rs.read(1)) {//读到文件最后char = null
            switch (char[0]) {
              case RETURN:
                break;
              //Mac下只有换行符,windows下是回车符和换行符,需要根据不同的转换。因为我这里是Mac
              case LINE:
                //如果是换行符就把数组转换为字符串
                let r = Buffer.from(arr).toString('utf8')
                //把数组清空
                arr.length = 0
                //触发newLine事件,把得到的一行数据输出
                this.emit('newLine', r)
                break;
              default:
                //如果不是换行符,就放入数组中
                arr.push(char[0])
            }
          }
        })
      }
    })
    //以上只能取出换行符之前的代码,最后一行的后面没有换行符,所以需要特殊处理。当读流读完需要触发end事件时
    this.rs.on('end', () => {
      //取出最后一行数据,转成字符串
      let r = Buffer.from(arr).toString('utf8')
      arr.length = 0
      this.emit('newLine', r)
    })
  }
}

let lineReader = new LineReader('./foo.txt')
lineReader.on('newLine', function (data) {
  console.log('a line');
  console.log(data);
})

返回:

node test.js //可见一次是只读取一行的
a line
if the truth is :
a line
I
a line
Am
a line
A
a line
boy

一般是将整个文件读取完的:

const fs = require('fs');
const rr = fs.createReadStream('foo.txt',{encoding: 'utf8'});
rr.on('readable', () => {
  console.log('one time');
  console.log(rr.read());
});
rr.on('end', () => {
  console.log('结束');
});

返回:

node test.js
one time
if the truth is :
I
Am
A
boy
one time
null
结束

下面接着方法的介绍:

readable.destroy([error])

销毁流,并且触发error事件。然后,可读流将释放所有的内部资源。

开发者不应该覆盖这个方法,应该覆盖readable._destroy方法。

const fs = require('fs');
const rr = fs.createReadStream('foo.txt',{encoding: 'utf8'});
rr.on('open', function () {
    console.log('文件被打开');
});
rr.destroy('something wrong');//有参数则为出现的错误,会触发error事件
rr.on('data', function (data) {
    console.log('data');
    console.log(data);

});
rr.on('error', function (err) {
    console.log('error');
    console.log(err);
});
rr.on('close', function (err) {
    console.log('close');
});
rr.on('end', () => {
  console.log('end');
});

返回:

node test.js
文件被打开
error
something wrong

如果rr.destroy();参数为空,则不会触发error事件,而是触发close事件,那么返回为:

node test.js
文件被打开
close
readable.isPaused()

readable.isPaused() 方法返回可读流的当前操作状态。 该方法主要是在 readable.pipe() 方法的底层机制中用到。大多数情况下,没有必要直接使用该方法

readable.pause()
  • 返回: this

readable.pause() 方法将会使 flowing 模式的流停止触发 'data' 事件, 进而切出 flowing 模式。任何可用的数据都将保存在内部缓存中

readable.read([size])

readable.read()方法从内部缓冲区中抽出并返回一些数据。 如果没有可读的数据,返回null。readable.read()方法默认数据将作为“Buffer”对象返回 ,除非已经使用readable.setEncoding()方法设置编码或流运行在对象模式。

可选的size参数指定要读取的特定数量的字节。如果size字节不可读,将返回null除非流已经结束,在这种情况下所有保留在内部缓冲区的数据将被返回。

如果没有指定size参数,则内部缓冲区包含的所有数据将返回。

readable.read()方法只应该在暂停模式下的可读流上运行。在流模式下,readable.read()自动调用直到内部缓冲区的数据完全耗尽。

一般来说,建议开发人员避免使用'readable'事件和readable.read()方法,使用readable.pipe()'data'事件代替。

无论size参数的值是什么,对象模式中的可读流将始终返回调用readable.read(size)的单个项目。

注意:如果readable.read()方法返回一个数据块,那么一个'data'事件也将被发送。

注意:在已经被发出的'end'事件后调用stream.read([size])事件将返回null不会抛出运行时错误。

//fd文件描述符,一般通过fs.open中获取
//buffer是读取后的数据放入的缓存目标
//0,从buffer的0位置开始放入
//BUFFER_SIZE,每次放BUFFER_SIZE这么长的长度
//index,每次从文件的index的位置开始读
//bytesRead,真实读到的个数
fs.read(fd,buffer,0,BUFFER_SIZE,index,function(err,bytesRead){

})
readable.resume()
  • 返回: this

readable.resume() 方法会重新触发 'data' 事件, 将暂停模式切换到流动模式。

readable.resume() 方法可以用来充分使用流中的数据,而不用实际处理任何数据,如以下示例所示:

getReadableStreamSomehow()
  .resume()
  .on('end', () => {
    console.log('Reached the end, but did not read anything.');
  });
readable.setEncoding(encoding)
  • encoding <string> 要使用的编码
  • Returns: this

readble.setEncoding() 方法会为从可读流读入的数据设置字符编码

默认返回Buffer对象。设置编码会使得该流数据返回指定编码的字符串而不是Buffer对象。例如,调用readable.setEncoding('utf8')会使得输出数据作为UTF-8数据解析,并作为字符串返回。调用readable.setEncoding('hex')使得数据被编码成16进制字符串格式。

可读流会妥善处理多字节字符,如果仅仅直接从流中取出Buffer对象,很可能会导致错误解码。

举例说明上面的事件和方法的使用:

const fs = require('fs');
const rr = fs.createReadStream('foo.txt',{encoding: 'utf8'});
rr.on('open', function () {//1 先响应open
    console.log('文件被打开');
});
rr.on('data', function (data) {//2 
    console.log('data');
    console.log(rr.isPaused()); //false
    rr.pause();//3 改为暂停模式,不读取数据了
    console.log(rr.isPaused());//true
    console.log(data);

});
setTimeout(function () {//7 两秒后恢复成流动模式继续读取数据
    console.log('resume');
    console.log(rr.isPaused());//true
    rr.resume();
    console.log(rr.isPaused());//true,因为添加 'readable' 事件句柄会使流自动停止流动,并通过 readable.read() 消费数据。 如果 'readable' 事件句柄被移除,且存在 'data' 事件句柄,则流会再次开始流动
},1000);                       //注释掉readable后,结果就为false

rr.on('error', function (err) {
    console.log(err);
});
rr.on('readable', () => {//4 因为data将所有数据都读完并将缓存清空,所以readable只输出null
  console.log('readable');
  console.log(rr.read());
});
rr.on('close', function (err) {//6 关闭
    console.log('close');
});
rr.on('end', () => {//5 结束
  console.log('end');
});

返回:

node test.js
文件被打开
data
false
true
if the truth is :
I
Am
A
boy
readable
null
end
close
resume
true
true

注释掉readable返回:

node test.js
文件被打开
data
false
true
if the truth is :
I
Am
A
boy
resume
true
false
end
close
readable.pipe(destination[, options])

readable.pipe() 绑定一个 [Writable][] 到 readable 上, 将可写流自动切换到 flowing 模式并将所有数据传给绑定的 [Writable][]。数据流将被自动管理。这样,即使是可读流较快,目标可写流也不会超负荷(overwhelmed)。

下面例子将 readable 中的所有数据通过管道传递给名为 foo.txt 的文件:

const fs = require('fs');
const rr = fs.createReadStream('foo.txt',{encoding: 'utf8'});
rr.pipe(process.stdout);

返回:

node test.js
if the truth is :
I
Am
A
boy

可以在单个可读流上绑定多个可写流。

readable.pipe() 方法返回 目标流 的引用,这样就可以对流进行链式地管道操作:

const fs = require('fs');
const zlib = require('zlib');
const rr = fs.createReadStream('foo.txt',{encoding: 'utf8'});
const z = zlib.createGzip();
const w = fs.createWriteStream('foo.txt.gz');
rr.pipe(z).pipe(w);
//运行后,文件夹中果然出现了一个压缩文件

默认情况下,当源可读流(the source Readable stream)触发 'end' 事件时,目标流也会调用 stream.end() 方法从而结束写入。要禁用这一默认行为, end 选项应该指定为 false, 这将使目标流保持打开, 如下面例子所示:

const fs = require('fs');
const rr = fs.createReadStream('foo.txt',{encoding: 'utf8'});
const writer = fs.createWriteStream('foo2.txt');
rr.pipe(writer,{end:false});
rr.on('end', () => {
  console.log('end reader');
});
setTimeout(function(){
    writer.write('请输入num1的值:');
    writer.end();
},2000);

返回:

node test.js
end reader

foo2.txt文件中内容为:

if the truth is :
I
Am
A
boy请输入num1的值:

如果去掉{ end: false },则出错:

node test.js
end reader
events.js:167
      throw er; // Unhandled 'error' event
      ^

Error [ERR_STREAM_WRITE_AFTER_END]: write after end //这就是因为当源可读流触发 'end' 事件时,目标流也会调用 stream.end() 方法从而结束写入

这里有一点要警惕,如果可读流在处理时发生错误,目标可写流 不会 自动关闭。 如果发生错误,需要 手动 关闭所有流以避免内存泄漏。

注意:不管对 process.stderr 和 process.stdout 指定什么选项,它们都是直到 Node.js 进程退出才关闭。

readable.unpipe([destination])

readable.unpipe() 方法将之前通过stream.pipe()方法绑定的流分离

如果 destination 没有传入, 则所有绑定的流都会被分离.

如果传入 destination, 但它没有被pipe()绑定过,则该方法不作为.

const readable = getReadableStreamSomehow();
const writable = fs.createWriteStream('file.txt');
// All the data from readable goes into 'file.txt',
// but only for the first second
readable.pipe(writable);
setTimeout(() => {
  console.log('Stop writing to file.txt');
  readable.unpipe(writable);
  console.log('Manually close the file stream');
  writable.end();
}, 1000);

readable源码实现,转自https://blog.csdn.net/eeewwwddd/article/details/81042225

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.flags = options.flags || 'r'
    this.start = options.start || 0
    this.pos = this.start     //会随着读取的位置改变
    this.autoClose = options.autoClose || true
    this.end = options.end || null
    //默认null就是buffer
    this.encoding = options.encoding || null

    //参数的问题
    this.reading = false //非流动模式
    //创建个buffer用来存储每次读出来的数据
    this.buffers = []
    //缓存区长度
    this.len = 0
    //是否要触发readable事件
    this.emittedReadable = false
    //触发open获取文件的fd标识符
    this.open()
    //此方法默认同步调用 每次设置on监听事件时都会调用之前所有的newListener事件
    this.on('newListener',(type)=>{// 等待着他监听data事件
      if(type === 'readable'){
        //开始读取 客户已经监听的data事件
        this.read()
      }
    })
  }
  //readable真正的源码中的方法,计算出和n最接近的2的幂次数
  computeNewHighWaterMark(n) {
    n--;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    n++;
    return n;
  }
  read(n){
    //当读的数量大于水平线,会通过取2的幂次取比他大和最接近的数
    if(this.len < n){
      this.highWaterMark = this.computeNewHighWaterMark(n)
      //重新触发readbale的callback,所以第一次会触发null
      this.emittedReadable = true
      //重新读新的水位线
      this._read()
    }
    //真正读取到的
    let buffer = null
    //说明缓存里有这么多,取出来
    if(n>0 && n<=this.len){
      //定义一个buffer
      buffer = Buffer.alloc(n)
      let buf
      let flag = true
      let index = 0
      //[buffer<1,2,3,4>,buffer<1,2,3,4>,buffer<1,2,3,4>]
      //每次取出缓存前的第一个buffer
      while(flag && (buf = this.buffers.shift())){
        for(let i=0;i<buf.length;i++){
          //把取出的一个buffer中的数据放入新定义的buffer中
          buffer[index++] = buf[i]
          //当buffer的长度和n(参数)长度一样时,停止循环
          if(index === n){
            flag = false
            //维护缓存,因为可能缓存中的buffer长度大于n,当取出n的长度时,还会剩下其余的buffer,我们需要切割buf并且放到缓存数组之前
            this.len -= n
            let r = buf.slice(i+1)
            if(r.length){
              this.buffers.unshift(r)
            }
            break
          }
        }
      }
    }
    //如果缓存区没有东西,等会读完需要触发readable事件
    //这里会有一种状况,就是如果每次Readable读取的数量正好等于highWaterMark(流读取到缓存的长度),就会每次都等于0,每次都触发Readable事件,就会每次读,读到没有为止,最后还会触发一下null
    if(this.len === 0){
      this.emittedReadable = true
    }
    if(this.len < this.highWaterMark){
      //默认,一开始的时候开始读取
      if(!this.reading){
        this.reading = true
        //真正多读取操作
        this._read()
      }
    }
    return buffer&&buffer.toString()
  }
  _read(){
    if(typeof this.fd != 'number'){
      //等待着触发open事件后fd肯定拿到了 再去执行read方法
      return this.once('open',()=>{this._read()})
    }
    //先读这么多buffer
    let buffer = Buffer.alloc(this.highWaterMark)
    fs.read(this.fd,buffer,0,buffer.length,this.pos,(err,byteRead)=>{
      if(byteRead > 0){
        //当第一次读到数据后,改变reading的状态,如果触发read事件,可能还会在触发第二次_read
        this.reading = false
        //每次读到数据增加缓存取得长度
        this.len += byteRead
        //每次读取之后,会增加读取的文件的读取开始位置
        this.pos += byteRead
        //将读到的buffer放入缓存区buffers中
        this.buffers.push(buffer.slice(0,byteRead))
        //触发readable
        if(this.emittedReadable){
          this.emittedReadable = false
          //可以读取了,默认开始的时候杯子填满了
          this.emit('readable')
        }
      }else{
        //没读到就出发end事件
        this.emit('end')
      }
    })
  }
  destory(){
    if(typeof this.fd != 'number'){
      return this.emit('close')
    }
    //如果文件被打开过 就关闭文件并且触发close事件
    fs.close(this.fd,()=>{
      this.emit('close')
    })
  }
  open(){
    //fd表示的就是当前this.path的这个文件,从3开始(number类型)
    fs.open(this.path,this.flags,(err,fd)=>{
      //有可能fd这个文件不存在 需要做处理
      if(err){
        //如果有自动关闭 则帮他销毁
        if(this.autoClose){
          //销毁(关闭文件,触发关闭文件事件)
          this.destory()
        }
        //如果有错误 就会触发error事件
        this.emit('error',err)
        return
      }
      //保存文件描述符
      this.fd = fd
      //当文件打开成功时触发open事件
      this.emit('open',this.fd)
    })
  }
}

自定义可读流

因为createReadStream内部调用了ReadStream类,ReadStream又实现了Readable接口,ReadStream实现了_read()方法,所以我们通过自定义一个类继承stream模块的Readable,并在原型上自定义一个_read()就可以自定义自己的可读流

let { Readable } = require('stream');

class MyRead extends Readable{
  //流需要一个_read方法,方法中push什么,外面就接收什么
  _read(){
    //push方法就是上面_read方法中的push一样,把数据放入缓存区中
    this.push('100');
    //如果push了null就表示没有东西可读了,停止(如果不写,就会一直push上面的值,死循环)
    this.push(null);
  }
}

let reader = new MyRead({encoding:'utf8'});
reader.on('readable',() => {
    console.log(reader.read());
});

返回:

node test.js
100

pipe——管道 可以控制速率,因为读快写慢

let fs = require('fs')
//pipe方法叫管道 可以控制速率
let rs = fs.createReadStream('./foo.txt',{
  highWaterMark: 4
})
let ws = fs.createWriteStream('./foo1.txt',{
  highWaterMark: 1
})
//会监听rs的on('data')将读取到的数据,通过ws.write的方法写入文件
//调用写的一个方法 返回boolean类型
//如果返回false就调用rs的pause方法 暂停读取
//等待可写流 写入完毕在监听drain resume rs
rs.pipe(ws) //会控制速率 防止淹没可用内存
let fs = require('fs')
//这两个是上面自己写的ReadStream和WriteStream
let { Readable } = require('stream');

class MyRead extends Readable{
  //流需要一个_read方法,方法中push什么,外面就接收什么
  _read(){
    //push方法就是上面_read方法中的push一样,把数据放入缓存区中
    this.push('100');
    //如果push了null就表示没有东西可读了,停止(如果不写,就会一直push上面的值,死循环)
    this.push(null);
  }
}

let writer = fs.createWriteStream('./foo1.txt',{
  highWaterMark: 1
});

//如果用原来的读写,因为写比较耗时,所以会多读少写,耗内存
MyRead.prototype.pipe = function(dest){
  this.on('data',(data)=>{
    let flag = dest.write(data)
    //如果写入的时候嘴巴吃满了就不继续读了,暂停
    if(!flag){
      this.pause()
    }
  });
  //如果写的时候嘴巴里的吃完了,就会继续读
  dest.on('drain',()=>{
    this.resume()
  });
  this.on('end',()=>{
    this.destroy()//销毁ReadStream
    //清空缓存中的数据
    fs.fsync(1,()=>{//fs.fsync作用是同步磁盘缓存,1代表的是文件描述符,0,1,2 文件描述符代表标准输入设备(比如键盘),标准输出设备(显示器)和标准错误
      dest.destroy()//销毁WriteStream,之前dest设的是,但是报错process.stdout cannot be closed
    });
  });
}
var reader = new MyRead();
reader.pipe(writer);//结果就是将100写到了文件foo1.txt

上面的文件描述符处本来写的是dest.fd,但是报错:

TypeError [ERR_INVALID_ARG_TYPE]: The "fd" argument must be of type number. Received type object

查看writer的fd为null,不知原因,待查明???????????

stream.pipeline(...streams[, callback])

  • ...streams <Stream> 两个或多个要用管道连接的流
  • callback <Function> 一个回调函数,可以带有一个错误信息参数

该模块方法用于在多个流之间架设管道,可以自动传递错误和完成扫尾工作,并且可在管道架设完成时提供一个回调函数:

const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');

// 使用 pipeline API 轻松连接多个流
// 并在管道完成时获得通知

// 使用pipeline高效压缩一个可能很大的tar文件:

pipeline(
  fs.createReadStream('foo.txt'),
  zlib.createGzip(),
  fs.createWriteStream('foo.tar.gz'),//运行后成功压缩并返回 管道架设成功 信息
  (err) => {
    if (err) {
      console.error('管道架设失败', err);
    } else {
      console.log('管道架设成功');
    }
  }
);

pipeline API 也可做成承诺:

const util = require('util');
const stream = require('stream');
const fs = require('fs');
const zlib = require('zlib');
const pipeline = util.promisify(stream.pipeline);

async function run() {
  await pipeline(
    fs.createReadStream('foo.txt'),
    zlib.createGzip(),
    fs.createWriteStream('foo.tar.gz')////运行后成功压缩并返回 管道架设成功 信息
  );
  console.log('管道架设成功');
}

run().catch(console.error);

用于实现流的 API

其实就是覆写下面的这些方法来实现自己的流操作:

新的流类必须实现一个或多个特定的方法,根据所创建的流类型,如下图所示:

用例实现的方法
只读流 Readable _read
只写流 writable _write ,_writev_final
可读可写流 Duplex _read ,_write ,_writev_final
操作写数据,然后读结果 Transform _transform_flush_final

注意:实现流的代码里面不应该出现调用“public”方法的地方因为这些方法是给使用者使用的(流使用者部分的API所述)。这样做可能会导致使用流的应用程序代码产生不利的副作用。

const { Writable } = require('stream');

class MyWritable extends Writable {
  constructor(options) {
    super(options);
    // ...
  }
}

双工流

有了双工流,我们可以在同一个对象上同时实现可读和可写,就好像同时继承这两个接口。 重要的是双工流的可读性和可写性操作完全独立于彼此。这仅仅是将两个特性组合成一个对象。

let { Duplex } = require('stream')
//双工流,可读可写
class MyDuplex extends Duplex{
  _read(){
    this.push('hello Duplex')
    this.push(null)
  }
  _write(chunk,encoding,clearBuffer){
    console.log(chunk)
    clearBuffer()
  }
}

let myDuplex = new MyDuplex()
//process.stdin是node自带的process进程中的可读流,会监听命令行的输入
//process.stdout是node自带的process进程中的可写流,会监听并输出在命令行中
//所以这里的意思就是在命令行先输出hello,然后我们输入什么他就出来对应的buffer(先作为可读流出来)
process.stdin.pipe(myDuplex).pipe(process.stdout)

返回:

node test.js
hello Duplex

转换流

在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate()

转换流的输出是从输入中计算出来的。对于转换流,我们不必实现readwrite的方法,我们只需要实现一个transform方法,将两者结合起来。它有write方法的意思,我们也可以用它来push数据。

let { Transform } = require('stream');

class MyTransform extends Transform{
  _transform(chunk,encoding,callback){//5 myTransform2 push时则触发myTransform_transform
    console.log(chunk.toString().toUpperCase());//6 然后输出from MyTransform2的大写内容
    callback();
  }
}
let myTransform = new MyTransform();


class MyTransform2 extends Transform{
  _transform(chunk,encoding,callback){//2 触发myTransform2的_transform
    console.log(chunk.toString().toUpperCase());//3 输出input的大写内容INPUT
    this.push('from MyTransform2');//4 将from MyTransform2内容写入myTransform
    this.push(null);
    callback();
  }
}
let myTransform2 = new MyTransform2();

//此时myTransform2被作为可写流触发_transform,输出输入的大写字符后,会通过可读流push字符到下一个转换流中
//当写入的时候才会触发transform的值,此时才会push,所以后面的pipe拿到的chunk是前面的push的值
process.stdin.pipe(myTransform2).pipe(myTransform);

返回:

node test.js
input //1 输入回车
INPUT

FROM MYTRANSFORM2

总结

可读流
在 flowing 模式下, 可读流自动从系统底层读取数据,并通过 EventEmitter 接口的事件尽快将数据提供给应用。
在 paused 模式下,必须显式调用 stream.read() 方法来从流中读取数据片段。
所有初始工作模式为 paused 的 Readable 流,可以通过下面三种途径切换到 flowing 模式:

  • 监听 ‘data’ 事件
  • 调用 stream.resume() 方法
  • 调用 stream.pipe() 方法将数据发送到 Writable

可读流可以通过下面途径切换到 paused 模式:

  • 如果不存在管道目标(pipe destination),可以通过调用 stream.pause() 方法实现。
  • 如果存在管道目标,可以通过取消 ‘data’ 事件监听,并调用 stream.unpipe() 方法移除所有管道目标来实现。


可写流
需要知道只有在嘴真正的吃满了,并且等到把嘴里的和地上的馒头(缓存中的)都吃下了才会触发drain事件
第一次写入会直接写入文件中,后面会从缓存中一个个取


双工流
只是对可写可读流的一种应用,既可作为可读流,也能作为可写流,并且作为可读或者可写时是隔离的


转换流
一般转换流是边输入边输出的,而且一般只有触发了写入操作时才会进入_transform方法中。跟双工流的区别就是,他的可读可写是在一起的

原文地址:https://www.cnblogs.com/wanghui-garcia/p/9798158.html