【问题标题】:How NOT to stop reading file when meeting EOF?遇到EOF时如何不停止读取文件?
【发布时间】:2015-04-28 15:12:00
【问题描述】:

我正在尝试为 Node.js 实现一个例程,该例程允许打开一个文件,此时其他进程正在附加该文件,然后在附加到数据块时立即返回它们文件。它可以被认为类似于tail -f UNIX 命令,但是当块可用时立即执行,而不是轮询随时间的变化。或者,可以将其视为处理文件,就像处理套接字一样——期望 on('data') 不时触发,直到文件被显式关闭。

在 C 语言中,如果我要实现这个,我只需打开文件,将其文件描述符提供给 select()(或具有类似名称的任何替代函数),然后在文件描述符标记为“时读取块”可读”。所以,当没有什么要读的时候,它就不能读了,当有东西附加到文件中时,它又可以读了。

对于以下 Javascript 代码示例,我有点预料到这种行为:

function readThatFile(filename) {
    const stream = fs.createReadStream(filename, {
        flags: 'r',
        encoding: 'utf8',
        autoClose: false // I thought this would prevent file closing on EOF too
    });

    stream.on('error', function(err) {
        // handle error
    });

    stream.on('open', function(fd) {
        // save fd, so I can close it later
    });

    stream.on('data', function(chunk) {
        // process chunk
        // fs.close() if I no longer need this file
    });
}

然而,这个代码示例只是在遇到 EOF 时退出,所以我不能等待新的块到达。当然,我可以使用fs.openfs.read 重新实现它,但这有点违背Node.js 的目的。或者,我可以fs.watch() 文件进行更改,但它不能通过网络工作,而且我不喜欢一直重新打开文件而不是保持打开状态的想法。

我试过这样做:

const fd = fs.openSync(filename, 'r'); // sync for readability' sake
const stream = net.Socket({ fd: fd, readable: true, writable: false });

但是没有运气——net.Socket 不高兴并抛出了TypeError: Unsupported fd type: FILE

那么,有什么解决办法吗?

UPD:这是不可能的,my answer 解释了原因。

【问题讨论】:

    标签: javascript node.js file-io


    【解决方案1】:

    我没有研究文件读取流的内部结构,但它们可能不支持等待文件写入更多数据。但是,fs 包绝对支持它的最基本功能。

    为了解释拖尾是如何工作的,我编写了一个有点 hacky 的 tail 函数,它将读取整个文件并为每一行调用回调(仅由 \n 分隔),然后等待文件更多行写入它。请注意,执行此操作的更有效方法是使用固定大小的行缓冲区并将字节随机放入其中(对于极长的行有特殊情况),而不是修改 JavaScript 字符串。

    var fs = require('fs');
    
    function tail(path, callback) {
      var descriptor, bytes = 0, buffer = new Buffer(256), line = '';
    
      function parse(err, bytesRead, buffer) {
        if (err) {
          callback(err, null);
          return;
        }
        // Keep track of the bytes we have consumed already.
        bytes += bytesRead;
        // Combine the buffered line with the new string data.
        line += buffer.toString('utf-8', 0, bytesRead);
        var i = 0, j;
        while ((j = line.indexOf('\n', i)) != -1) {
          // Callback with a single line at a time.
          callback(null, line.substring(i, j));
          // Skip the newline character.
          i = j + 1;
        }
        // Only keep the unparsed string contents for next iteration.
        line = line.substr(i);
        // Keep reading in the next tick (avoids CPU hogging).
        process.nextTick(read);
      }
    
      function read() {
        var stat = fs.fstatSync(descriptor);
        if (stat.size <= bytes) {
          // We're currently at the end of the file. Check again in 500 ms.
          setTimeout(read, 500);
          return;
        }
        fs.read(descriptor, buffer, 0, buffer.length, bytes, parse);
      }
    
      fs.open(path, 'r', function (err, fd) {
        if (err) {
          callback(err, null);
        } else {
          descriptor = fd;
          read();
        }
      });
    
      return {close: function close(callback) {
        fs.close(descriptor, callback);
      }};
    }
    
    // This will tail the system log on a Mac.
    var t = tail('/var/log/system.log', function (err, line) {
      console.log(err, line);
    });
    
    // Unceremoniously close the file handle after one minute.
    setTimeout(t.close, 60000);
    

    话虽如此,您还应该尝试利用 NPM 社区。经过一番搜索,我找到了 tail-stream 包,它可以做你想做的事情,带有流。

    【讨论】:

    • 好吧,您提出的解决方案受到计时器粒度的影响——如果您轮询过于频繁,您将消耗过多的 CPU,否则您将失去“实时性”,因为更新会延迟。出于这个原因,我在我的问题中提到了select()——它不需要等待超时,所以它可以无限期地阻塞直到有可以读取的东西,或者,对于实际用例,直到其他事件唤醒它——并处理马上。而且,还要提一下,你的方法就是我所说的“使用fs.open/fs.read 重新实现”——优雅的解决方案需要直接处理 node.js 反应器。
    • 关于 NPM 社区——我现在正在寻找其他解决方案中的 idea,因为检查消息来源表明他们使用与您类似的方法,或者 fs.watch,要么尝试fs.readsetInterval...也许我不够彻底,但我必须看到很好的解决方案——或者最终在收到答案后自己写:)
    • @modchan: setTimeout 在这种情况下应该没问题。通过文件系统监视,您很少能达到亚毫秒精度(我认为没有理由这样做——用于实时通信的文件系统很糟糕),您提供的示例tail -f 实际上会每 1 秒轮询一次 IIRC,所以甚至 500 毫秒也是一个进步。
    • 或者,换句话说——如果 C 可以完全以这种方式做到这一点,并且 Node.js 以任何方式使用 select()kqueue() 和朋友们,为什么不可能呢?做那种 Node.js 方式,保留相同的概念? :)
    • 我想我明白你现在想要什么,但你的问题并没有真正这样表达。当您以tail -f 为例并询问如何不在 EOF 上停止时,您似乎是在询问例如 tail-stream 提供的解决方案(在将新数据添加到文件时继续读取它,而无需关闭并重新- 打开文件)。因此,如果我对您的理解正确,您正在寻找监视文件系统事件的最优化方法,以便您只能在实际有新数据可用时尝试读取。这似乎更注重性能。也许你可以澄清你的问题?
    【解决方案2】:

    以前的答案提到了tail-stream 的方法,它使用 fs.watch、fs.read 和 fs.stat 一起创建流文件内容的效果。你可以看到代码在运行here

    另一种可能更骇人听闻的方法可能是通过使用 tail 生成一个子进程来使用它。这当然伴随着 tail 必须存在于目标平台上的限制,但是 node 的优势之一是使用它通过 spawn 进行异步系统开发,甚至在 windows 上,您可以在 msysgit 或 cygwin 等备用 shell 中执行 node 来获取访问 tail 实用程序。

    代码:

    var spawn = require('child_process').spawn;
    
    var child = spawn('tail',
        ['-f', 'my.log']);
    
    child.stdout.on('data',
        function (data) {
            console.log('tail output: ' + data);
        }
    );
    
    child.stderr.on('data',
        function (data) {
            console.log('err data: ' + data);
        }
    );
    

    【讨论】:

    • 节点优势在于您必须将不可移植的 C 实用程序与您的 .js 应用程序捆绑在一起,而不是“仅仅解决问题”? :)
    • 哈哈好点,“其中一个”优势是可以轻松使用系统实用程序,但请确保您有道理。尾流是一个非常清楚的例子,说明了如何完成你所追求的。你也可以写一个原生模块? 鸭子
    【解决方案3】:

    所以,似乎人们已经在寻找这个问题的答案五年了,但还没有关于该主题的答案。

    简而言之:你不能。尤其是在 Node.js 中,你根本不能。

    长答案:原因很少。

    首先,POSIX标准clarifies select() behavior在这方面如下:

    与常规文件关联的文件描述符应始终为准备读取、准备写入和错误条件选择 true。

    所以,select() 无法帮助检测超出文件末尾的写入。

    poll() it's similar:

    常规文件应始终轮询 TRUE 进行读取和写入。

    我无法确定epoll(),因为它不是标准化的,你必须阅读quite lengthy implementation,但我认为它是相似的。

    由于 libuv 是 Node.js 实现的核心,它使用read(), pread() and preadv() in its uv__fs_read(),在文件末尾调用时都不会阻塞,遇到 EOF 时它总是返回空缓冲区。所以,这里也没有运气。

    所以,总结一下,如果需要这样的功能,你的设计一定有问题,你应该修改它。

    【讨论】:

      【解决方案4】:

      你想要做的是一个 FIFO 文件(First In First Out 的首字母缩写),正如你所说的,它就像一个套接字。

      有一个node.js module that allows you to work with fifo 文件。

      我不知道你想要这个做什么,但是有更好的方法来处理 node.js 上的套接字。请改用socket.io

      您还可以查看上一个问题: Reading a file in real-time using Node.js

      更新 1

      我不熟悉任何可以使用常规文件而不是使用套接字类型的模块来执行您想要的操作。但正如您所说,您可以使用tail -f 来解决问题:

      // filename must exist at the time of running the script
      var filename = 'somefile.txt';
      
      var spawn = require('child_process').spawn;
      var tail = spawn('tail', ['-f', filename]);
      
      tail.stdout.on('data', function (data) {
          data = data.toString().replace(/^[\s]+/i,'').replace(/[\s]+$/i,'');
          console.log(data);
      });
      

      然后从命令行尝试echo someline &gt; somefile.txt 并在控制台观看。

      您可能还想看看这个:https://github.com/layerssss/node-tailer

      【讨论】:

      • 不,这不是我想要做的。请再次阅读问题。我只想使用普通文件,等待追加发生。不一样。
      • 我完全不明白为什么会出现 FIFO
      猜你喜欢
      • 2015-06-21
      • 2011-07-31
      • 2013-01-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-25
      相关资源
      最近更新 更多