node 中的stream

829 阅读5分钟

学习目标:

  1. 掌握基础的API;
  2. 文件流的使用和实现 (fs.createReadStream, fs.createWriteStream) 。
  3. nodejs 中的stream;

node 中如何读写文件:

常用api: writeFile, readFile;

let fs = require('fs');
let path = require('path');
// 读写文件
fs.writeFile(path.join(__dirname, 'test', '1.txt'), Date.now() + '\n', function () {
  // console.log('ok');
})
fs.readFile(path.join(__dirname,'test', '1.txt'), function (err,data) {
  // console.log(data.toString())
})

思考上述代码会存在什么问题?

在读取大文件的时候(比如4个G的视频), 如何使用readFile 将整个视频数据读到内存中,那么将会出现内存不够用的情况。(导致程序卡死)

那如何解决这个问题呢?

可以采用 文件流 (createReadStream,createWriteStream)

文件流的使用

通过文件流的形式,我们就可以操作大文件了。

var from = fs.createReadStream(path.join(__dirname, 'test', 'demo1.txt'));
var to = fs.createWriteStream(path.join(__dirname, 'test', 'demo3.txt'));
from.pipe(to);

为什么上面的代码会内存溢出呢?

上诉代码是一种语法糖,我们看看它原始的模样。(上面的代码和下面的代码是一个意思)

// 使用事件的方式操作读写流
var ws = fs.createWriteStream(path.join(__dirname, 'test', 'demo1.txt'),{
	highWaterMark: 3
});
var rs = fs.createReadStream(path.join(__dirname, 'test', 'demo2.txt'),{
	highWaterMark: 3
});
// 读到数据出发的事件
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();
});

api 功能讲解:

  1. 属性
    • highWaterMark // 水位线
  2. 方法
    • ws.write 将数据写到磁盘中。
    • rs.pause 暂停读取磁盘的内容到内存中。
    • rs.resume 继续读取磁盘的内容到内存中。
    • ws.end 写入内容已经结束。
  3. 事件
    • rs.on('data',hander) 每当从磁盘读取内容到内存中触发
    • rs.on('end',hander) 磁盘内容读取完成触发。
    • ws.on('drain',hander) 内存的中的数据都写入文件时触发。

原理图:

image.png

在现实生活中,如果漏斗满了,我们很容易想到先不给漏斗在添加水了。而文件流代码中的实现也如此。

原理:

一边读文件一边写文件,当读的文件在内存中占用太多时,则不在会读取内容到内存中。让内存中的内容先写入文件,写完之后在读。

根据上面的思路,我们可以根据一个fs的基础api实现如上功能:

let BUFFER_SIZE = 3;
let buf = Buffer.alloc(BUFFER_SIZE);
var readOffset = 0;
fs.open(path.join(__dirname, 'test', 'demo1.txt'), 'r', 0600, function (err, readFd) {
   fs.open(path.join(__dirname, 'test', 'demo2.txt'), 'w', (err, writeFd) => {
     function read() {
       fs.read(readFd, buf, 0, BUFFER_SIZE, null, (err, bytesRead) => {
         if (bytesRead>0){
            fs.write(writeFd, buf, 0, bytesRead, read);
         }else{
            fs.close(readFd,()=>{})
            fs.close(writeFd, () => {})
         }
       });
     }
     read()
   })
});

代码功能介绍

  1. buf 他是一个缓冲区的概念。
  2. fs.open 打开文件,获取文件标识符
  3. fs.close 关闭文件。
  4. fs.read 从磁盘读取数据
  5. fs.write 将数据写入磁盘

通过上述代码,我们也可以实现大文件读取。

缺点:

  1. 代码耦合性高。
  2. 文件回调嵌套很深,不利于代码阅读。

node 是如何解耦的?

采用发布订阅模式

node events 模块讲解

let EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// 只处理一次,避免无限循环。
myEmitter.on('newListener', (event, listener) => {
  if (event === 'data') {
    // 在前面插入一个新的监听器。
     console.log(22)
  }
});
myEmitter.on('open', function () {
  console.log("open");
});
myEmitter.on('data', function (data) {
  console.log(data);
});

myEmitter.once('aa',()=>{
  console.log(1)
})

myEmitter.emit('aa');
myEmitter.emit('aa');

api讲解:

  1. 事件
    1. newListener 每次注册事件的时候出发(on)。
    2. once 只出发一次。

通过发布订阅实现解耦:

ReadStream.js

let fs = require("fs");
let EventEmitter = require("events");
class ReadStream extends EventEmitter {
  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") {
        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"); });
  }
}

ReadStream.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", () => {dest.end();});
};
ReadStream.prototype.pause = function () {
  this.flowing = false;
};
ReadStream.prototype.resume = function () {
  this.flowing = true;
  this.read();
};

module.exports = ReadStream;

WriteStream.js

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;

上述就是通过发布订阅模式对于文件流的简单实现。

注意:理解了上述思路和代码,当你去调试node fs 文件流的源码时,还是会发现自己看不懂,因为他是基于node steam 流实现的。 **

node 中的stream(流)

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

此外,该模块还包括实用函数 stream.pipeline()stream.finished()stream.Readable.from()

继承四种基本流需要实现的方法: image.png node stream

实现一个自己的可读流,可写流

var stream = require('stream');
var util = require('util');
util.inherits(MyReadable, stream.Readable);
// 可读流
function MyReadable(options) {
  stream.Readable.call(this, options);
  this._index = 0;
  this.tem = 0;
}
MyReadable.prototype._read = function () {
  this.push('黄鹏a');
  this.push(null);
};
var myReadable = new MyReadable({
  highWaterMark: 2
});

// 可写流
util.inherits(MyWriter, stream.Writable);
function MyWriter(opt) {
  stream.Writable.call(this, opt);
}
MyWriter.prototype._write = function (chunk, encoding, callback) {
  stock.push(chunk.toString('utf8'));
  callback();
  
};
var myWriter = new MyWriter();
let stock = [];

通过steam模拟文件流

function eventMethods() {
  myReadable.on('data', function (data) {
    var flag = myWriter.write(data);
    if (!flag)
      myReadable.pause();
  });
  myReadable.on('end', function (data) {
    // console.log("读完了");
    myWriter.end();
  });

  // 缓冲区的文件,消耗完触发的事件
  myWriter.on('drain', function () {
    myReadable.resume();
  });
  myWriter.on('finish', () => {
    console.log(stock)
  })
}

pipe 方式

function pipeMethods() {
    myReadable.pipe(myWriter)
    myWriter.on('finish', () => {
      console.log(stock, 'finish')
    })
}

转换流的使用(将字符串转换成base64)

function tansformMethods() {


  const upperCase = new stream.Transform({
    transform(chunk, encoding, callback) {
      this.push(chunk.toString('base64'));
      callback();
    }
  });
  myReadable.pipe(upperCase).pipe(myWriter);
  myWriter.on('finish', (data) => {
    console.log(stock)
  })
}

扩展与思考

var http=require("http");
var fs=require("fs");
var server= http.createServer(function(req,res){
   // 告诉浏览器发送什么样的资源
  res.writeHead(200,{"Content-Type":'application/html'});
  fs.createReadStream('/index.html').pipe(res);
});

上述代码,就是用node启动一个http服务器,据观察api发现,其实req,res 也是一种stream , 以后在发开时,使用流的思维想res,req ,很多问题就可以解决了。