使用Node.js实时读取文件

时间:2012-06-27 11:18:02

标签: javascript node.js real-time fifo unix-socket

我需要使用node.js实时读取正在写入文件的数据的最佳方法。麻烦的是,Node是一艘快速发展的船,它使寻找解决问题的最佳方法变得困难。

我想做什么
我有一个正在做某事的java进程,然后将它所做的事情的结果写入文本文件。它通常需要5分钟到5个小时才能运行,数据写入的时间很长,并且可以达到一些相当高的吞吐率(大约1000线/秒)。

我想实时阅读这个文件,然后使用节点聚合数据并将其写入套接字,可以在客户端上绘制图形。

客户端,图形,套接字和聚合逻辑都已完成,但我对阅读文件的最佳方法感到困惑。

我尝试过(或者至少玩过)
FIFO - 我可以告诉我的Java进程写入fifo并使用node读取它,这实际上是我们如何使用Perl实现这一点,但是因为其他所有东西都在节点中运行,所以有意义的是代码结束。

Unix Sockets - 如上所述。

fs.watchFile - 这会对我们需要的东西起作用吗?

fs.createReadStream - 这比watchFile好吗?

fs& tail -f - 似乎是一个黑客。

实际上,我的问题是什么
我倾向于使用Unix套接字,这似乎是最快的选择。但节点是否具有更好的内置功能,可以实时读取fs中的文件?

4 个答案:

答案 0 :(得分:8)

如果您希望将文件保留为数据的持久存储,以防止在系统崩溃或网络中正在运行的进程中的某个成员死亡时丢失流,您仍然可以继续写入归档并从中读取。

如果您不需要将此文件作为Java进程生成结果的持久存储,那么使用Unix套接字对于简易性和性能都要好得多。

fs.watchFile()不是你需要的,因为它适用于文件统计,因为文件系统会报告它,因为你想要读取已经写入的文件,这不是你想要的。

短消息更新:我很遗憾地发现虽然我曾指责fs.watchFile()使用上一段中的文件统计信息,但我在下面的示例代码中自己做了同样的事情。 !虽然我已经警告读者“要小心!”因为我在几分钟内写完了它,甚至没有测试好;如果基础系统支持,可以使用fs.watch()代替watchFilefstatSync来做得更好。

对于从文件中读取/写入,我在下面的内容中写了以下内容以获得乐趣:

test-fs-writer.js :[您在Java过程中编写文件后不需要这样做]

var fs = require('fs'),
    lineno=0;

var stream = fs.createWriteStream('test-read-write.txt', {flags:'a'});

stream.on('open', function() {
    console.log('Stream opened, will start writing in 2 secs');
    setInterval(function() { stream.write((++lineno)+' oi!\n'); }, 2000);
});

test-fs-reader.js :[注意,这只是演示,检查错误的对象!]

var fs = require('fs'),
    bite_size = 256,
    readbytes = 0,
    file;

fs.open('test-read-write.txt', 'r', function(err, fd) { file = fd; readsome(); });

function readsome() {
    var stats = fs.fstatSync(file); // yes sometimes async does not make sense!
    if(stats.size<readbytes+1) {
        console.log('Hehe I am much faster than your writer..! I will sleep for a while, I deserve it!');
        setTimeout(readsome, 3000);
    }
    else {
        fs.read(file, new Buffer(bite_size), 0, bite_size, readbytes, processsome);
    }
}

function processsome(err, bytecount, buff) {
    console.log('Read', bytecount, 'and will process it now.');

    // Here we will process our incoming data:
        // Do whatever you need. Just be careful about not using beyond the bytecount in buff.
        console.log(buff.toString('utf-8', 0, bytecount));

    // So we continue reading from where we left:
    readbytes+=bytecount;
    process.nextTick(readsome);
}

您可以安全地避免使用nextTick并直接致电readsome()。由于我们仍然在这里工作同步,因此在任何意义上都没有必要。我喜欢它。 :P

Oliver Lloyd编辑

采用上面的示例,但将其扩展为读取CSV数据,可以得到:

var lastLineFeed,
    lineArray;
