如何在Stream.js中使用stream.Writable的drain事件

时间:2013-09-21 12:17:17

标签: javascript node.js stream

在Node.js中,我使用fs.createWriteStream方法将数据附加到本地文件。在Node文档中,他们在使用drain时提到fs.createWriteStream事件,但我不明白。

var stream = fs.createWriteStream('fileName.txt');
var result = stream.write(data);

在上面的代码中,我如何使用排水事件?事件是否在下面正确使用?

var data = 'this is my data';
if (!streamExists) {
  var stream = fs.createWriteStream('fileName.txt');
}

var result = stream.write(data);
if (!result) {
  stream.once('drain', function() {
    stream.write(data);
  });
}

4 个答案:

答案 0 :(得分:21)

drain事件用于清空可写流的内部缓冲区。

只有当内部缓冲区的大小超过其highWaterMark属性时,才会发生这种情况,该属性是可写入流的内部缓冲区内可以存储的数据的最大字节数,直到它停止从数据源读取为止。

这种情况的原因可能是由于设置涉及从一个流中读取数据源的速度快于可以写入另一个资源的数据源。例如,采取两个流:

var fs = require('fs');

var read = fs.createReadStream('./read');
var write = fs.createWriteStream('./write');

现在假设文件read位于SSD上,可以500MB / s读取,而write位于只能写入150MB/s的HDD上。写入流将无法跟上,并将开始将数据存储在内部缓冲区中。一旦缓冲区达到highWaterMark(默认为16KB),写入将开始返回false,并且流将在内部排队。一旦内部缓冲区的长度为0,就会触发drain事件。

这就是排水工作的方式:

if (state.length === 0 && state.needDrain) {
  state.needDrain = false;
  stream.emit('drain');
}

这些是排水的先决条件,它是writeOrBuffer功能的一部分:

var ret = state.length < state.highWaterMark;
state.needDrain = !ret;

要了解如何使用drain事件,请参阅Node.js文档中的示例。

function writeOneMillionTimes(writer, data, encoding, callback) {
  var i = 1000000;
  write();
  function write() {
    var ok = true;
    do {
      i -= 1;
      if (i === 0) {
        // last time!
        writer.write(data, encoding, callback);
      } else {
        // see if we should continue, or wait
        // don't pass the callback, because we're not done yet.
        ok = writer.write(data, encoding);
      }
    } while (i > 0 && ok);
    if (i > 0) {
      // had to stop early!
      // write some more once it drains
      writer.once('drain', write);
    }
  }
}

该函数的目标是向可写流写入1,000,000次。变量ok设置为true,循环仅在ok为真时执行。对于每个循环迭代,ok的值设置为stream.write()的值,如果需要drain,则返回false。如果ok变为false,那么drain的事件处理程序将等待,并且着火后将恢复写入。


特别是关于您的代码,您不需要使用drain事件,因为您在打开流后只编写了一次。由于您尚未向流中写入任何内容,因此内部缓冲区为空,您必须以块的形式写入至少16KB才能触发drain事件。 drain事件用于写入多次,其数据多于可写流的highWaterMark设置。

答案 1 :(得分:8)

想象一下,您正在连接具有不同带宽的2个流,例如,将本地文件上载到慢速服务器。 (快速)文件流将比(慢)套接字流消耗它更快地发出数据。

在这种情况下,node.js会将数据保存在内存中,直到慢流有机会处理它。如果文件非常大,这可能会出现问题。

为避免这种情况,Stream.write在底层系统缓冲区已满时返回false。如果您停止写入,流将稍后发出drain事件以指示系统缓冲区已清空,并且适合再次写入。

您可以使用pause/resume可读流并控制可读流的带宽。

更好:您可以使用readable.pipe(writable)来为您执行此操作。

编辑:您的代码中存在一个错误:无论write返回什么,您的数据都已写入。您无需重试它。在您的情况下,您要写data两次。

这样的事情会起作用:

var packets = […],
    current = -1;

function niceWrite() {
  current += 1;

  if (current === packets.length)
    return stream.end();

  var nextPacket = packets[current],
      canContinue = stream.write(nextPacket);

  // wait until stream drains to continue
  if (!canContinue)
    stream.once('drain', niceWrite);
  else
    niceWrite();
}

答案 2 :(得分:0)

这是带有异步/等待的版本

const write = (writer, data) => {
  return new Promise((resolve) => {
    if (!writer.write(data)) {
      writer.once('drain', resolve)
    }
    else {
      resolve()
    }
  })
}

// usage
const run = async () => {
  const write_stream = fs.createWriteStream('...')
  const max = 1000000
  let current = 0
  while (current <= max) {
    await write(write_stream, current++)
  }
}

https://gist.github.com/stevenkaspar/509f792cbf1194f9fb05e7d60a1fbc73

答案 3 :(得分:0)

这是使用Promises(异步/等待)的速度优化版本。调用方必须检查是否返回了promise,只有在这种情况下才必须调用await。每次通话都需要等待,这会使程序减慢3倍...

const write = (writer, data) => {
    // return a promise only when we get a drain
    if (!writer.write(data)) {
        return new Promise((resolve) => {
            writer.once('drain', resolve)
        })
    }
}

// usage
const run = async () => {
    const write_stream = fs.createWriteStream('...')
    const max = 1000000
    let current = 0
    while (current <= max) {
        const promise = write(write_stream, current++)
        // since drain happens rarely, awaiting each write call is really slow.
        if (promise) {
            // we got a drain event, therefore we wait
            await promise
        }
    }
}