Асинхронная запись файла




Для начала создадим константы, которые будут хранить логи наших запросов.

const log1 = '127.0.0.1 - - [30/Jan/2021:11:11:20 -0300] "POST /foo HTTP/1.1" 200 0 "-" "curl/7.47.0"'; const log2 = '127.0.0.1 - - [30/Jan/2021:11:11:25 -0300] "GET /boo HTTP/1.1" 404 0 "-" "curl/7.47.0"';

Запишем лог первого запроса в файл, который читали ранее:

fs.writeFile('./access.log', log1, (err) => console.log(err));

 

Если всё прошло успешно, то в терминале появится следующее:

null

 

Теперь откроем файл access.log и посмотрим на его содержимое:

127.0.0.1 - - [30/Jan/2021:11:11:20 -0300] "POST /foo HTTP/1.1" 200 0 "-" "curl/7.47.0"

 

Мы видим, что содержимое файла полностью перезаписалось. Но ведь если теперь мы попытаемся записать лог следующего запроса, он «перезатрёт» текущий. Таким образом, в файле всегда будет только один лог, что не очень логично. Ведь смысл файла с логами — хранить все логи приложения, чтобы понимать происходящее.

Как вариант, можно считать данные из файла, добавить к ним новые логи и снова записать в файл. И так каждый раз. Это работает на одной строчке, на двух, но на больших объёмах данных это работать не будет. С каждой новой записью количество данных будет увеличиваться, и, соответственно, время на операции чтения и записи тоже.

Чтобы добавить данные в файл вместо перезаписи, в options надо передать параметр flag = ‘a’. Узнать, какие бывают флаги и их предназначения, можно в официальной документации.

fs.writeFile('./access.log', log1, { flag: 'a' }, (err) => console.log(err));

 

Результат в файле будет следующим:

127.0.0.1 - - [30/Jan/2021:11:10:15 -0300] "GET /sitemap.xml HTTP/1.1" 200 0 "-" "curl/7.47.0"127.0.0.1 - - [30/Jan/2021:11:11:20 -0300] "POST /foo HTTP/1.1" 200 0 "-" "curl/7.47.0"

 

Ура! Мы дописали данные в конец файла, и это успех. Осталось научиться добавлять данные так, чтобы их удобно было читать. В нашем файле мы видим, что новая строчка записалась вплотную к старой. Так произошло, потому что функция дозаписала данные в конец файла. Чтобы этого избежать, надо между двумя записями вставить символ переноса строки.

В общем случае символ переноса строки — ‘\n’. Однако, если мы работаем в Windows, наша программа для чтения текстовых файлов может не воспринять такой перенос строки. Это связано с особенностями работы Windows. В таком случае для корректной работы к символу переноса строки требуется добавить символ возврата каретки — ‘\r’.

Теперь, когда мы знаем, что делать, выберем вариант реализации. В нашей задаче есть два варианта добавления символа, относящиеся к переносу строки:

  1. Добавить его в начало записываемого лога и всё вместе записать в файл.
  2. Разделить операции и записать перенос строки отдельной командой.

Для наглядности выберем вариант №2.

fs.writeFile('./access.log', '\n', { flag: 'a' }, (err) => console.log(err)); fs.writeFile('./access.log', log1, { flag: 'a' }, (err) => console.log(err));

 

Результат в файле будет следующим:

127.0.0.1 - - [30/Jan/2021:11:10:15 -0300] "GET /sitemap.xml HTTP/1.1" 200 0 "-" "curl/7.47.0"127.0.0.1 - - [30/Jan/2021:11:11:20 -0300] "POST /foo HTTP/1.1" 200 0 "-" "curl/7.47.0" 127.0.0.1 - - [30/Jan/2021:11:11:20 -0300] "POST /foo HTTP/1.1" 200 0 "-" "curl/7.47.0"

 

Синхронная запись файла

В некоторых случаях требуется синхронная запись. Например, надо убедиться, что данные записались, и только после этого выполнить какие-то следующие инструкции.

Синхронная запись осуществляется методом writeFileSync. Запишем второй лог в файл:

fs.writeFileSync('./access.log', log2);

 

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

