【问题标题】:How to read large files with fs.read and a buffer in javascript?如何使用 fs.read 和 javascript 中的缓冲区读取大文件?
【发布时间】:2019-03-09 08:51:53
【问题描述】:

我只是在学习 javascript,在学习一门新语言时我执行的一项常见任务是编写一个 hex-dump 程序。要求是 1. 读取命令行提供的文件,2. 能够读取大文件(一次读取缓冲区),3. 输出十六进制数字和可打印的 ascii 字符。

尽我所能,我无法让 fs.read(...) 函数实际执行。这是我开始使用的代码:

    console.log(process.argv);
    if (process.argv.length < 3) {
        console.log("usage: node hd <filename>");
        process.exit(1);
    }
    fs.open(process.argv[2], 'r', (err,fd) => {
        if (err) {
            console.log("Error: ", err);
            process.exit(2);
        } else {
            fs.fstat(fd, (err,stats) => {
                if (err) {
                    process.exit(4);
                } else { 
                    var size = stats.size;
                    console.log("size = " + size);
                    going = true;
                    var buffer = new Buffer(8192);
                    var offset = 0;
                    //while( going ){
                    while( going ){
                        console.log("Reading...");
                        fs.read(fd, buffer, 0, Math.min(size-offset, 8192), offset, (error_reading_file, bytesRead, buffer) => {
                            console.log("READ");
                            if (error_reading_file)
                            {
                                console.log(error_reading_file.message);
                                going = false;
                            }else{
                                offset += bytesRead;
                                for (a=0; a< bytesRead; a++) {
                                    var z = buffer[a];
                                    console.log(z);
                                }
                                if (offset >= size) {
                                    going = false;
                                }
                            }
                        });
                    }
                    //}
                    fs.close(fd, (err) => {
                        if (err) {
                            console.log("Error closing file!");
                            process.exit(3);
                        }
                    });
                }
            });
        }
    });

如果我注释掉 while() 循环,read() 函数会执行,但当然只执行一次(适用于 8K 以下的文件)。现在,我只是没有看到像这样采用缓冲区和偏移量的 read() 函数的目的......有什么诀窍?

节点 v8.11.1,OSX 10.13.6

【问题讨论】:

  • 为什么不调试你的程序看看发生了什么?
  • @JeroenHeier 刚试过,它只是跳过 fs.read() 函数和循环。
  • 好吧,fs.read() 是异步的,因此它在while() 循环中不会按照您希望的方式工作。我建议您使用该流中的fs.createReadStream()data 事件。然后,数据将在一系列事件中提供给您。而且,要在 node.js 中做任何有用的事情,您确实需要了解异步 I/O 的概念以及如何使用它进行编程。
  • @jfriend00 谢谢,这很有帮助。我将为 createReadSteam() 方法重构...但我仍然很好奇 fs.read() 方法有什么价值?
  • 你明白fs.read() 是非阻塞的吗?你调用它,它开始一个读取操作,它立即返回并且你的while() 循环继续执行调用一大堆更多的fs.read() 操作。事实上,while 循环会一直持续下去,因为它永远不会停止,所以它永远不会让事件循环有机会处理您的任何fs.read() 完成事件。正如我上面所说,您不能将非阻塞操作放在 while 循环中,并且像阻塞一样进行编程。

标签: javascript node.js file while-loop binary


【解决方案1】:

首先,如果这只是您不时运行的一次性脚本,而不是服务器中的代码,则无需使用更复杂的异步 I/O。您可以使用同步的阻塞 I/O 调用,例如 fs.openSync()fs.statSync()fs.readSync() 等...然后认为将在您的 while 循环中工作,因为这些调用是阻塞的(它们不会返回,直到结果完成)。您可以使用它们编写正常的循环和顺序代码。永远不应该在服务器环境中使用同步的阻塞 I/O,因为它会破坏服务器进程的可扩展性(它能够处理来自多个客户端的请求),但如果这是一个一次性的本地脚本,只有一项工作要做,那么同步 I/O 就非常合适了。

其次,这就是您的代码无法正常工作的原因。 node.js 中的 Javascript 是单线程和事件驱动的。这意味着解释器从事件队列中拉出一个事件,运行与该事件关联的代码,并且在该代码将控制权返回给解释器之前什么都不做。此时,它会从事件队列中拉出下一个事件并运行它。

