Node.js多文件Stream合并,串行和并发两种模式的实现方式

 更新时间:2023年10月27日 09:15:46   作者:高先生的猫  
这篇文章主要介绍了Node.js多文件Stream合并,串行和并发两种模式的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

将多个文件合并为一个文件,常见的场景是类似于大文件分片上传,事先根据一定的文件大小拆分为多个小文件上传到服务端,最后服务端在合并起来。

怎么合并?一种简单的办法是使用 fs.readFile 读取,fs.writeFile 追加写入,这种方式是将文件数据先读入应用内存再写入,不是很推荐,Node.js 本身提供了 Stream 模块可以更好的处理这种场景。

在 Stream 中合并文件之前一个比较常用的 api 是 pipe,但是这个 API 对于错误处理不是很友好,一不小心还能搞出文件句柄内存泄漏问题。

本文先介绍 pipe 方法的使用及什么情况下会遇到文件句柄的内存泄漏问题,之后再分别介绍 Stream 合并的两种实现模式。

pipe VS pipeline

pipe

创建一个可读流 readable 和一个可写流 writeable,通过管道 pipe 将可写流绑定到可读流,一个简单的 Stream 操作就完成了。

const fs = require('fs');
const readable = fs.createReadStream('./test1.txt');
const writeable = fs.createWriteStream('./test2.txt');
 
readable.pipe(writeable);

pipe 方法的两个参数:

destination:是一个可写流对象,也就是一个数据写入的目标对象,例如,上面我们创建的 writeable 就是一个可写流对象

options:

  • end:读取结束时终止写入流,默认值是 true
readable.pipe(destination[, options])

默认情况下我们是不需要手动调用写入流的 end 方法关闭的。

现在我们改一下, 设置 end 为 false 写入的目标流将会一直处于打开状态, 此时就需要监听可读流的 end 事件,结束之后手动调用可写流的 end 方法结束( 为什么要这样做?下文 Stream 串行合并会再用到这一特性 )。

// readable.pipe(writeable);
 
readable.pipe(writeable, {
  end: false,
});
readable.on('end', function() {
  writeable.end('结束');
});

还需要注意一点 如果可读流期间发生什么错误,则写入的目标流将不会关闭 ,例如:process.stderr 和 process.stdout 可写流在 Nodejs 进程退出前将永远不会关闭,所以 需要监听错误事件,手动关闭可写流,防止内存泄漏 。

Linux 下一切皆文件,为了测试,在创建可读流时,你可以不创建 test1.txt 文件,让可读流自动触发 error 事件并且将 writeable 的 close 方法注释掉,通过 linux 命令 ls -l /proc/${pid}/fd 查看 error 和非 error 前后的文件句柄变化。

readable.on('error', function(err) {
  console.log('error', err);
  // writeable.close();
});
 
console.log(process.pid); // 打印进程 ID
setInterval(function(){}, 5000) // 让程序不中断,进程不退出

以下为触发 error 错误下 test2.txt 这个文件 fd 将会一直打开,除非进程退出,所以重要的事情再说一遍, 如果使用 pipe 一定要做好错误监听手动关闭每个写入流 ,以防止 “ 内存泄漏 ”。

...
l-wx------ 1 root root 64 Apr 10 15:47 19 -> /root/study/test2.txt
...

注意,Mac 下没有 /proc 文件,可通过 docker 测试。

不想开两个终端的,也可以在程序 setInterval 定时器函数里使用 child_process 模块的 exec 函数执行 ls -l /proc/${process.pid}/fd 命令。

const { exec } = require('child_process');
 
setInterval(function(){
  exec(`ls -l /proc/${process.pid}/fd`, (error, stdout, stderr) => {
    console.log(`stdout: \n`, stdout);
  })
}, 5000) // 让程序不中断,进程不退出

pipeline

Stream 模块的一个新 API pipeline 方法,添加于 Node.js v10.0,Promise 风格需要 Node.js  v15.0+ 支持。

相比较于 pipe 方法增加了错误处理机制,当管道中的某个流发生错误,它会自动处理并释放掉相应的资源。

try {
  await pipeline(
    readable,
    writable
  );
  console.log('Pipeline succeeded.');
} catch (err) {
  console.log('error', err);
}

串行模式 Stream 合并

使用 pipe 方法实现串行模式的流合并,根据前面讲的,设置可读流的 end 为 false 保持写入流一直处于打开状态,直到所有的可读流结束(待合并的文件完成后),我们再将可写流给关闭。

  • streamMerge 函数为入口函数
  • streamMergeRecursive 函数递归调用合并文件
const fs = require('fs');
const path = require('path');
 
/**
 * Stream 合并
 * @param { String } sourceFileDirectory 源文件目录
 * @param { String } targetFile 目标文件
 */
function streamMerge(sourceFileDirectory, targetFile) {
  const scripts =  fs.readdirSync(path.resolve(__dirname, sourceFileDirectory)); // 获取源文件目录下的所有文件
  const fileWriteStream = fs.createWriteStream(path.resolve(__dirname, targetFile)); // 创建一个可写流
 
  // fs.readdir 读取出来的结果,根据具体的规则做下排序,防止因为顺序不对导致最终合并之后的文件无效。
  
  return streamMergeRecursive(scripts, fileWriteStream, sourceFileDirectory);
}
 
/**
 * Stream 合并的递归调用
 * @param { Array } scripts
 * @param { Stream } fileWriteStream
 */