function processsome(err, bytecount, buff) {
    lastLineFeed = buff.toString('utf-8', 0, bytecount).lastIndexOf('\n');

    if(lastLineFeed > -1){

        // Split the buffer by line
        lineArray = buff.toString('utf-8', 0, bytecount).slice(0,lastLineFeed).split('\n');

        // Then split each line by comma
        for(i=0;i<lineArray.length;i++){
            // Add read rows to an array for use elsewhere
            valueArray.push(lineArray[i].split(','));
        }   

        // Set a new position to read from
        readbytes+=lastLineFeed+1;
    } else {
        // No complete lines were read
        readbytes+=bytecount;
    }
    process.nextTick(readFile);
}

答案 1 :(得分:6)

为什么你认为tail -f是黑客?

虽然弄清楚我找到了一个很好的例子但我会做类似的事情。 node.js和WebSocket的实时在线活动监控示例:
http://blog.new-bamboo.co.uk/2009/12/7/real-time-online-activity-monitor-example-with-node-js-and-websocket

为了让这个答案完整,我给你写了一个代码,它会在0.8.0下运行 - (http服务器可能是黑客)。

子进程是使用tail运行的,因为子进程是一个包含三个流的EventEmitter(在我们的例子中我们使用stdout),你可以添加一个带有on的侦听器

filename: tailServer.js

用法:node tailServer /var/log/filename.log

var http = require("http");
var filename = process.argv[2];


if (!filename)
    return console.log("Usage: node tailServer filename");

var spawn = require('child_process').spawn;
var tail = spawn('tail', ['-f', filename]);

http.createServer(function (request, response) {
    console.log('request starting...');

    response.writeHead(200, {'Content-Type': 'text/plain' });

    tail.stdout.on('data', function (data) {
      response.write('' + data);                
    });
}).listen(8088);

console.log('Server running at http://127.0.0.1:8088/');

答案 2 :(得分:1)

这个模块是@hasanyasin建议的原则的实现:

https://github.com/felixge/node-growing-file

答案 3 :(得分:0)

我从@hasanyasin那里得到答案并将其包装成模块化的承诺。基本思想是传递一个文件和一个处理函数,它对从文件中读取的字符串化缓冲区执行某些操作。如果处理函数返回true,则文件将停止被读取。如果处理程序没有足够快地返回true,你也可以设置一个超时读取的终止。

如果因超时调用resolve(),则promiser将返回true,否则返回false。

请参阅底部的使用示例。

// https://stackoverflow.com/a/11233045

var fs = require('fs');
var Promise = require('promise');

class liveReaderPromiseMe {
    constructor(file, buffStringHandler, opts) {
        /*
            var opts = {
                starting_position: 0,
                byte_size: 256,
                check_for_bytes_every_ms: 3000,
                no_handler_resolution_timeout_ms: null
            };
        */

        if (file == null) {
            throw new Error("file arg must be present");
        } else {
            this.file = file;
        }

        if (buffStringHandler == null) {
            throw new Error("buffStringHandler arg must be present");
        } else {
            this.buffStringHandler = buffStringHandler;
        }

        if (opts == null) {
            opts = {};
        }

        if (opts.starting_position == null) {
            this.current_position = 0;
        } else {
            this.current_position = opts.starting_position;
        }

        if (opts.byte_size == null) {
            this.byte_size = 256;
        } else {
            this.byte_size = opts.byte_size;
        }

        if (opts.check_for_bytes_every_ms == null) {
            this.check_for_bytes_every_ms = 3000;
        } else {
            this.check_for_bytes_every_ms = opts.check_for_bytes_every_ms;
        }

        if (opts.no_handler_resolution_timeout_ms == null) {
            this.no_handler_resolution_timeout_ms = null;
        } else {
            this.no_handler_resolution_timeout_ms = opts.no_handler_resolution_timeout_ms;
        }
    }


    startHandlerTimeout() {
        if (this.no_handler_resolution_timeout_ms && (this._handlerTimer == null)) {
            var that = this;
            this._handlerTimer = setTimeout(
                function() {
                    that._is_handler_timed_out = true;
                },
                this.no_handler_resolution_timeout_ms
            );
        }
    }

