Чтение файла в режиме реального времени с помощью Node.js

Мне нужно найти лучший способ читать данные, которые записываются в файл, используя node.js, в режиме реального времени. Проблема заключается в том, что Node - быстроходное судно, которое затрудняет поиск наилучшего метода решения проблемы.

Что я хочу делать
У меня есть java-процесс, который что-то делает, а затем записывает результаты этой вещи в текстовый файл. Обычно это занимает от 5 минут до 5 часов, при этом данные записываются все время и могут достигать довольно высокой пропускной способности (около 1000 строк/сек).

Я хотел бы прочитать этот файл в режиме реального времени, а затем, используя Node, агрегировать данные и записать их в сокет, где его можно начертить на клиенте.

Клиент, графики, сокеты и логика агрегации выполняются, но я смущен наилучшим подходом для чтения файла.

Что я пробовал (или, по крайней мере, играл)
FIFO - Я могу сказать, что мой Java-процесс записывается в fifo и читает его с помощью node, это на самом деле то, как мы это реализуем в настоящее время с использованием Perl, но поскольку все остальное работает в Node, имеет смысл переместите код.

Unix Sockets - как указано выше.

fs.watchFile - будет ли это работать для того, что нам нужно?

fs.createReadStream - это лучше, чем watchFile?

fs и tail -f - похоже на хак.

Что, собственно, мой вопрос
Я стараюсь использовать Unix Sockets, это самый быстрый вариант. Но имеет ли Node лучшие встроенные функции для чтения файлов из fs в режиме реального времени?

Ответ 1

Если вы хотите сохранить файл в качестве постоянного хранилища своих данных, чтобы предотвратить потерю потока в случае сбоя системы или один из членов вашей сети запущенных процессов, вы можете продолжать писать на файл и чтение из него.

Если вам не нужен этот файл как постоянное хранилище полученных результатов из вашего Java-процесса, то переход с помощью Unix-сокета намного лучше как для удобства, так и для производительности.

fs.watchFile() не то, что вам нужно, потому что он работает с файловой статистикой, поскольку файловая система сообщает об этом, и поскольку вы хотите прочитать файл, поскольку он уже написан, это не то, что вы хотите.

КРАТКОЕ ОБНОВЛЕНИЕ: Мне очень жаль осознавать, что, хотя я обвинял fs.watchFile() за использование статистики файла в предыдущем абзаце, я сам сделал то же самое в своем примере кода ниже! Хотя я уже предупреждал читателей "заботиться!" потому что я написал его всего за несколько минут, даже не проверив хорошо; все же, это можно сделать лучше, используя fs.watch() вместо watchFile или fstatSync, если базовая система поддерживает его.

Для чтения/записи из файла, я только что написал ниже для удовольствия в своем перерыве:

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: [Позаботьтесь, это просто демонстрация, проверьте объекты err!]

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() напрямую. Поскольку мы все еще работаем здесь, это не нужно ни в каком смысле. Мне просто нравится.: Р

ИЗМЕНИТЬ Оливер Ллойд

Взяв вышеприведенный пример, но расширив его для чтения 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);
}

Ответ 2

Почему, по-вашему, 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-сервер может быть взломан, возможно).

Создается дочерний процесс с хвостом, а поскольку дочерний процесс является 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/');

Ответ 4

Я взял ответ от @hasanyasin и завернул его в модульное обещание. Основная идея заключается в том, что вы передаете файл и функцию обработчика, которая что-то делает с строковым буфером, который считывается из файла. Если функция обработчика возвращает значение true, файл перестает считаться. Вы также можете установить таймаут, который будет убивать чтение, если обработчик не возвращает true достаточно быстро.

Провайсер вернет 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;
}