Node.js Stream流详解:可读与可写流的实现及用法

作者:栖木

时间:2026-10-24

浏览:134

本文深入解析Node.js中Stream流的核心机制,涵盖Readable与Writable流的创建与监听。详细讲解流动模式与暂停模式的切换方法,以及cork/uncork性能优化技巧。通过实例演示如何利用pipe方法连接流,实现高效的数据传输与处理。

文章目录

Node.js Stream流概述与类型

本文通过具体实例,详细解析Node.js中Stream流机制下可读流与可写流的实现原理及使用方法,旨在为开发者提供清晰的参考指南。

在Node.js环境中,Stream(流)是处理流式数据的核心抽象接口。系统内置了多种流对象,例如HTTP模块中的request和response对象,以及process.stdout,它们均为Stream的具体实例。

根据数据流向的不同,流可分为可读(Readable)、可写(Writable)或双向(Duplex)类型。值得注意的是,所有的流对象均继承自events模块,因此它们都是EventEmitter的实例,支持事件监听机制。

一、流的类型

Node.js主要提供了四种基础流类型,分别适用于不同的数据处理场景:

1、Writable(可写流):典型实现为fs.createWriteStream(),用于将数据写入目标资源。

2、Readable(可读流):典型实现为fs.createReadStream(),用于从源资源读取数据。

3、Duplex(双向流):同时具备可读和可写功能,例如net.Socket网络套接字。

4、Transform(转换流):一种特殊的Duplex流,可在读写过程中对数据进行修改或转换,例如zlib.createDeflate()用于数据压缩。

流的数据模式与可读流控制

二、流中的数据有两种模式

1、二进制模式:在此模式下,流处理的数据单元为字符串(string)或Buffer对象。

2、对象模式:在此模式下,流内部处理的是普通的JavaScript对象,适用于需要传递复杂数据结构而非原始字节的场景。

三、可读流的两种模式

1、流动模式(flowing):在此模式下,系统会自动从底层读取数据,并通过事件机制尽可能快地将数据推送给应用程序。

2、暂停模式(paused):在此模式下,数据不会自动推送,必须显式调用read()方法才能从缓冲区中读取数据。

所有可读流在初始化时默认处于暂停模式。开发者可以通过以下几种方式将其切换至流动模式:

1、为流添加'data'事件监听回调。

2、直接调用resume()方法。

3、调用pipe()方法将流数据管道传输至另一个可写流。

若需将可读流从流动模式切换回暂停模式,可采用以下方法:

1、若当前没有配置管道目标(pipe target),直接调用pause()方法。

2、若已配置管道目标,需先移除所有管道目标,或调用unpipe()方法移除特定的管道目标。

可读流的事件监听与模式切换

四、创建可读流,并监听事件


const fs = require('fs');
//创建一个文件可读流
let rs = fs.createReadStream('./1.txt', {
  //文件系统标志
  flags: 'r',
  //数据编码,如果调置了该参数,则读取的数据会自动解析
  //如果没调置,则读取的数据会是 Buffer
  //也可以通过 rs.setEncoding() 进行设置
  encoding: 'utf8',
  //文件描述符,默认为null
  fd: null,
  //文件权限
  mode: 0o666,
  //文件读取的开始位置
  start: 0,
  //文件读取的结束位置(包括结束位置)
  end: Infinity,
  //读取缓冲区的大小,默认64K
  highWaterMark: 3
});
//文件被打开时触发
rs.on('open', function () {
  console.log('文件打开');
});
//监听data事件,会让当前流切换到流动模式
//当流中将数据传给消费者后触发
//由于我们在上面配置了 highWaterMark 为 3字节,所以下面会打印多次。
rs.on('data', function (data) {
  console.log(data);
});
//流中没有数据可供消费者时触发
rs.on('end', function () {
  console.log('数据读取完毕');
});
//读取数据出错时触发
rs.on('error', function () {
  console.log('读取错误');
});
//当文件被关闭时触发
rs.on('close', function () {
  console.log('文件关闭');
});

