我只是在学习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
答案 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编程的一项基本技能。
有多种解决方法:
使用fs.createReadStream()
并通过侦听data
事件来读取文件。这可能是最干净的方案。如果您的目标是做一个十六进制输出器,您甚至可能想要学习一种称为转换的流功能,在该功能中,您可以将二进制流转换为十六进制流。
在此处使用所有相关fs
函数的promise版本,并使用async / await来允许for
循环等待异步操作完成,然后再进行下一次迭代。这样,您就可以编写看似同步的代码,但使用异步I / O。
编写另一种类型的循环结构(不使用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);
});