学习笔记-nodejs之stream

一:stream

stream是nodejs中处理流式数据的抽象接口,stream模块为实现了stream接口的对象提供了基础的api。在nodejs中有很多流失对象,例如http请求、process.stdout等。流可以分为可读型、可写型、可读可写型,所有的流式对象都继承了EventEmiter,都是EventEmiter类的实例。在代码中引入流stream模块:

const stream=require('stream')

四种基本的流类型

  • Readable:可读的流 (例如 fs.createReadStream()、request)
  • Writeable: 可写的流(例如 fs.createWriteStream()、response)
  • Duplex:双工流,可读可写,例如socket
    Transform:在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate()).

缓存

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

  • 缓冲器的大小取决于传递给流构造函数的 highWaterMark 选项。 对于普通的流, highWaterMark 选项指定了总共的字节数。对于工作在对象模式的流, highWaterMark 指定了对象的总数。

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

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

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

  • stream API 的关键目标, 尤其对于 stream.pipe() 方法, 就是限制缓冲器数据大小,以达到可接受的程度。这样,对于读写速度不匹配的源头和目标,就不会超出可用的内存大小。

  • Duplex 和 Transform 都是可读写的。 在内部,它们都维护了 两个 相互独立的缓冲器用于读和写。 在维持了合理高效的数据流的同时,也使得对于读和写可以独立进行而互不影响。

二:可读流 Readable

nodejs中的可读流有:

  • client端的http responses
  • server端的http requests
  • fs 中的可读 streams
  • zlib 模块的 streams
  • crypto 模块的 streams
  • tcp sockets
  • child process 模块的 stdout 和 stderr
  • process.stdin

1创建可读流

var rs = fs.createReadStream(path,[options]);

  1. path读取文件的路径
  2. options
    • flags打开文件要做的操作,默认为’r’
    • encoding默认为null
    • start开始读取的索引位置
    • end结束读取的索引位置(包括结束位置)
    • highWaterMark读取缓存区默认的大小64kb

2data事件:数据来了

数据一块一块的来了

3pasuse事件: 暂停读取

缓冲区大小限制加上读写速不一致,有时可能需要暂停读取

4resume方法:重启读取任务

继续读取

5end事件:接收完毕

所有流数据已被接收完毕,回调方法里可以写业务逻辑了,该事件会在读完数据后被触发

6close事件

7error事件

8可读流简易实现

  • 实现 readable.js
const fs = require('fs')
const EventEmiter = require('events')

class ReadAble extends EventEmiter {
  constructor(path, options) {
    super()
    this.path = path //要读的文件的路径
    this.highWaterMark = options.highWaterMark || 64 * 1024
    this.flags = options.flags || 'r';
    this.encoding = options.encoding || null
    this.buffer = Buffer.alloc(this.highWaterMark) // 缓存池
    this.flowing = null // 流的状态

    this.start = 0;//初始化开始位置
    this.fd = -1 //文件打开句柄
    //开始读取文件
    this.open(() => {
      // 监测外部的读取事件
      this.on('newListener', (event, listener) => {
        //当外部监听data事件时就开始读取文件
        if (event === 'data') {
          this.flowing = true
          this.read()
        }
      })
    })
  }

  open(cb) {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        this.emit('error', err)
        return
      }
      else {
        this.emit('open') //向外发射open事件,通知文件已被打开
        this.fd = fd
        cb()
      }
    })
  }

  read() {
    let offset = 0
    let length = this.highWaterMark //每次读取highWaterMark字节
    fs.read(this.fd, this.buffer, offset, length, this.start, (error, bytesRead, buffer) => {
      if (error) {
        this.emit('error', error)
        this.destory()
        return
      }
      else {
        if (bytesRead) {
          this.start += bytesRead; // 下一次读取的起始位置

          let data = this.buffer
          if (this.encoding) {
            data = this.buffer.toString(this.encoding)
          }
          this.emit('data', data)
          if (this.flowing) {
            this.read()
          }
        }
        else {
          this.emit('end')
          this.destory()
        }
      }
    })
  }

  resume() {
    console.log('重启');
    this.flowing = true
    this.read()
  }

  pause() {
    console.log('暂停');
    this.flowing = false
  }

  destory() {
    if (this.fd != -1) {
      fs.closeSync(this.fd)
    }
    this.emit('close')
  }

  /**
   * @parmas: ws 可写流
   * @Description: 实现管道
   */
  pipe(ws){
    this.on('data',chunk=>{
      let full=ws.write(chunk)
      if (!full) {
        this.pause()
      }
      ws.on('drain',_=>{
        this.resume()
      })
    })
  }
}

