Node.js中的流(一、可读流的简单实现)

1,114 阅读8分钟

stream(流)

流(stream)在Node.js中是处理流数据的抽象接口(abstract interface)。 stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。

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

流可以是可读的、可写的,或是可读写的。所有的流都是 EventEmitter 的实例。

在Node.js开发中,理解流的工作方式,这点很重要,,几乎所有的Node.js应用,不管多么简单,都在某种程度上使用了流。这会让你在使用Node开发的过程中如虎添翼。

Node.js 中流的类型:

  1. stream.Readable - 可读流 (例:fs.createReadStream -用于在I/O上获取数据)
  2. stream.writable - 可写流 (例:fs.createWriteStream -用于在输出的目标写入数据)
  3. stream.Duplex - 双工流 (例:TCP sockets zlib streams crypto streams -一个可读可写的流,例如网络连接)
  4. stream.Transform - 变换流 (例:zlib streams crypto streams -是一种双工流他的输出与输入是通过某种方式关联的,在读写过程中可以修改与变换数据)

流中的数据有两种模式,二进制模式和对象模式

  1. 二进制模式, 每个分块都是buffer或者string对象。

  2. 对象模式, 流内部处理的是一系列普通对象。

    所有使用 Node.js API 创建的流对象都只能操作 strings 和 Buffer对象。但是,通过一些第三方流的实现,你依然能够处理其它类型的 JavaScript 值 (除了 null,它在流处理中有特殊意义)。 这些流被认为是工作在 “对象模式”(object mode)。 在创建流的实例时,可以通过 objectMode 选项使流的实例切换到对象模式。试图将已经存在的流切换到对象模式是不安全的。

缓冲

Writable 和 Readable 流都会将数据存储到内部的缓冲器(buffer)中。这些缓冲器可以通过相应的writable._writableState.getBuffer() 或 readable._readableState.buffer 来获取。

缓冲器的大小取决于传递给构造函数的highWaterMark选项。

默认的可读流Readable的highWaterMark的值为64*1024(64kb),可写流Writable 的highWaterMark的值为16*1024(16kb)。一般来说这个状态可以维持读写的相对平衡。

当可读流的实现调用 stream.push(chunk) 方法时,数据被放到缓冲器中。如果流的消费者 没有调用 stream.read() 方法, 这些数据会始终存在于内部队列中,直到被消费。

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

可写流通过反复调用 writable.write(chunk) 方法将数据放到缓冲器。 当内部可写缓冲器的总大小小于 highWaterMark 指定的阈值时, 调用 writable.write() 将返回true。 一旦内部缓冲器的大小达到或超过 highWaterMark ,调用 writable.write() 将返回 false 。

可读流的三种状态

在任意时刻,任意可读流的状态应该确切的处于下列三种状态之一:

  1. readable._readableState = null 初始状态

    若 readable._readableState.flowing 为 null,由于不存在数据消费者,可读流将不会产生数据。 在这个状态下,监听 'data' 事件,调用 readable.pipe() 方法,或者调用 readable.resume() 方法, readable._readableState.flowing 的值将会变为 true 。这时,随着数据生成,可读流开始频繁触发事件。

        let fs = require('fs');
        let rs = rs.createReadStream('./a.txt');
        rs.on('data',(data)=>{ // 监听data事件时,流的状态自动转为流动状态
            cosnole.log(rs._readableState.flowing) // true
        })
    
  2. readable._readableState = false 非流动状态

    调用 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背压”(back pressure), 将导致 readable._readableState.flowing 值变为 false。 这将暂停事件流,但不会暂停数据生成。 在这种情况下,为 'data' 事件设置监听函数不会导致 readable._readableState.flowing 变为 true。当 readable._readableState.flowing 值为 false 时, 数据可能堆积到流的内部缓存中。

      let fs = require('fs');
      let rs = fs.createReadStream('./a.txt');
      rs.on('data',(data)=>{ // 不被触发
          cosnole.log(rs._readableState.flowing) // null
      })
      rs.pause(); // 这将暂停事件流,但不会暂停数据生成
    
  3. readable._readableState = true 流动状态

readable

  • 'readable' 事件将在流中有数据可供读取时触发。在某些情况下,为 'readable' 事件添加回调将会导致一些数据被读取到内部缓存中。

    let rs = fs.createReadStream('./a.txt',{highWaterMark:3});
    rs.on('readable',()=>{ 
        console.log(rs._readableState.length)
        rs.read(5) 
        console.log(rs._readableState.length)
    })
    
    
  • 一些数据被读取到了内部缓存中,超过了highWaterMark指定的大小

  • 当到达流数据尾部时, 'readable' 事件也会触发。触发顺序在 'end' 事件之前。 事实上, 'readable' 事件表明流有了新的动态:要么是有了新的数据,要么是到了流的尾部。 对于前者, stream.read() 将返回可用的数据。而对于后者, stream.read() 将返回 null。

    let fs =require('fs');
    let rs = fs.createReadStream('./a.txt',{
      start:3,
      end:8,
      encoding:'utf8',
      highWaterMark:3
    });
    rs.on('readable',function () {
      console.log('readable');
      console.log('rs._readableState.buffer.length',rs._readableState.length);
      let d = rs.read(3);
      console.log('rs._readableState.buffer.length',rs._readableState.length);
      console.log(d);
      setTimeout(()=>{
          console.log('rs._readableState.buffer.length',rs._readableState.length);
      },500)
    });
    


可读流上的事件

  1. 监听readable事件

    'readable' 事件将在流中有数据可供读取时触发。在某些情况下,为 'readable' 事件添加回调将会导致一些数据被读取到内部缓存中。

    rs.on('readable',data=>{
        console.log('有一些数据可以读了');
    })
    
  2. 监听data事件

    'data' 事件会在流将数据传递给消费者时触发。当流转换到 flowing 模式时会触发该事件。调用 readable.pipe(), readable.resume() 方法,或为 'data' 事件添加回调可以将流转换到 flowing 模式。 'data' 事件也会在调用 readable.read() 方法并有数据返回时触发。

    在没有明确暂停的流上添加 'data' 事件监听会将流转换为 flowing 模式。 数据会在可用时尽快传递给下个流程。

    如果调用 readable.setEncoding() 方法明确为流指定了默认编码,回调函数将接收到一个字符串,否则接收到的数据将是一个 Buffer 实例。

    rs.on('data',data=>{
        console.log(data.toString());
    })
    
  3. 监听end事件(数据读取完毕)

    'end' 事件将在流中再没有数据可供消费时触发。

    注意: 'end' 事件只有在数据被完全消费后 才会触发 。 可以在数据被完全消费后,通过将流转换到 flowing 模式, 或反复调用 stream.read() 方法来实现这一点。

    rs.on('end',_=>{
        console.log('没有更多数据了');
    })
    
    
  4. 监听close事件

    'close' 事件将在流或其底层资源(比如一个文件)关闭后触发。'close' 事件触发后,该流将不会再触发任何事件

    rs.on('close',_=>{
        console.log("close");
    })
    
  5. 监听error事件

    'error' 事件可以在任何时候在可读流实现(Readable implementation)上触发。 通常,这会在底层系统内部出错从而不能产生数据,或当流的实现试图传递错误数据时发生。

    回调函数将接收到一个 Error 对象。

    rs.on('error',err=>{
        console.log(err);
    })
    

可读流 暂停模式的简单实现

引入Node.js模块

可读流需要进行文件读取操作,并且,每个流都是EventEmitter的实例

let fs = require('fs');
let EventEmitter = require('events');

构造方法

class ReadStream extends EventRmmiter {
    constructor(path,options = {}){
        super();
        this.path = path;
        this.flags = options.flags || 'r';
        this.autoClose = options.autoCLose === undefined ? true : options.autoCLose; // 是否自动关闭
        this.encoding = options.encoding || null; // 编码格式,默认为buffer
        this.highWaterMark = options.highWaterMark || 64 * 1024; // 文件一次读多少字节
        this.buffers = []; // 缓存区
        this.len = 0; // 用来维护当前缓存区buffer的总长度
        this.reading = false;// 如果正在读取时,就不要再去读取了
        this.start = options.start || 0; // 
        this.end = options.end || null;
        this.pos = this.start; // 读取的偏移位置
        this.on('newListener', (type) => {
            if (type === 'readable') { // 看受否监听了readable事件
                this.read(); // 开始读取,读highWaterMark这么多
            }
        })
        this.open(); // 创建时,自动调用open方法,调用时还没有拿到fd
    }
}

文件打开方法

open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        this.emit('error', err);
        if (this.autoClose) { // 当自动关闭为true时,发生错误,销毁该可读流
          this.destroy();
        }
        return;
      }
      this.fd = fd;
      this.emit('open', this.fd);
    })
}

销毁方法

当文件还未打开时,直接触发colse事件
当文件已经打开,文件描述符fd有值时,关闭文件,然后触发close事件
destroy() {
    if (typeof this.fd !== 'number') { 
      return this.emit('close');
    }
    fs.close(this.fd, () => { 
      this.emit('close');
    })
}

读取方法

当读取到的数据在缓存区超过原highWaterMark,根据规则重新计算设置highWaterMark
read(n) { // n表示需要读多少个
    // 如果缓存区没有东西等会读完内容后需要触发readable事件
    if(n>this.len){ // 更改水位线后重新触发read事件
      this.highWaterMark = computeNewHighWaterMark(n);
      this.emittedReadable = true; 
      this._read();
    }
    let buffer; // buffer就是读取到的内容
    if (n > 0 && n <= this.len) { // 说明缓存里有这么多,在缓存里取出来
      // [buffer<5,6,7,8>,buffer<9,10,11,12>]
      buffer = Buffer.alloc(n); // 读取的结果
      let buf;
      let index = 0;
      let flag = true;
      while (flag && (buf = this.buffers.shift())) {
        for (let i = 0; i < buf.length; i++) {
          buffer[index++] = buf[i];
          if (index === n) {
            flag = false;
            this.len -= n; // 维护缓存;
            let r = buf.slice(i + 1);
            if (r.length) {
              this.buffers.unshift(r); // 将剩余没有消耗的在塞回到数组中
            }
            break;
          }
        }
      }
    }
    if (this.len === 0) {
      this.emittedReadable = true;
    }
    if (this.len < this.highWaterMark) { // 读取内容
      if (!this.reading) { // 默认不是正在读取时才能读取
        this.reading = true;
        this._read(); // 开始读取
      }
    }
    return buffer;
}

新水位线计算函数

function computeNewHighWaterMark(n) {
  n--;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  n++;
  return n;
}

真实读取文件的方法

_read() {
    if (typeof this.fd !== 'number') {
      return this.once('open', () => this._read());
    }
    let buffer = Buffer.alloc(this.highWaterMark); // 每次读highWaterMark这么多
    fs.read(this.fd, buffer, 0, buffer.length, this.pos, (err, byteRead) => {
      this.reading = false; // 不是正在读取了
      if (byteRead > 0) {
        this.len += byteRead; // 维护缓存的长度
        this.pos += byteRead;
        this.buffers.push(buffer.slice(0, byteRead)); // 将读取到的buffer放到缓存区中
        if (this.emittedReadable) {
          this.emittedReadable = false; //默认下一次不触发readable事件
          this.emit('readable'); // 可以读取了,默认杯子填满了
        }
      } else {
        this.emit('end'); // 当读取不到内容时触发end事件
      }
    });
}

到这里就结束了,如果你有更好的思路和想法,欢迎分享哦!

如果觉得还可以,请点赞鼓励一下,谢谢!