    clearHandlerTimeout() {
        if (this._handlerTimer != null) {
            clearTimeout(this._handlerTimer);
            this._handlerTimer = null;
        }
        this._is_handler_timed_out = false;
    }

    isHandlerTimedOut() {
        return !!this._is_handler_timed_out;
    }


    fsReadCallback(err, bytecount, buff) {
        try {
            if (err) {
                throw err;
            } else {
                this.current_position += bytecount;
                var buff_str = buff.toString('utf-8', 0, bytecount);

                var that = this;

                Promise.resolve().then(function() {
                    return that.buffStringHandler(buff_str);
                }).then(function(is_handler_resolved) {
                    if (is_handler_resolved) {
                        that.resolve(false);
                    } else {
                        process.nextTick(that.doReading.bind(that));
                    }
                }).catch(function(err) {
                    that.reject(err);
                });
            }
        } catch(err) {
            this.reject(err);
        }
    }

    fsRead(bytecount) {
        fs.read(
            this.file,
            new Buffer(bytecount),
            0,
            bytecount,
            this.current_position,
            this.fsReadCallback.bind(this)
        );
    }

    doReading() {
        if (this.isHandlerTimedOut()) {
            return this.resolve(true);
        } 

        var max_next_bytes = fs.fstatSync(this.file).size - this.current_position;
        if (max_next_bytes) {
            this.fsRead( (this.byte_size > max_next_bytes) ? max_next_bytes : this.byte_size );
        } else {
            setTimeout(this.doReading.bind(this), this.check_for_bytes_every_ms);
        }
    }


    promiser() {
        var that = this;
        return new Promise(function(resolve, reject) {
            that.resolve = resolve;
            that.reject = reject;
            that.doReading();
            that.startHandlerTimeout();
        }).then(function(was_resolved_by_timeout) {
            that.clearHandlerTimeout();
            return was_resolved_by_timeout;
        });
    }
}


module.exports = function(file, buffStringHandler, opts) {
    try {
        var live_reader = new liveReaderPromiseMe(file, buffStringHandler, opts);
        return live_reader.promiser();
    } catch(err) {
        return Promise.reject(err);
    }
};

然后使用上面的代码:

var fs = require('fs');
var path = require('path');
var Promise = require('promise');
var liveReadAppendingFilePromiser = require('./path/to/liveReadAppendingFilePromiser');

var ending_str = '_THIS_IS_THE_END_';
var test_path = path.join('E:/tmp/test.txt');

var s_list = [];
var buffStringHandler = function(s) {
    s_list.push(s);
    var tmp = s_list.join('');
    if (-1 !== tmp.indexOf(ending_str)) {
        // if this return never occurs, then the file will be read until no_handler_resolution_timeout_ms
        // by default, no_handler_resolution_timeout_ms is null, so read will continue forever until this function returns something that evaluates to true
        return true;
        // you can also return a promise:
        //  return Promise.resolve().then(function() { return true; } );
    }
};

var appender = fs.openSync(test_path, 'a');
try {
    var reader = fs.openSync(test_path, 'r');
    try {
        var options = {
            starting_position: 0,
            byte_size: 256,
            check_for_bytes_every_ms: 3000,
            no_handler_resolution_timeout_ms: 10000,
        };

        liveReadAppendingFilePromiser(reader, buffStringHandler, options)
        .then(function(did_reader_time_out) {
            console.log('reader timed out: ', did_reader_time_out);
            console.log(s_list.join(''));
        }).catch(function(err) {
            console.error('bad stuff: ', err);
        }).then(function() {
            fs.closeSync(appender);
            fs.closeSync(reader);
        });

        fs.write(appender, '\ncheck it out, I am a string');
        fs.write(appender, '\nwho killed kenny');
        //fs.write(appender, ending_str);
    } catch(err) {
        fs.closeSync(reader);
        console.log('err1');
        throw err;
    }
} catch(err) {
    fs.closeSync(appender);
        console.log('err2');
    throw err;
}