function streamMergeRecursive(scripts=[], fileWriteStream, sourceFileDirectory) {
  // 递归到尾部情况判断
  if (!scripts.length) {
    return fileWriteStream.end("console.log('Stream 合并完成')"); // 最后关闭可写流,防止内存泄漏
  }
 
  const currentFile = path.resolve(__dirname, sourceFileDirectory, scripts.shift());
  const currentReadStream = fs.createReadStream(currentFile); // 获取当前的可读流
 
  currentReadStream.pipe(fileWriteStream, { end: false });
  currentReadStream.on('end', function() {
    streamMergeRecursive(scripts, fileWriteStream, sourceFileDirectory);
  });
 
  currentReadStream.on('error', function(error) { // 监听错误事件,关闭可写流,防止内存泄漏
    console.error(error);
    fileWriteStream.close();
  });
}
 
streamMerge('./files', './file.js');

并发模式 Stream 合并

流合并也是可以采用并发模式的,核心是通过可写流的 start、end 属性控制。

start 有点类似于数据库查询的 skip,在 计算时要求文件分块的下标必须是 0、1、2... 这样的规则 ,这种方式可以不用关注每一个流分块在文件中的存储顺序,也可以将可读流传输至可写流的指定位置。

例如,有一个大文件 dec47b76e3220432100a1155eff7f402(文件 md5 后的 hash 值) 根据 chunkSize(1048576)拆分为 3 个小文件。

/chunks
└── dec47b76e3220432100a1155eff7f402-1048576
    ├── dec47b76e3220432100a1155eff7f402-0
    ├── dec47b76e3220432100a1155eff7f402-1
    └── dec47b76e3220432100a1155eff7f402-2

并发模式的 Stream 合并代码实现如下:

/**
 * Stream concurrent merge
 * @param {String} sourceFileDirectory
 * @param {String} targetFile
 * @param {Number} chunkSize
 */
export const streamConcurrentMerge = async (sourceFileDirectory, targetFile, chunkSize) => {
  const filenames = await fs.readdir(sourceFileDirectory);
  
  await Promise.all(filenames.map(filename => {
    const index = filename.split('-').pop();
    const start = index * chunkSize;
    const end = (index + 1) * chunkSize;
 
    return pipeline(
      createReadStream(path.join(sourceFileDirectory, filename)),
      createWriteStream(targetFile, {
        start,
        end,
      })
    );
  }))
}

总结

使用 pipe 时错误处理是件需要注意的事情,特别是出现这种情况 readable.pipe(a).pipe(b).pipe(writable) 其中任何一个流关闭或出错都会导致整个管道停止工作,这个时候就要销毁所有的流,这种复杂的处理起来极其麻烦, 推荐使用 stream API pipeline 处理,或使用社区 npm 库 pump。

将多个文件合并为一个文件,使用流的方式有两种:

  • 第一种是串行模式依次读取每个文件的内容,通过 pipe 方法写入可写流,直到最后一个文件读取完成关闭写入流。
  • 另一种是并发模式,核心实现是利用写入流的 start、end 属性将可读流传输至可写流的指定位置,上面的实现还可以在优化,比如控制下并发的数量。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • node.js中的fs.createWriteStream方法使用说明

    node.js中的fs.createWriteStream方法使用说明

    这篇文章主要介绍了node.js中的fs.createWriteStream方法使用说明,本文介绍了fs.createWriteStream方法说明、语法、接收参数、使用实例和实现源码,需要的朋友可以参考下
    2014-12-12
  • node读写Excel操作实例分析

    node读写Excel操作实例分析

    这篇文章主要介绍了node读写Excel操作,结合实例形式分析了node.js使用node-xlsx模块模块读写Excel的相关实现技巧与操作注意事项,需要的朋友可以参考下
    2019-11-11
  • Ubuntu安装Node.js的2种方式

    Ubuntu安装Node.js的2种方式

    这篇文章主要给大家介绍了关于Ubuntu安装Node.js的2种方式,在Ubuntu系统上安装Node.js和npm非常简单,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-10-10
  • node+socket实现简易聊天室功能

    node+socket实现简易聊天室功能

    这篇文章主要为大家详细介绍了node+socket实现简易聊天室功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-07-07
  • 使用nodejs开发cli项目实例

    使用nodejs开发cli项目实例

    这篇文章主要介绍了使用nodejs开发cli项目实例,本文讲解使用generator-cli-starter实现cli项目的开发,需要的朋友可以参考下
    2015-06-06
  • node结合swig渲染摸板的方法

    node结合swig渲染摸板的方法

    本篇文章主要介绍了node结合swig渲染摸板的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-04-04
  • koa框架的原理、功能,与基本使用方法概述

    koa框架的原理、功能,与基本使用方法概述

    这篇文章主要介绍了koa框架的原理、功能,与基本使用方法,结合实例形式分析了koa框架的基本功能、原理、使用方法与相关注意事项,需要的朋友可以参考下
    2023-04-04
  • nodejs检测因特网是否断开的解决方案

    nodejs检测因特网是否断开的解决方案

    这篇文章主要给大家介绍了关于nodejs如何检测因特网是否断开的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用nodejs具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-04-04
  • 如何通过node.js来写接口详解

    如何通过node.js来写接口详解

    最近研究了一下nodejs写接口,发现接口并不难写,这篇文章主要给大家介绍了关于如何通过node.js来写接口的相关资料,文中通过实例代码和图文介绍的非常详细,需要的朋友可以参考下
    2022-09-09
  • 安装Node.js并启动本地服务的操作教程

    安装Node.js并启动本地服务的操作教程

    今天小编就为大家分享一篇安装Node.js并启动本地服务的操作教程,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-05-05

最新评论