需要特别注意,'open'和'close'事件并非所有类型的流都会触发,具体取决于流的实现方式。

当监听'data'事件后,系统会尽可能快地读取并推送数据。但在某些场景下,可能需要暂停数据读取以执行其他操作。

此时,可以使用pause()和resume()方法来手动控制流的读取节奏。


const fs = require('fs');
//创建一个文件可读流
let rs = fs.createReadStream('./1.txt', {
  highWaterMark: 3
});
rs.on('data', function (data) {
  console.log(`读取了 ${data.length} 字节数据 : ${data.toString()}`);
  //使流动模式的流停止触发'data'事件,切换出流动模式,数据都会保留在内部缓存中。
  rs.pause();
  //等待3秒后,再恢复触发'data'事件,将流切换回流动模式。
  setTimeout(function () {
    rs.resume();
  }, 3000);
});

可读流还提供了一个'readable'事件,当缓冲区中有数据可供读取时触发该事件。

需注意,一旦监听'readable'事件,流会自动停止流动(即进入暂停模式)。此时必须调用read()方法显式读取数据,否则数据将滞留在缓冲区中。

强烈建议不要混用on('data')、on('readable')和pipe()这三种数据消费方式,否则可能导致不可预知的行为或数据丢失。


const fs = require('fs');
let rs = fs.createReadStream('./1.txt', {
  highWaterMark: 1
});
//当流中有数据可供读取时就触发
rs.on('readable', function () {
  let data;
  //循环读取数据
  //参数表示要读取的字节数
  //如果可读的数据不足字节数,则返回缓冲区剩余数据
  //如是没有指定字节数,则返回缓冲区中所有数据
  while (data = rs.read()) {
    console.log(`读取到 ${data.length} 字节数据`);
    console.log(data.toString());
  }
});

可写流的写入优化与管道传输

五、创建可写流,并监听事件


const fs = require('fs');
//创建一个文件可写流
let ws = fs.createWriteStream('./1.txt', {
  highWaterMark: 3
});
//往流中写入数据
//参数一表示要写入的数据
//参数二表示编码方式
//参数三表示写入成功的回调
//缓冲区满时返回false,未满时返回true。
//由于上面我们设置的缓冲区大小为 3字节,所以到写入第3个时,就返回了false。
console.log(ws.write('1', 'utf8'));
console.log(ws.write('2', 'utf8'));
console.log(ws.write('3', 'utf8'));
console.log(ws.write('4', 'utf8'));
function writeData() {
  let cnt = 9;
  return function () {
    let flag = true;
    while (cnt && flag) {
      flag = ws.write(`${cnt}`);
      console.log('缓冲区中写入的字节数', ws.writableLength);
      cnt--;
    }
  };
}
let wd = writeData();
wd();
//当缓冲区中的数据满的时候,应停止写入数据,
//一旦缓冲区中的数据写入文件了,并清空了,则会触发 'drain' 事件,告诉生产者可以继续写数据了。
ws.on('drain', function () {
  console.log('可以继续写数据了');
  console.log('缓冲区中写入的字节数', ws.writableLength);
  wd();
});
//当流或底层资源关闭时触发
ws.on('close', function () {
  console.log('文件被关闭');
});
//当写入数据出错时触发
ws.on('error', function () {
  console.log('写入数据错误');
});

对于可写流,end()方法用于结束写入操作,并触发'finish'事件,表示所有数据已成功写入底层系统。


const fs = require('fs');
//创建一个文件可写流
let ws = fs.createWriteStream('./1.txt', {
  highWaterMark: 3
});
//往流中写入数据
//参数一表示要写入的数据
//参数二表示编码方式
//参数三表示写入成功的回调
//缓冲区满时返回false,未满时返回true。
//由于上面我们设置的缓冲区大小为 3字节,所以到写入第3个时,就返回了false。
console.log(ws.write('1', 'utf8'));
console.log(ws.write('2', 'utf8'));
console.log(ws.write('3', 'utf8'));
console.log(ws.write('4', 'utf8'));
//调用end()表明已经没有数据要被写入,在关闭流之前再写一块数据。
//如果传入了回调函数,则将作为 'finish' 事件的回调函数
ws.end('最后一点数据', 'utf8');
//调用 end() 且缓冲区数据都已传给底层系统时触发
ws.on('finish', function () {
  console.log('写入完成');
});