Модуль fs предоставляет специальные методы для дополнения файлов, вместо перезаписывания. В некоторых случаях их использование более предпочтительно, так как упрощает читаемость кода.

Есть два метода — асинхронный и синхронный:

  1. appendFile(path, data[, options], callback) — асинхронное дополнение данных в файле. Если файла нет — он создаётся. По завершении записи вызывается переданный в метод коллбэк, где будет доступен объект ошибки, если она произошла в процессе записи файла.
  2. appendFileSync(path, data[, options]) — синхронное дополнение данных в файле. Как и в асинхронной версии, файл создаётся, если его нет.

Эти методы поддерживают те же параметры, что и writeFile/writeFileSync.

Потоки

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

В Node.js потоки — это абстрактный интерфейс для работы с такими данными. Его «абстрактность» заключается в предоставлении единого интерфейса для работы с данными в оперативной памяти, на диске и с данными из сети. Этот интерфейс позволяет обрабатывать данные небольшими порциями, не загружая полного объёма в оперативную память сразу.

Потоки используются во многих встроенных Node.js-модулях:

● http — для работы с обработкой запросов и ответов;

● fs — чтение или запись файлов;

● zlib — преобразование данных;

● в модулях шифрования;

● и других.

Без использования потоков практически невозможно обрабатывать большие файлы.

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

Стандартный объём памяти, выделяемый для Node.js-приложения:

● 700 Мб — для 32-битных систем;

● 1400 Мб — для 64-битных систем.

Таким образом, если мы разом прочитаем, синхронно или асинхронно, файл объёмом 2 Гб, то при стандартных настройках, на любой машине наша программа упадёт с ошибкой JavaScript heap out of memory. В подобных задачах и используются потоки, которые позволяют обрабатывать такие данные порционно.

В Node.js есть всего 4 вида потоков:

  1. Readable — поток на чтение.
  2. Writable — поток на запись.
  3. Duplex — поток на чтение и запись.
  4. Transform — подвид Duplex-потока, который изменяет данные.

Для работы с потоками в Node.js есть стандартный модуль streams. Каждый вид потока в Node.js представлен соответствующим классом в модуле streams.

Вид потока Класс
Readable stream.Readable
Writable stream.Writable
Duplex stream.Duplex
Transform stream.Transform

 

Примечательно, что все потоки — экземпляры класса EventEmitter, о котором мы говорили в прошлый раз. Благодаря этому, у каждого потока есть свой набор событий, и обработка этих событий позволяет нам работать с данными.

Поток на чтение

Как было сказано выше, инструментарий потоков используется также в модуле fs для чтения и записи данных в файл. Использование потоков вместо прямых методов чтения или записи позволяет:

● более гибко подходить к решению связанных с этим задач;

● управлять использованием памяти;

● записывать и считывать большие объёмы данных, не теряя в производительности.

Для чтения данных из файла, используя потоки, в модуле fs есть класс fs.ReadStream и метод fs.createReadStream. Причём метод fs.createReadStream — это функция-обёртка для создания экземпляра класса fs.ReadStream. В документации так и написано: Returns: <fs.ReadStream>. Убедиться в этом можно, заглянув в исходный код:

fs.createReadStream = function(path, options) { return new ReadStream(path, options); };

 

Таким образом, поток для чтения файла создаётся двумя способами:

  1. Создать вручную экземпляр класса fs.ReadStream.
  2. Вызвать функцию fs.createReadStream, которая сделает это за нас.

