如何使用fs.read和javascript中的缓冲区读取大文件?

时间:2018-10-04 03:40:46

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

我只是在学习javascript,选择一种新语言时执行的常见任务是编写一个十六进制转储程序。要求是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

1 个答案:

答案 0 :(得分:0)

首先,如果这只是您一次运行的一次性脚本,而这不是服务器中的代码,那么就不需要使用更严格的异步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中具有promise。

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);
});