此外,cork()和uncork()方法主要用于优化性能。当需要写入大量小块数据时,内部缓冲机制可能导致性能下降,通过cork()暂停缓冲输出,待批量数据处理完毕后再调用uncork()释放缓冲,可显著提升写入效率。


const fs = require('fs');
let ws = fs.createWriteStream('./1.txt', {
  highWaterMark: 1
});
//调用 cork() 后,会强制把所有写入的数据缓冲到内存中。
//不会因为写入的数据超过了 highWaterMark 的设置而写入到文件中。
ws.cork();
ws.write('1');
console.log(ws.writableLength);
ws.write('2');
console.log(ws.writableLength);
ws.write('3');
console.log(ws.writableLength);
//将调用 cork() 后的缓冲数据都输出到目标,也就是写入文件中。
ws.uncork();

务必确保cork()的调用次数与uncork()一致,否则可能导致缓冲数据无法正确输出或状态异常。


const fs = require('fs');
let ws = fs.createWriteStream('./1.txt', {
  highWaterMark: 1
});
//调用一次 cork() 就应该写一次 uncork(),两者要一一对应。
ws.cork();
ws.write('4');
ws.write('5');
ws.cork();
ws.write('6');
process.nextTick(function () {
  //注意这里只调用了一次 uncork()
  ws.uncork();
  //只有调用同样次数的 uncork() 数据才会被输出。
  ws.uncork();
});

六、可读流的 pipe() 方法

pipe()方法在可读流与可写流之间建立连接,其内部逻辑类似于手动处理数据的代码实现,但更加简洁高效。


const fs = require('fs');
//创建一个可读流
let rs = fs.createReadStream('./1.txt', {
  highWaterMark: 3
});
//创建一个可写流
let ws = fs.createWriteStream('./2.txt', {
  highWaterMark: 3
});
rs.on('data', function (data) {
  let flag = ws.write(data);
  console.log(`往可写流中写入 ${data.length} 字节数据`);
  //如果写入缓冲区已满,则暂停可读流的读取
  if (!flag) {
    rs.pause();
    console.log('暂停可读流');
  }
});
//监控可读流数据是否读完
rs.on('end', function () {
  console.log('数据已读完');
  //如果可读流读完了,则调用 end() 表示可写流已写入完成
  ws.end();
});
//如果可写流缓冲区已清空,可以再次写入,则重新打开可读流
ws.on('drain', function () {
  rs.resume();
  console.log('重新开启可读流');
});

我们可以直接使用pipe()方法完成上述复杂的数据传输功能,无需手动处理事件和缓冲。


const fs = require('fs');
//创建一个可读流
let rs = fs.createReadStream('./1.txt', {
  highWaterMark: 3
});
//创建一个可写流
let ws = fs.createWriteStream('./2.txt', {
  highWaterMark: 3
});
let ws2 = fs.createWriteStream('./3.txt', {
  highWaterMark: 3
});
//绑定可写流到可读流,自动将可读流切换到流动模式,将可读流的所有数据推送到可写流。
rs.pipe(ws);
//可以绑定多个可写流
rs.pipe(ws2);

若需断开连接,可以手动调用unpipe()方法将可写流从管道中解绑。


const fs = require('fs');
//创建一个可读流
let rs = fs.createReadStream('./1.txt', {
  highWaterMark: 3
});
//创建一个可写流
let ws = fs.createWriteStream('./2.txt', {
  highWaterMark: 3
});
let ws2 = fs.createWriteStream('./3.txt', {
  highWaterMark: 3
});
rs.pipe(ws);
rs.pipe(ws2);
//解绑可写流,如果参数没写,则解绑所有管道
setTimeout(function () {
  rs.unpipe(ws2);
}, 0);

希望本文所述的Node.js Stream流实现与使用方法,能对大家的程序设计工作有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章