Разберём, какие параметры требуется передать в конструктор.

  1. path — путь к файлу, который надо прочитать.
  2. options — необязательный параметр. Если он передаётся, то может быть строкой или объектом. Строкой указывается только кодировка. Если же передавать объект, то подключаются следующие параметры:
    1. flags — знакомые по методам чтения или записи флаги для работы с файловой системой. Значение по умолчанию — ‘r’. Это означает открыть файл только для чтения, бросить исключение, если файла нет.
    2. encoding — кодировка содержимого файла. По умолчанию — null.
    3. fd — файловый дескриптор (неотрицательное целое число) или экземпляр класса-обёртки над файловым дескриптором FileHandle. Его отличие от файлового дескриптора в том, что он предоставляет API для работы с файлом. По умолчанию — null. Здесь мы не рассматриваем понятие «файловый дескриптор», но подробнее об этом — здесь.
    4. mode — этот параметр позволяет устанавливать специальные свойства файла, например, разрешение только для чтения. Значение по умолчанию — ‘0o666’ — файл доступен для чтения и записи любым пользователем.
    5. autoClose — это флаг. Если он выставлен в false, то при ошибке или событии ‘end’ дескриптор файла не закроется, и это станет обязанностью приложения. По умолчанию — true.
    6. emitClose — флаг, отвечающий за генерирование события ‘close’ при закрытии потока.
    7. start — неотрицательное целое число, обозначающее байт, с которого надо начать чтение включительно. Если он не определён, чтение станет происходить с текущей позиции файла.
    8. end — неотрицательное целое число, обозначающее байт, до которого нужно читать файл включительно. По умолчанию — Infinity.
    9. highWaterMark — параметр, влияющий на размер одной «порции» данных в потоке. По умолчанию — 64 Кб (64 * 1024).
    10. fs — объект, в котором передаются методы ‘open’, ‘read’ и ‘close’ для переопределения текущих методов ‘open’, ‘read’ и ‘close’. По умолчанию — null.

Посмотрим, как это работает в минимальной конфигурации. Создадим поток на чтение файла логов.

const readStream = fs.createReadStream('./access.log');

 

При создании объекта потока он подключается к источнику данных (к файлу ‘access.log’) и начинает считывать данные из него. Как только первая порция данных готова — генерируется событие ‘data’, и в него передаются данные. Таким образом, чтобы взаимодействовать с данными, требуется создать обработчик этого события.

readStream.on('data', (chunk) => { console.log('Chunk'); console.log(chunk); });

 

В этом обработчике выводятся считанные данные в консоль. Чтобы наглядно показать те самые «порции» данных, мы добавили console.log со словом ‘chunk’ (с англ. — часть).

При запуске программы появится сообщение в терминале:

Chunk <Buffer 31 32 37 2e 30 2e 30 2e 31 20 2d 20 2d 20 5b 33 30 2f 4a 61 6e 2f 32 30 32 31 3a 31 31 3a 31 30 3a 31 35 20 2d 30 33 30 30 5d 20 22 47 45 54 20 2f 73... 118 more bytes>

 

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

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

В терминале появится следующее:

Chunk <Buffer 31 22 20 32 30 30 20 30 20 22 2d 22 0a 31 32 37 2e 30 2e 30 2e 31 20 2d 20 2d 20 5b 33 30 2f 4a 61 6e 2f 32 30 32 31 3a 31 31 3a 31 31 3a 32 30 20 2d... 65486 more bytes> Chunk <Buffer 3a 31 35 20 2d 30 33 30 30 5d 20 22 47 45 54 20 2f 73 69 74 65 6d 61 70 2e 78 6d 6c 20 48 54 54 50 2f 31 2e 31 22 20 32 30 30 20 30 20 22 2d 22 0a 31... 65486 more bytes> Chunk <Buffer 2e 30 22 0a 31 32 37 2e 30 2e 30 2e 31 20 2d 20 2d 20 5b 33 30 2f 4a 61 6e 2f 32 30 32 31 3a 31 31 3a 31 30 3a 31 35 20 2d 30 33 30 30 5d 20 22 47 45... 10433 more bytes>

 

Теперь мы видим, что поток читает данные порционно.

Данные снова представлены классом Buffer. Чтобы превратить их в человеческий текст, достаточно указать кодировку при создании объекта потока на чтение.

const readStream = fs.createReadStream('./access.log', 'utf8');

 

Если чтение источника данных закончилось, то генерируется событие ‘end’. Его также можно обработать:

readStream.on('end', () => console.log('File reading finished'));

 

Если в процессе чтения файла произошла ошибка, то поток генерирует событие ошибки. В Node.js хорошей практикой считается обработка ошибок. Обработаем и мы.

readStream.on('error', () => console.log(err));

 

Итоговый код:

