转载

如何实时监听文件的新增内容:一个简单 tailf 命令的实现

在 Linux/Uinx 系统上,有一个 tail 命令,它可以用来显示一个文件尾部的内容,比如执行 tail large_file.txt 仅仅显示该文件最后的 10 行内容(通过 -n 参数可以指定显示的行数)。

tail 命令还有一个 -f 选项,可以监听文件内容的变化,当有新增的内容时会继续打印到屏幕上,因此在处理日志文件时常常会使用到它来跟踪文件变化。

前段时间在研究 Node.js 上的日志文件的处理时,偶然得知 tail -f 命令(下文简称 tailf )的用法,因此十分好奇, 是否有一种 API 可以实时监听文件内容的变化? ,当然答案是否定的。后来经过网上查找资料,发现其实原理很简单,无非是不断地尝试 read() 文件的内容,如果能读取到就输出,仅此而已。

首先从网上找到一段 使用 Java 实现 tail -f 的代码 :

BufferedReader br = new BufferedReader(...); String line; while (keepReading) {     line = reader.readLine();     if (line == null) {         //wait until there is more of the file for us to read         Thread.sleep(1000);     }     else {         //do something interesting with the line     } }

从上面的程序逻辑可以看出,在 while 循环里面,不断地尝试 readLine() 来读取一行内容,如果读取成功就继续,不成功则先 sleep(1000) 等待 1 秒钟。

实现一个简单的 tailf

因此我们可以使用以下 Node.js 代码实现相似的功能:

'use strict';  const fs = require('fs');  /**  * tailf  *  * @param {String} filename 文件名  * @param {Number} delay 读取不到内容时等待的时间,ms  */ function tailf(filename, delay) {    // 每次读取文件块大小,16K   const CHUNK_SIZE = 16 * 1024;   // 打开文件,获取文件句柄   const fd = fs.openSync(filename, 'r');   // 文件开始位置   let position = 0;   // 循环读取   const loop = () => {      const buf = new Buffer(CHUNK_SIZE);     const bytesRead = fs.readSync(fd, buf, 0, CHUNK_SIZE, position);     // 实际读取的内容长度以 bytesRead 为准,并且更新 position 位置     position += bytesRead;     process.stdout.write(buf.slice(0, bytesRead));      if (bytesRead < CHUNK_SIZE) {       // 如果当前已到达文件末尾,则先等待一段时间再继续       setTimeout(loop, delay);     } else {       loop();     }   };   loop(); }

说明:

  • 首先使用 fs.openSync() 来打开文件,得到文件句柄之后,再通过 fs.readSync() 读取文件内容
  • 由于 Node.js 中并没有阻塞的 sleep() 方法,我们只能使用 setTimeout() 来模拟,不能直接使用 while 死循环,否则程序会占满整个 CPU 资源

将以上的代码保存为文件 tailf.js ,并且在文件末尾增加以下代码:

const filename = process.argv[2]; if (filename) {   tailf(filename, 100); } else {   console.log('使用方法: node tailf <文件名>'); }

现在我们来测试一下。首先执行以下命令新建一个日志文件:

$ echo "hello" > test.log

然后再开始监听文件的变化:

$ node tailf test.log

执行以上命令后,可以看到屏幕上打印出内容 hello ,但是程序还没有结束。再尝试在另一个控制台窗口下执行以下命令:

$ echo "$(date) hello, world" >> test.log

如果能看到 tailf 屏幕上打印出 Sat Jul 23 01:46:05 CST 2016 hello, world 这样的内容,说明我们实现的这个 tailf 命令已经基本上能用了。我们也不妨多执行几次上面的命令,还可以把 hello, world 改成其他的内容,好好感受一下,有木有一股很强的成就感迎面吹来呢……

从文件末尾开始

上面的程序有一个小问题:每次执行 tailf 时都会先从头读取一遍文件,然后才开始监听,假如我们是用来处理很大的日志文件,每次都重头读取一遍似乎不太好,也对不起 tail 这个单词。所以呢,我们机智地修改一行代码解决它吧:

// 文件开始位置 let position = fs.fstatSync(fd).size;

说明:通过 fs.fstatSync() 读取文件的属性,然后得到当前文件的尺寸,直接把 position 设置到文件最末尾就行啦。

使用异步方法

为了使得程序简单清晰,上文的程序用的都是 Sync 后缀的方法,这在只处理一个任务的 tailf 命令是最简单直接的。假如我们要实现一个 tailf 函数,将它嵌入到我们编写的项目里面处理多个监听文件内容的任务,那就得使用非阻塞的方法来操作文件了:

'use strict';  const fs = require('fs');  /**  * tailf  *  * @param {String} filename 文件名  * @param {Number} delay 读取不到内容时等待的时间,ms  * @param {Function} onError 操作出错时的回调函数,onError(err)  * @param {Function} onData 读取到文件内容时的回调函数,onData(data)  */ function tailf(filename, delay, onError, onData) {    // 每次读取文件块大小,16K   const CHUNK_SIZE = 16 * 1024;   // 打开文件,获取文件句柄   fs.open(filename, 'r', (err, fd) => {     if (err) return onError(err);      // 文件开始位置     fs.fstat(fd, (err, stats) => {       if (err) return onError(err);        // 文件开始位置       let position = stats.size;       // 循环读取       const loop = () => {          const buf = new Buffer(CHUNK_SIZE);         fs.read(fd, buf, 0, CHUNK_SIZE, position,         (err, bytesRead, buf) => {           if (err) return onError(err);            // 实际读取的内容长度以 bytesRead 为准           // 并且更新 position 位置           position += bytesRead;           onData(buf.slice(0, bytesRead));            if (bytesRead < CHUNK_SIZE) {             // 如果当前已到达文件末尾,则先等待一段时间再继续             setTimeout(loop, delay);           } else {             loop();           }         });       };       loop();     });   }); }

说明:

  • 所以操作文件的方法去掉 Sync 后缀,改用回调函数获取结果
  • tailf 新增了两个参数 onErroronData ,分别用来回调操作时发生错误和检测到文件内容更新,其中 onData 会被执行多次

现在可以这样使用 tailf()

const filename = process.argv[2]; if (filename) {   tailf(filename, 100, err => {     if (err) console.error(err);   }, data => {     process.stdout.write(data);   }); } else {   console.log('使用方法: node tailf <文件名>'); }

测试方法还是跟上文的一样,当然这么简单的场景根本看不出区别啦。

相关链接

原文  http://morning.work/page/2016-07/how-to-implement-a-tail-f-command-in-nodejs.html
正文到此结束
Loading...