module.exports = ReadAble

  • 调用
const ReadAble = require('./readable.js')
const filepath = './tmp.txt'
const filepath2 = './tmp2.txt'
const options = {
  highWaterMark: 3,
  flags: 'r',
  encoding: 'utf8'
}

var endFlag = false

const rs = new ReadAble(filepath, options)

rs.on('data', chunk => {
  // console.log('--------------------------')
  console.log(chunk);
})

rs.on('end', _ => {
  endFlag=true
  console.log('读取完毕')
})

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

const fs=require('fs')
var ws=fs.createWriteStream(filepath2,{
  flags:'w+'
})
// 管道写入ws
rs.pipe(ws)

// 用于测试暂停重启
var tmer1 = setInterval(() => {
  if (!endFlag) {
    rs.pause()
    var timer2 = setTimeout(() => {
      rs.resume()
    }, 150);
  }
  else {
    clearInterval(tmer1)
    clearTimeout(timer2)
  }

}, 300);

  • 实验数据 tmp.txt
123456789

  • 实验结果
暂停
重启
123
456
789
读取完毕

三:可写流 Writeable

nodejs中的可写流有:

  • 客户端http requests
  • 服务器端的http responses
  • fs模块的写入流
  • zlib streams
  • crypto streams
  • tcp sockets
  • child process模块的stdin
  • process.stdout, process.stderr

1创建可写流

var ws = fs.createWriteStream(path,[options]);

2wirte()方法

3drain()方法

  • 当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false。
  • 一旦所有当前所有缓存的数据块都排空了(被操作系统接受来进行输出), 那么 ‘drain’ 事件就会被触发
    建议, 一旦 write() 返回 false, 在 ‘drain’ 事件触发前, 不能写入任何数据块

4end()方法

表明接下来没有数据要被写入 Writable 通过传入可选的 chunk 和 encoding 参数,可以在关闭流之前再写入一段数据 如果传入了可选的 callback 函数,它将作为 ‘finish’ 事件的回调函数

5finish()方法

在调用了 stream.end() 方法,且缓冲区数据都已经传给底层系统之后, ‘finish’ 事件将被触发。

四:可写流与可读流使用实例

此示例演示了使用可写流与可读流实现复制文件的功能,涉及到的api有readable的data事件、end事件、pause方法、resume方法,writeable的drain事件、write方法、end方法,读写流都设置了highWaterMark可以直观的观察各事件的触发时机

var fs = require('fs'),
  path = require('path'),
  out = process.stdout;

var filePath = '/Users/hyb/Downloads/node-v10.14.1.pkg';

var readStream = fs.createReadStream(filePath, {
  highWaterMark: 16 //默认64k
});
var writeStream = fs.createWriteStream('./node.pkg', {
  highWaterMark: 16 //默认16k
});

var stat = fs.statSync(filePath);

var totalSize = stat.size;
var passedLength = 0;
var lastSize = 0;
var startTime = Date.now();

readStream.on('data', function (chunk) {
  passedLength += chunk.length;
  // 写入的缓存区已满 达到writeable的highWaterMark
  if (writeStream.write(chunk) === false) {
    out.write(`writeStream highWaterMark;readStream.pause ${Date.now()} \n`)
    readStream.pause();
  }
});

readStream.on('end', function () {
  writeStream.end();
});

writeStream.on('drain', function () {
  out.write(`writeStream drain;readStream.resume' ${Date.now()}\n`)
  readStream.resume();
});

setTimeout(function show() {
  var percent = Math.ceil((passedLength / totalSize) * 100);
  var size = Math.ceil(passedLength / 1000000);
  var diff = size - lastSize;
  lastSize = size;
  out.clearLine();
  out.cursorTo(0);
  out.write('已完成' + size + 'MB, ' + percent + '%, 速度:' + diff * 2 + 'MB/s');
  if (passedLength < totalSize) {
    setTimeout(show, 500);
  } else {
    var endTime = Date.now();
    console.log();
    console.log('共用时:' + (endTime - startTime) / 1000 + '秒。');
  }
}, 500);

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值