当你这样做时:

 while(going) {
     fs.read(... => (err, data) {
         // some logic here that may change the value of the going variable
     });
 }

您刚刚为自己创建了一个无限循环。这是因为while(going) 循环永远运行。它永远不会停止循环,也永远不会将控制权返回给解释器,以便它可以从事件队列中获取下一个事件。它只是不断循环。但是,异步、非阻塞fs.read() 的完成是通过事件队列完成的。因此,您正在等待going 标志更改,但您永远不允许系统处理可以实际更改going 标志的事件。在您的实际情况下,您可能最终会因为在紧密循环中调用 fs.read() 太多次而耗尽某种资源,或者解释器将陷入无限循环。

了解如何对涉及异步操作的重复循环类型的任务进行编程需要学习一些新的编程技术。由于 node.js 中的很多 I/O 都是异步且非阻塞的,因此这是 node.js 编程开发的必备技能。

有很多不同的方法可以解决这个问题:

  1. 使用fs.createReadStream() 并通过侦听data 事件来读取文件。这可能是最干净的方案。如果您的目标是做一个十六进制输出器,您甚至可能想学习一种称为转换的流功能,您可以将二进制流转换为十六进制流。

  2. 在此处使用所有相关 fs 函数的 promise 版本,并使用 async/await 允许您的 for 循环等待异步操作完成,然后再进行下一次迭代。这允许您编写外观同步的代码,但使用异步 I/O。

  3. 编写不同类型的循环构造(不使用 while)循环,在 fs.read() 完成后手动重复循环。


这是一个使用fs.createReadStream()的简单示例:

const fs = require('fs');

function convertToHex(val) {
    let str = val.toString(16);
    if (str.length < 2) {
        str = "0" + str;
    }
    return str.toUpperCase();
}

let stream = fs.createReadStream(process.argv[2]);
let outputBuffer = "";
stream.on('data', (data) => {
    // you get an unknown length chunk of data from the file here in a Buffer object
    for (const val of data) {
        outputBuffer += convertToHex(val) + " ";
        if (outputBuffer.length > 100) {
            console.log(outputBuffer);
            outputBuffer = "";
        }
    }
}).on('error', err => {
    // some sort of error reading the file
    console.log(err);
}).on('end', () => {
    // output any remaining buffer
    console.log(outputBuffer);
});

希望您会注意到,由于流会为您处理文件的打开、关闭和读取操作,因此这是一种更简单的编码方式。您所要做的就是为读取的数据、读取错误和操作结束提供事件处理程序。


这是一个使用 async/await 和新文件接口(其中文件描述符是您调用方法的对象)和节点 v10 中的承诺的版本。

const fs = require('fs').promises;

function convertToHex(val) {
    let str = val.toString(16);
    if (str.length < 2) {
        str = "0" + str;
    }
    return str.toUpperCase();
}

async function run() {
    const readSize = 8192; 
    let cntr = 0;
    const buffer = Buffer.alloc(readSize);
    const fd = await fs.open(process.argv[2], 'r');
    try {
        let outputBuffer = "";
        while (true) {
            let data = await fd.read(buffer, 0, readSize, null);
            for (let i = 0; i < data.bytesRead; i++) {
                cntr++;
                outputBuffer += convertToHex(buffer.readUInt8(i)) + " ";
                if (outputBuffer.length > 100) {
                    console.log(outputBuffer);
                    outputBuffer = "";
                }
            }
            // see if all data has been read
            if (data.bytesRead !== readSize) {
                console.log(outputBuffer);
                break;
            }
        }
    } finally {
        await fd.close();
    }
    return cntr;
}

run().then(cntr => {
    console.log(`done - ${cntr} bytes read`);
}).catch(err => {
    console.log(err);
});

【讨论】:

  • 谢谢! - 您最初的解释足以为我指明正确的方向,而且我确实让 hexdump 程序使用基于回调的模型!但是,我非常感谢这里的详细解释。
猜你喜欢
  • 1970-01-01
  • 2010-12-05
  • 2021-11-30
  • 1970-01-01
  • 1970-01-01
  • 2019-06-07
  • 2015-04-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多