const fs = require('fs'); const readStream = fs.createReadStream('./access.log', 'utf8'); readStream.on('data', (chunk) => { console.log('Chunk'); console.log(chunk); }); readStream.on('end', () => console.log('File reading finished')); readStream.on('error', () => console.log(err));

 

В этом примере мы написали программу, считывающую данные из файлов любого объёма. Теперь пора научится эти данные записывать.

Поток на запись

Аналогично потоку на чтение данных, функциональность потока на запись в модуле fs представлена классом fs.WriteStream и методом fs.createWriteStream. Метод fs.createWriteStream также считается функцией-обёрткой для создания экземпляра класса fs.WriteStream. Чтобы убедиться в этом, заглянем в исходный код:

fs.createWriteStream = function(path, options) { return new WriteStream(path, options); };

 

Параметры, которые передаются в конструктор, в целом аналогичны параметрам при создании потока на чтение:

  1. path — путь к файлу, в который требуется записать данные.
  2. options — необязательный параметр. Если передаётся — может быть строкой или объектом. Строкой указывается только кодировка. Если же передавать объект, то подключаются следующие параметры:
    1. flags — флаги для работы с файловой системой. Значение по умолчанию — ‘w’. Означает открыть файл для записи. Если файла нет — он будет создан, а, наоборот — перезаписан.
    2. encoding — кодировка содержимого файла. По умолчанию — ‘utf8’.
    3. fd — файловый дескриптор (неотрицательное целое число) или экземпляр класса-обёртки над файловым дескриптором FileHandle. Его отличие от файлового дескриптора в том, что он предоставляет API для работы с файлом. По умолчанию — null. Здесь мы не рассматриваем понятие «файловый дескриптор», но подробнее об этом — здесь.
    4. mode — этот параметр позволяет устанавливать специальные свойства файла, например, разрешение только для чтения. Значение по умолчанию — ‘0o666’. То есть файл доступен для чтения и записи любым пользователям.
    5. autoClose — это флаг. Если выставлен в false, то при ошибке или событии ‘end’ дескриптор файла не закроется, и это станет обязанностью приложения. По умолчанию — true.
    6. emitClose — флаг, отвечающий за генерирование события ‘close’ при закрытии потока.
    7. start — неотрицательное целое число, обозначающее байт, с которого требуется начать чтение включительно. Если он не определён — чтение будет происходить с текущей позиции файла.
    8. fs — объект, в котором передаются методы ‘open’, ‘write’, ‘writev’ и ‘close’ для переопределения текущих методов ‘open’, ‘read’ и ‘close’. По умолчанию — null.

Добавим лог ещё одного запроса к серверу в файл, используя поток на запись.

Сначала создадим объект потока на запись, сразу указав кодировку:

const writeStream = fs.createWriteStream('./access.log', 'utf8');

 

Чтобы записать данные, используем метод write потока на запись:

writeStream.write(log1);

 

Наш файл снова перезаписался, и в нём опять только одна запись. Вспомним о флаге, который позволяет добавлять данные в файл, а не перезаписывать его полностью. Сразу добавим перенос строки.

const writeStream = fs.createWriteStream('./access.log', { flags: 'a', encoding: 'utf8' }); writeStream.write('\n'); writeStream.write(log1);

 

Теперь результат соответствует ожиданиям:

127.0.0.1 - - [30/Jan/2021:11:11:20 -0300] "POST /foo HTTP/1.1" 200 0 "-" "curl/7.47.0" 127.0.0.1 - - [30/Jan/2021:11:11:20 -0300] "POST /foo HTTP/1.1" 200 0 "-" "curl/7.47.0"

 

После окончания записи данных в файл закрываем поток:

writeStream.end(() => console.log('File writing finished'));

 

Итоговый код:

const fs = require('fs'); const log1 = '127.0.0.1 - - [30/Jan/2021:11:11:20 -0300] "POST /foo HTTP/1.1" 200 0 "-" "curl/7.47.0"'; const writeStream = fs.createWriteStream('./access.log', { flags: 'a', encoding: 'utf8' }); writeStream.write('\n'); writeStream.write(log1); writeStream.end(() => console.log('File writing finished'));

 



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2021-12-15 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: