Продолжение следует…

 

Я заметил, что мой блог таки читают. Это очень радует, что кому-то это приносит пользу. У меня просьба, напишите отзыв в поле для комментариев, на той странице, которая для вас оказалась полезна. Это помогло бы мне сделать блог лучше или удобней. Напишите, свое первое впечатление или мнение. В общем если понравилось пишите, если не понравилось, тем более пишите, будем исправлять.
Спасибо)

 

С Уважением
Konstantin F.

27. Чат через long-polling, чтение POST

Цель этой главы, научиться делать чат на Node.JS. Для начала, наш чат будет достаточно простой, всего лишь каждый кто заходить по этому url — localhost:3000 автоматически попадает в комнату, в которой получает сообщения. Например я набираю что то в одном окне браузера и то что я набрал появляется и в другом окне браузера, в данном случае оба браузера на одном компьютере но могут быть и на разных краях света.

screenshot_27_01

Вот такой чат, просто обмен сообщениями. Мы будем делать его в начале без пользователей, без базы данных, без авторизации, такой вот простой, но качественно созданный чат. И так, поехали. Для начала как оно в общем будет устроено. Алгоритм общения с сервером, который изображен на этой схеме

screenshot_27_02

Называется «long polling», что в переводе длинные запросы. Он с одной стороны очень простой, с другой стороны в девяносто процентах задач, когда нужно общаться с сервером, он отлично подходит. Посмотрим на него повнимательней. Когда клиент хочет получать данные от сервера, то он отправляет на сервер XMLHttpRequest, самый обычный запрос, но не обычной является его обработка сервером. Сервер, получив такой запрос, не будет сразу на него отвечать, а просто оставит запрос подвисшим, дальше в будущем, как только появятся данные для клиента, сервер ответит на этот запрос, клиент получит ответ, какое то сообщение, обработает его выведет сообщение и сделает новый запрос на сервер, сервер опять, если данных нет то подождет, подождет, как только данные появятся, тут же ответит. Фактически получается, что клиент все время старается держать рабочее соединение к серверу, по которому, как только данные будут готовы, он их сразу же получит. Соответствующий код на стороне клиента, выглядит так

Есть форма, для отправки сообщений

И есть список, «messages», куда сообщения приходят

При submit формы, создается XMLHttpRequest и сообщение обычным порядком постится на сервер

Ну а для получения новых сообщений, как раз используется алгоритм long polling описанный ранее. Есть функция subscribe()

которая запускает XMLHttpRequest и говорит получи ка  данные с этого url — xhr.open(«GET», «/subscribe», true); Когда будет получен ответ с сервера, он показывается в  виде сообщения и заново вызывается функция subscribe();

то есть делается новый запрос. И так все это идет по кругу.

Исключение, если произошла ошибка или что то не так, в этом случае мы subscribe() тоже заново отправим, но с небольшой задержкой, чтобы не завалить сервер —

Обратим внимание, что этот код реализует алгоритм long polling, он не привязан к какому то конкретному чату, это просто код подписки на сообщение сервера, его можно расширять, можно добавлять различные каналы получения сообщений и так далее, но сейчас пока мы этого делать не будем, а перейдем к Node.JS.

Для серверной части у нас так же есть не большая заготовка

которая представляет собой Http server, умеющий отдавать index.html в качестве главной страницы. Так же будут два url, вот такой — ‘/subscribe’, для подписки на сообщение, и такой — ‘/publish’, для отправки сообщений. Они в точности такие же, какие вы видели в index.html.

Начнем реализацию с подписки. Функция subscribe, со странички index.html, будет отправлять длинные запросы именно на url ‘/subscribe’. Клиент, который отправил запрос на subscribe, с одной стороны не должен получить ответ прямо сейчас, с другой стороны мы должны запомнить, что он обратился за данными, чтобы потом, когда данные появятся, ему их передать. Для решения этой задачи создадим специальный объект который будет называться «chat» и «chat.subscribe» будет запоминать что пришел клиент, для этого мы передадим ему объекты req и res — «chat.subscribe(req, res)». Ну а «chat.publish(«….»)» будет пересылать это сообщение всем клиентам которые сейчас есть. Описывать этот объект chat я буду в отдельном модуле который расположу в текущей директории.

26. Writable поток ответа res, метод pipe

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

Пример решения этой задачи без потоков может быть таким

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

а если все хорошо, то ставим заголовок, чтоб указать какой это файл

и записываем содержимое файла в ответ вызовом

который отдает content и завершает соединение.

Это решение в принципе работает, но его проблема, это пожирание памяти. Потому что, если файл большой, то  readFile его сначала считает, а потом вызовет callback, в результате получится, что если клиент медленный, то весь этот считанный content зависнет в памяти до того пока клиент его получит. А что если у нас таких медленных клиентов много? А если файл очень большой? Получается, что сервер может почти мгновенно занять всю доступную память, что конечно же совершенно не приемлемо. Чтобы такого не происходило, мы заменим код отдачи файла на принципиально другой, использующий потоки.

Мы уже умеем читать из файла используя ReadStream, это будет входным потоком данных, а выходным будет объект ответа «res», который является  объектом класса ServerResponse наследующим от stream.Writable. Общий алгоритм использования потоков для записи сильно отличается от того, что мы рассматривали ранее и выглядит так

screenshot_26_01

В начале мы создаем объект потока, если у нас http.Server, то этот объект уже создан, это res. Дальше мы хотим отправить что то клиенту, это можно сделать вызовом res.write и передать там наши данные, обычно это либо буфер, либо строка. Наши данные при этом добавляются к специальному свойству потока, которое называют его буфером. Если, пока, этот буфер не очень большой, то данные прибавляются к нему и write возвращает true, что означает, что мы можем писать еще, при этом обязательство по отсылке данных берет на себя поток, как правило эта отсылка происходит асинхронно. Возможен другой вариант, например если мы передали очень много данных или если буфер уже был чем то занят, то метод write() может вернуть false. False означает, что внутренний буфер потока переполнен и прямо сейчас запись конечно можно сделать, но это будет не целесообразно, потому что в буфере все будет просто копиться, копиться, копиться, по этому при получении false, обычно запись не продолжают, а ждут специального события «drain», которое будет сгенерировано потоком когда он все отошлет, то есть когда его внутренний буфер опустеет. Таким образом мы можем вызывать write много, много раз и когда мы понимаем, что всё, все данные записаны, то  мы должны вызвать метод end(), тут тоже можем передать с первым аргументом данные — end([data]), в этом случае он просто write вызовет, самая главная задача end() это закончить запись. Поток это делает, при необходимости вызывает внутренние операции закрытия ресурсов, то есть файлов, соединений и так далее и затем генерирует событие finish, что означает, запись полностью завершена. Обращаю ваше внимание, что аналогичное событие у stream.Readable называется end(), это различие не случайно, потому что есть потоки дуплекс, которые умеют и читать и писать соответственно они могут генерировать как одно событие и другое.

Поток в любой момент можно разрушить вызовом метода destroy(), при вызове этого метода, работа потока прекращается и все ассоциированные с ним ресурсы будут освобождены. Конечно же событие «finish» уже никогда не состоится, потому что finish, это успешное окончание работы потока. Успешная отдача всех данных.

Реализуем успешную передачу всех данных используя эту схему

screenshot_26_02

Я буду делать это в отдельной функции, которая будет называться sendFile(), она будет принимать один поток для файла и второй поток для ответа.

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

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

В этом небольшом коде изложен пример более универсального решения этой задачи

мы тоже читаем содержимое из файла на событии readable, но мы не просто отправляем его вызовом res.write(… ), а еще и анализируем, что этот вызов вернет. Если res принимает данные очень быстро, то res.write(…. ) будет возвращать true, это означает, что эта ветка if никогда не выполнится

соответственно мы получим read, write, read, write и так далее. Более интересный случай когда res.write(… ) вернул false, то есть когда буфер переполнен, в этом случае, мы временно отказываемся обрабатывать события readable на файле

Само по себе, такое снятие обработчика не означает, что файловый поток перестанет читать данные, он будет читать данные, но он дочитает до определенного уровня, заполнит свой внутренний буфер объекта файл и затем, так как никто read не вызывает, то этот внутренний буфер останется заполненным на определенном уровне.  То есть файловый поток что то считает и там застопорится, далее мы дождемся событие ‘drain’, то есть когда данные будут успешно отданы в ответ и когда данные отданы в ответ, это означает, что мы можем принять что то еще из файла, мы вновь показываем свой интерес в событиях ‘readable’ и вызываем метод write() сразу. Сразу, потому что пока мы ждали события ‘drain’ новые данные могли прийти, это означает, что имеет смысл их тут же прочитать, вызов read() вернет null если данных нет, ну а если есть, то они просто будут обработаны, тем же способом, о котором мы говорили раньше.

Такая вот своеобразная рекурсивная функция получается — считать, отправить то что считано, при необходимости подождать ‘drain’, считать дальше, отправить, подождать и так далее по циклу, пока файл не закончится. По окончанию файла наступит событие ‘end’ в обработчике которого мы завершим ответ, вызовом res.end(), таким образом будет закрыто исходящее соединение, потому что файл полностью отослан.

Получившийся код является весьма универсальным, он реализует достаточно общий алгоритм отправки данных из одного потока в другой, используя самые стандартные методы потоков readable и writable. Об этом конечно же подумали и разработчики самого Node.JS и добавили его несколько более оптимизированную реализацию в стандартную библиотеку потоков. Соответствующий метод называется pipe(), он есть у всех readable потоков и работает так —

screenshot_26_03

Кроме того, что это всего лишь одна строка тут есть еще один бонус, например можно один и тот же входной поток «пайпить» в несколько выходных, например кроме ответа клиенту, будем выводить его еще в стандартный вывод процесса. Давайте запустим такой код

screenshot_26_04

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

Что же, мы теперь немножко ближе к реальной жизни и в ряде руководств такой код выдается за вполне нормальный, но на самом деле это не так, ставить такой код на живой сервер ни в коем случае нельзя. В чем же дело? для того чтобы продемонстрировать проблему я сейчас добавлю дополнительные обработчики на события open и close для файла.

стартую и обновляю в браузере страницу, обновляю, обновляю несколько раз

screenshot_26_05

Видите файл перезагружается и совершенно нормально, то что файл открывается, потом он целиком отдается и закрывается. Теперь я открою консоль и запущу утилиту curl которая будет скачивать вот этот url — http://localhost:3000/big.html с ограничением скорости в один килобайт в секунду.

Давайте поставим эту утилиту. Для этого ищем в гугле curl

screenshot_26_06

раз википедия, так википедия, там находим официальный сайт — https://curl.haxx.se/ и на нем во вкладке download в низу списка находим нашу винду и качаем архив

screenshot_26_07

Распаковав архив в корень диска С: в директорию curl  мы сначала устанавливаем сертификат безопасности кликнув по нему дважды, а потом прописываем в глобальную переменную path путь ко второму файлу в этой папке, к curl.exe, это нужно, чтоб я мог обращаться к этой утилите из консоли, то есть дописываем в path путь к папке, в которой находится наша утилита.

Продолжим, напомню у нас запущен сервер pipe.js и следим за консолью IDE, открыли окно команд в Windows, и вводим следующую команду

Запустили, и открывается файл начинается получение, с виду все хорошо, но если нажать Ctrl+C, то есть прекратить загрузку, мы не увидим в консоле нашей IDE  ни какого close

screenshot_26_08

видите, я трижды начинал загрузку и прерывал, ни одного закрытия. Для того чтоб повторить эксперимент файл должен быть более 3Mb.  Иначе говоря, если клиент открыл соединение, но закрыл его до того как загрузка файла была завершена, то получается что файл останется подвисшим. А если файл остался открытым, то во первых все ассоциированные  с ним структуры остались тоже в памяти, во вторых в операционных системах зачастую есть лимит на количество одновременно открытых файлов. А в третьих вместе с файлом навечно зависает в памяти и соответствующий объект потока, а вместе с ним и все замыкание в котором он находится. Чтобы избежать этой проблемы и следствий, достаточно всего лишь отловить момент, когда соединение закрыто и при этом удостовериться, что файл тоже будет закрыт. Событие которое нас интересует, называется res.on(‘close’, ….) и это событие отсутствует в обычном Stream.writable, то есть это именно расширение стандартного интерфейса потоков, так же как у файла есть close — file.on(‘close’, …), так и у объекта ответа server response тоже есть close — res.on(‘close’, ….). Но смысл последнего сильно отличается от первого, это очень важно, потому что на файловом потоке ‘close’ это нормальное завершение, файл закрывается всегда в конце, а для объекта ответа, ‘close’ это сигнал о том, что соединение было оборвано, при нормально завершении происходит не ‘close’, а ‘finish’. Итак, если соединение было оборвано, то нам нужно закрыть файл и освободить все ресурсы, поскольку файл нам больше передавать некому, для этого мы вызываем метод потока file.destroy(); теперь все будет хорошо. Теперь давайте еще раз проверим

screenshot_26_09

Теперь наш код можно пускать на живой сервер.

25. Потоки данных в Node.JS, fs.ReadStream

Тема этой главы, потоки в Node.JS. Мы постараемся разобраться в этой теме хорошо и подробно, по сколько, с одной стороны , так получается, что потоки в обычной браузерной JavaScript разработке отсутствуют, а с другой стороны, уверенное владение потоками необходимо для грамотной серверной разработке, по скольку поток, является универсальным способом работы с источниками данных, которые используются повсеместно.

Можно выделить два основных типа потоков.

Первый поток — stream.Readable — чтение.
stream.Readable это встроенный класс, который реализует потоки для чтения, как правило сам он не используется, а используются его наследники. В частности для чтения из файла есть fs.ReadSream. Для чтения запроса посетителя, server.on(‘request’, …req…), при его обработки, есть специальный объект, который мы раньше видели под именем req, первый аргумент обработчика запроса.

Второй поток — stream.Writable — запись.
stream.Writable это универсальный способ записи и здесь тоже, сам stream.Writable обычно не используется, но используются его наследники.
…в файл: fs.WriteStream
…в ответ посетителю: server.on(‘request’, …res…)

Есть и некоторые другие типы потоков, но наиболее востребованные это предыдущие два и производные от них.

Самый лучший способ разобраться с потоками это посмотреть как они работают на практике. Поэтому сейчас мы начнем с того, что используем fs.ReadStream для чтения файла.

Итак, здесь я подключаю модуль fs  и создаю поток. Поток это JavaScript объект, который получает информацию о ресурсе, в данном случае путь к файлу — «__filename» и который умеет с этим ресурсом работать. fs.ReadStream реализует стандартный интерфейс чтения который описан в классе stream.Readable. Посмотрим его на схеме

screenshot_25_01

Когда создается объект потока — «new stream.Readable», он подключается к источнику данных, в нашем случае это файл, и пытается начать из него читать. Когда он что то прочитал, то он эмитирует событие — «readable», это событие означает, что данные просчитаны и находятся во внутреннем буфере потока, который мы можем получить используя вызов «read()». Затем мы можем что то сделать с данными — «data» и подождать следующего «readable» и снова если придется, и так дальше. Когда источник данных иссяк, бывают конечно источники которые не иссякают, например датчики случайных чисел, но размер файла то ограничен, поэтому в конце будет событие «end», которое означает, что данных больше не будет. Так же, на любом этапе работы с потоком, я могу вызвать метод «destroy()» потока. Этот метод означает, что мы больше не нуждаемся в потоке и можно его закрыть, и закрыть соответствующие источники данных, полностью все очистить.

А теперь вернемся к исходному коду. Итак здесь мы создаем ReadStream

и он тут же хочет открыть файл. Но тут же, в данном случае вовсе не означает на этой же строке, потому что как мы помним, все операции с вводом выводом, реализуются через «LibUV», а «LibUV» устроено так, что все синхронные обработчики ввода вывода сработают на следующей итерации событийного цикла, то есть заведомо после того, как весь текущий JavaScript закончит работу. Это означает, что я могу без проблем навесить все обработчики и я твердо знаю что они будут установлены до того как будет считан первый фрагмент данных. Запускаю этот код и смотрим, что вывелось в консоле

screenshot_25_02

Первое сработало событие ‘readable’ и оно вывело данные, сейчас это обычный буфер, но я могу преобразовать его к строке используя кодировку utf-8 обычным вызовом toString

screenshot_25_03

Еще один вариант, указать кодировку непосредственно при открытии потока

screenshot_25_04

тогда преобразование будет автоматическим и toString() нам не нужен.

Наконец когда файл закончился,

то событие ‘end’ вывело мне в консоль «THE END». Здесь фай закончился почти сразу, поскольку он был очень маленький. Сейчас я не много модифицирую пример, сделаю вместо «__filename», то есть вместо текущего файла, файл «big.html», который в текущей директории находится.

screenshot_25_05

Файл big.html большой, по этому событие readable срабатывало многократно и каждый раз мы получали очередной фрагмент данных в виде буфера. Так же обратите внимание на вывод null который нас постоянно преследует, о причине этого вывода вы можете прочесть в документации, там сказано, что после того как данные заканчиваются readable возвращает null. Возвращаясь к нашему буферу, давайте я выведу в консоль его размер и заодно сделаю проверку на но то чтоб вывод был не null

screenshot_25_06

Эти числа, не что иное как длина прочитанного фрагмента файла, потому что поток когда открывает файл, он читает из него не весь файл конечно же, а только кусок и помещает его в свою внутреннюю переменную и максимальный размер, это как раз шестьдесят четыре килобайта. Пока мы не вызовем stream.read(), он дальше читать не будет. После того как я получил очередные данные, то внутренний буфер очищается и он может еще фрагмент прочитать, и так далее и так далее, последний фрагмент имеет длину остатка данных. На этом примере мы отлично видим важное преимущество использования потоков, они экономят память, какой бы большой файл не был, все равно, единовременно мы обрабатываем вот такой небольшой фрагмент. Второе, менее очевидное преимущество, это универсальность интерфейса. Здесь

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

Потому что потоки это в первую очередь интерфейс, то есть в теории, если наш поток реализует необходимые события и методы, в частности наследует от stream.Readable, то все должно работать хорошо, но это конечно же только в том случае если мы не использовали специальных возможностей, которые есть у файловых потоков. В частности у потока ReadStream есть дополнительные события

Здесь изображена схема именно для fs.ReadStram и новые события изображены красным

screenshot_25_07

Вначале это открытие файла, а в конце закрытие. Обратим внимание, что если файл полностью дочитан, то возникает событие «end» затем «close», а если файл не дочитан, например из за ошибки или при вызове метода destroy(), то «end» не будет, поскольку файл не закончился, но всегда гарантируется, при закрытии файла, событие «close».

И наконец, последняя по коду, но не последняя по важности деталь, обработка ошибок. Например посмотрим что будет если файла нет

screenshot_25_08

упс, все упало. Обратите внимание, потоки наследуют от event EventEmitter,  про него была глава, если происходит ошибка, то весь процесс node.js падает. Это в том случае конечно, если  на эту ошибку нет обработчиков, по этому если мы хотим, чтоб Node.JS вообще не упал, то нужно обязательно обработчик поставить

screenshot_25_09

Итак, для работы с источниками данных в Node.JS используются потоки, здесь мы рассмотрели общую схему по которой они работают

screenshot_25_10

и ее конкретную реализацию, а именно fs.ReadStrem

screenshot_25_11

которая умеет читать из файла.

24. Безопасный путь к файлу в fs и path

В этой главе мы рассмотрим, как при помощи Node.JS создать веб сервер, который будет возвращать файл юзеру из директории public. Может возникнуть вопрос, зачем здесь Node.JS? Почему бы не сделать это на другом сервере, например Nginx. Вопрос совершенно уместен, да для отдачи файлов, как правило, другие сервера будут более эффективны. С Другой стороны node, во первых, работает тоже весьма неплохо, а во вторых, может, перед отдачей файла, совершить какие то интеллектуальные действия. Например обратится к базе данных, проверить имеет ли юзер право на доступ к  файлам, и только если имеет, тогда уже отдавать.

Итак начинаем.

screenshot_24_05

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

http.createServer(… ) здесь очень прост

он будет проверять, есть ли доступ к данному файлу

и если есть, отдавать

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

которая будет парсить url и если есть параметр ‘secret’, который равен ‘o_O’, то считается, что доступ есть. В реальной жизни такая проверка будет производиться, конечно же при помощи cookie, базы данных и так далее. Основная функция, которая нас здесь интересует, это sendFileSafe(…. ). Именно эта функция должна, получив, в качестве первого параметра, путь от юзера — «url.parse(req.url).pathname» отослать соответствующие файлы из директории «public», учитывая поддиректории. И важнейший аспект, который в ней должен быть заложен, это безопасность. Какой бы путь не передал юзер, он ни в коем случае не должен получить файл вне этой директории. Например, вот такое обращение

screenshot_24_01

должно возвращать файл index.html  и картинка здесь взята из директории deep\nodejs.jpg

screenshot_24_02

А если бы я не указал seceret=o_O, то оно должно было выдать мне ошибку с кодом 403

screenshot_24_03

Ну а если я попробовал указать вот так вот

screenshot_24_04

тоже ошибка. И так для любых попыток выйти за пределы директории.

Итак смотрим функцию sendFileSafe(filePath, res), чтобы получить пример безопасной работы с путем от посетителя.

Эта функция состоит из нескольких шагов. На первом шаге я пропускаю путь через decodeURIComponent(filePath),

ведь по стандарту http многие символы кодируются, в частности русская буква «я» будет иметь вот такой вид в url -«%D1%8F» и это корректно. Получив такой url мы обязаны его декодировать обратно в русскую букву «я» при помощи вызова decodeURIComponent(…. ), при этом если url закодирован неверно, то возникнет ошибка, которую необходимо поймать и обработать. В catch мы как раз указываем, resStatusCode = 400, что означает, что url некорректен, запрос неверен, можно конечно и просто вернуть res.statusCode = 404.

Далее когда мы раскодировали запрос, время его проверить

есть такой специальный нулевой байт, который, по идеи, в строке url присутствовать не должен. Если он есть, это означает, что кто то его злонамеренно передал, потому что некоторые встроенные функции Node.JS будут работать с таким байтом некорректно. Соответственно, если такой байт есть, то мы тоже возвращаем- до свидание, запрос некорректен.

Теперь настало получить полный путь к файлу на диске. Для этого мы будем использовать модуль path.

Этот модуль содержит пачку самых разных функций для работы с путями. Например join объединяет пути, normalize — удаляет из пути, всякие странные вещи типа «.» «..» «\\» и так далее, то есть делает путь более корректным. Если url который передал юзер выглядел так — «/deep/nodejs.jpg», то после join  с ROOT, который представляет собой вот эту — «var ROOT = __dirname + «\\public»» директорию, он будет выглядеть уже по другому — «C:\node\server\public\deep\nodejs.jpg»

Наша следующая задача это убедится, что путь действительно находится внутри директории public. Сейчас, когда у нас уже есть абсолютно точный, корректный абсолютный путь, это сделать очень просто — достаточно всего лишь проверить, что в начале находится вот такой вот префикс —  «C:\node\server\public\» то есть, что путь начинается с ROOT. Проверяем и если это не так, то до свидание файла нет

Далее, если путь разрешен, то проверим, что по нему лежит. Если ничего нет, то fs.stat вернет ошибку ну или если даже ошибки нет, то нужно проверить файл ли это

В том случае если это не файл — ошибка, ну а если файл, то все проверено, там файл, надо его отослать. Это делает вложенный вызов sendFile(…. ).

sendFile(…. ), функция которая есть в этом же файле чуть чуть ниже.

Она для чтения файла использует вызов fs.readFile(…. ) и когда он будет прочитан, то выводит его через res.end(…). Обращаю ваше внимание вот на что, во первых ошибка в этом callback очень мало вероятна, хотя бы потому что мы уже проверили, что файл есть, это действительно файл, то есть его можно отдать, но тем не менее мало ли что, например может возникнуть ошибка при чтении с диска, так или иначе, как то обработать ошибку надо — «if (err) throw err» .

Далее, мало просто считать содержимое файла и отправить его, ведь различные файлы должны снабжаться различными заголовками contant-type — «res.setHeader(‘Content-Type’, mime + «; charset=utf-8″)». Например html файл должен иметь тип text/html, файл с картинкой jpg — image/jpg и так далее. Нужный тип файла определяется по расширению с использованием модуля «mime», для того чтоб это работало, нужно его поставить дополнительно «npm install mime», и затем вызвать.

Ну и на конец последнее: эта глава была сосредоточена на том, чтобы корректно работать с путем от посетителя, чтобы сделать все необходимые проверки, но что касается отдачи файла, этот код не верен, я про функцию sendFile(… ), потому что readFile полностью прочитывает файл и потом в content его отсылает. А представьте, что будет если файл очень большой, а если он превышает количество свободной памяти, вообще же все упадет. По этому для того чтобы отсылать файл нужно либо дать команду специализированному серверу, либо использовать потоки которые мы рассмотрим в следующих главах.

23. Работа с файлами, модуль fs

Цель этой главы, научить нас работать с бинарными данными и файловой системой. В Node.JS, для работы с файлами существует модуль «FS» и в нем есть множество функций для самых различных операций с файлами и директориями. Вот документация. Если мы приглядимся внимательно, то увидим первую особенность этого модуля, почти все функции имеют два варианта.

screenshot_23_01

Первое просто имя, второе со словом Sync. Слово Sync означает синхронно.Если я например вызову fs.readFile(file[, options], callback), то он сначала прочитает файл полностью, а потом вызовет callback. А fs.readFileSync(file[, options]) затормозит выполнение процесса пока файл не будет прочитан. По этому, как правило синхронный вызов используют либо в консольных утилитах, либо на стадии инициализации сервера, когда такие тормоза допустимы. А асинхронный вызов, в тех случаях когда хочется, чтоб полноценно работал событийный цикл, то есть, чтоб Node.JS не ждал пока диск сработает, медленно и файл прочитается.

Посмотрим на реальный пример использования.

Здесь я подключаю модуль «fs»  и вызываю, асинхронно, функцию readFile(…). Эта функция принимает имя файла, в данном  случае «__filename» это путь к текущему файлу модуля, и получает callback, первый аргумент, как всегда, ошибка, второй данные, то есть, содержимое файла. Если бы это был синхронный вызов, то это выглядело бы так

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

и вот, что я получаю в качестве вывода.

screenshot_23_02

Обратите внимание, вывелось не содержимое файла в виде строки, а специальный объект буфер. Этот объект буфер является высокоэффективным средством Node.JS для работы с бинарными данными. Технически буфер, это непрерывная область памяти, которая, в данном случае, заполнена этими данными. И работа с буфером достаточно похожа на работу со строкой. То есть можно взять, например, и получить нулевой элемент.

Можно взять и получить длину буфера

Но в отличии от строк, которые в JavaScript абсолютно неизменяемы, содержимое буфера можно менять. Для этого в документации предусмотрено ряд методов, от простейшего метода buf.write(string[, offset[, length]][, encoding]) , который пишет в буфер строку, преобразуя ее в бинарный формат, учитывая  данную кодировку и заканчивая различными методами которые записывают в буфер целые числа, дробные числа, числа в формате double и другие числа, учитывая внутреннее, компьютерное, двоичное представление данных форматов

screenshot_23_03

В данном случае, мы бы хотели вывести содержимое файла в виде строки. По этому давайте преобразуем буфер в строку, это можно делать вызовом toString и в скобках указать кодировку, то есть таблицу, которая указывает как преобразовать байты в символы алфавита. Обычно кодировка по умолчанию — это ‘utf-8’. Если хотим так и оставить,  то можно не указывать. Запускаем

screenshot_23_04

Ну вот, теперь строка.

Если точно знаю, что я работаю со строками, то я могу указать кодировку прямо здесь, это будет выглядеть так

screenshot_23_05

В этом случае, преобразование в строку происходит непосредственно внутри функции fs.readFile(…. ).

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

screenshot_23_06

О! Вывелась ошибка в консоль Error. Обращаю ваше внимание, что в ошибке есть следующие данные:

Во первых, имя ошибки — code: ‘ENOENT’, в данном случае  означает, что файла нет.
Во вторых это код цифровой — errno: -4058, и оба кода являются полностью кроссплатформенными, то есть не важно, под Windows, под Linux, еще под чем то я нахожусь, всегда если файл не найден, то это означает ошибка ‘ENOENT’. Соответственно мы можем проверить если код такой

то обработать его определенным образом, а иначе сделать что то еще.

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

Если мы заведомо знаем, что файл может не существовать, то мы можем проверить его при помощи специального вызова. Для этого есть вызов fs.stat(path, callback) и различные его варианты, которые вы можете более подробно изучит в документации. Как  правило в большинстве ситуаций подходит просто stat. Он получает путь и возвращает объект специального типа fs.Stats, который содержит подробную информацию о том, что по нему находится. Вот пример его использования

запускаю

screenshot_23_07

console.log первый, вывело true, по тому что это файл, а второй вывел полную информацию о том, что такое находится по данному пути, это немножко зависит от операционной системы, от файловой системы, но практически всегда есть размер — size, а также модификация — mtime и дата создания — ctime.

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

Обратите внимание, что в каждом callback я проверяю ошибку, то есть после того как файл создан, я обязательно проверяю, если есть ошибка, нужно ее как то обработать, самый простейший способ это throw. Везде, в каждом callback должна быть обработка ошибок. Потому что ошибки могут быть в самых непредсказуемых местах.

Итак мы кратко познакомились с основными возможностями модуля «FS» и с некоторыми примерами их применения. Вообще же  у этого модуля действительно очень много методов, я рекомендую посмотреть их в документации, просто, чтоб понимать, что вообще существует.

22. Таймеры, process.nextTick, ref/unref

Всем привет. Эта глава посвящена таймерам в Node.JS. В ней я постараюсь в первую очередь рассказать о тех различиях, которые есть между таймерами в браузерах и в Node.JS. Для этого я открыл документацию

screenshot_22_01

и мы видим, что несколько методов здесь очень похожи — setTimeout(), setInterval(), clearTimeout(), clearInterval(), работают практически одинаково, что в Node.JS, что в браузерах. А вот дальше уже начинаются различия. И первое отличие, мы посмотрим на примере такого вот сервера.

Как видим сервер этот очень простой, это можно сказать абстрактный http сервер который слушает порт 3000 и что то там делает, неважно что, с запросами. В определенный момент, например через две с половиной секунды, мы решаем прекратить функционирование этого сервера. При вызове server.close() сервер прекращает принимать новые соединения, но пока есть принятые, но не оконченные запросы они еще будут обрабатываться и только когда все соединения будут обработаны и закрыты, тогда процесс прекратится. В данном случае, я запускаю наш сервер, предварительно создав новую конфигурацию для запуска ref.js в качестве сервера, и если никаких запросов нет, то через две с половиной секунды процесс завершится. Все пока понятно, все предсказуемо.

screenshot_22_02

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

Каждую секунду выводим, есть специальный вызов «process.memoryUsage()». Итак запускаем и смотрим,

screenshot_22_03

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

Как мы помним, за таймеры, за события ввода-вывода отвечает библиотека LibUV и пока есть активный таймер LibUV не может завершить процесс. Что делать? Давайте рассмотрим несколько решений.
Первое решение, это  сделать callback функции close, которая сработает когда сервер полностью закроет и обработает все соединения и в нем написать «process.exit()»

Давайте ка попробуем

screenshot_22_04

работает. С одной стороны нормально, с другой, как то это слишком уж брутально, это просто жесткое прибивание процесса. Давайте чуть чуть мягче, сделаем «clearInterval(timer)», будем очищать конкретно вот этот таймер

ну ка

screenshot_22_05

тоже все хорошо, но архитектурно и это решение не самое лучшее. Потому что, подумаем, вот сервер

он может быть в одном файле, а этот setInterval(),

он может быть, вообще, в другом модуле. А может быть еще и третий модуль, который тоже держит какой то свой сервер или осуществляет какие то свои операции. Что ж clearInterval(). Теперь просто остановится вывод этих сообщений, но другие операции продолжат выполняться, это чуть лучше, но все равно не так хорошо.

На самом деле, правильное решение будет в использовании специализированных возможностей Node.JS. А именно, вот здесь я оставлю close() как и было, а для setInterval(), я использую специальный метод, который называется timer.unref();

Как видим, в отличии от браузерного JavaScript, здесь timer это объект, и метод unref() указывает LibUV, что этот timer является второстепенным, то есть его не следует учитывать при проверки внутренних watcher на завершение процесса. Давайте я запущу

screenshot_22_06

И теперь, как только серверы закончат работу, то есть, как только не останется никаких других внутренних watcher кроме вот этого timer который timer.unref(), процесс, как видим, завершился.
Есть еще метод ref(), он является противоположенным unref(), то есть если я сделал timer.unref(), потом передумал и вызвал timer.ref()то выполнение не прервется, как будто unref() не было.

screenshot_22_07

На практике ref() используется очень редко. Почему это решение лучше? Да просто потому, что здесь timer просто указывает, что он не важен, что по сути нам и требуется. Никаких побочных эффектов это не несет.

Обращаю ваше внимание, что метод unref() есть не только у timer, он есть кроме timer еще например у серверов — server.unref() или у сетевых сокетов — socket.unref(). То есть я могу сделать сетевое соединение, которое тоже не будет препятствовать завершению процесса, если оно почему то не важно.

Далее мы видим методы «setImmediate(callback[, arg][, …])» и «clearImmediate(immediateObject)»

screenshot_22_08

Они тоже отличаются от браузерных. Для того чтобы лучше это понять рассмотрим следующий пример. У нас есть веб сервер и  там в функции обработчике запроса понадобилось выполнить какую то операцию асинхронно.  В браузере для этого обычно используется либо setTimeout(f,0), либо setImmediate или его эмуляция различными хаками, но обращаю ваше внимание в браузере немножко по другому работает событийный цикл и setImmediate браузерный, немножко не тот, мы сейчас его обсуждать не будем. Посмотрим, что в ноде происходит с setTimeout(f,0), когда сработает этот код. Можем ли мы гарантировать, что он выполнится до того как придет следующий запрос. Конечно же нет! setTimeout() выполнит его в ближайшее время, но совершенно не понятно может быть до следующего запроса, а может и после.

Однако, есть такие ситуации когда мы должны четко знать, что некий асинхронный код выполнится до того как в ноду придет следующий запрос или вообще любое следующие событие ввода вывода. Например потому что мы хотим повесить обработчик, скажем у нас есть req и мы хотим повесить на него, в этом setTimeout(), обработчик на следующие данные и мы должны точно знать, что этот обработчик повесится, до того как эти следующие данные будут просчитаны.

Для решения этой задачи в ноде есть специальный вызов — process.nextTick().

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

То есть вот здесь будет это выполнение

screenshot_22_09

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

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

screenshot_22_09

мы можем его попробовать разбить на части. Одну часть запустить тут же, а другую запустить так, чтоб она заработала на следующей итерации этого цикла, и другая на следующей и так далее. Для реализации этого в Node.JS есть метод — setImmediate(callback[, arg][, …]). Этот вызов как раз  и планирует вызов функции так, чтоб она с одной стороны сработала как можно скорее, с другой стороны на следующей итерации цикла, после обработки текущих событий.

Рассмотрим отличия между nextTick() и setImmediate(callback[, arg][, …]) на конкретном примере.

Здесь я использую модуль «fs»  для того чтобы открыть файл, открытие файла здесь просто как вариант операции ввода вывода. Когда файл будет открыт, то внутреннее событие LibUV  которое вызовет эту функцию.

И Дальше я через setImmediate и process.nextTick планирую вывод сообщений. Посмотрим в каком порядке они выведутся. Создам еще одну конфигурацию сервера — io.js и запускаю

screenshot_22_10

Итак, сначала конечно же вывелась nextTick, потому что nextTick планируется по окончанию текущего JavaScript, но до любых событий ввода вывода, то есть до реально открытия файла.  setImmediate сработала до вводы вывода, потому что она так запланировала выполнение. А если бы я сюда добавил setTimeout(f,0), где был бы он, а вот неизвестно, может быть и здесь, а может быть и здесь гарантий нет.

Итак мы рассмотрели, чем timer в Node.JS отличаются от браузерных,

1.Это влияние на завершение процесса и методы ref(), unref().
Есть различные setTimeout(f,0) —
2. process.nextTick(f) = setTimeout(f,0) до I/O
3. setImmediate(f) = setTimeout(f,0) после I/O
В большинстве ситуаций используется process.nextTick(f), он гарантирует, что выполнение произойдет до новых событий, в частности до новых операций ввода вывода, до новых данных, как правило это наиболее безопасный вариант. Ну а setImmediate(f) планирует выполнение на следующую итерацию цикла, после обработки событий. Как правило это нужно тогда когда нам без разницы обработаются какие события или нет, то есть мы хотим что то сделать асинхронно и нам не хочется лишний раз тормозить событийный цикл, либо при разбитии сложных задач на части, чтобы одну часть обработать сейчас, другую на следующую итерацию цикла и так далее, при этом получается что задача с одной стороны постепенно делается, а с другой стороны между ее частями могут проскакивать какие то другие события, другие клиенты и серьезной задержки в обслуживании не произойдет.

 

21. Событийный цикл, библиотека libUV

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

здесь для чтения файла использован асинхронный вызов fs.readFile(), но любые ли операции можно сделать асинхронными? На сколько действительно опасны синхронные вызовы, и что делать если какая то синхронная операция есть и избежать ее никак нельзя, как снизить ее вредный эффект? Что происходит с теми запросами которые приходят пока интерпретатор занят. Например если здесь

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

Для того чтоб глубже понимать происходящее, познакомимся с библиотекой LibUV. Вот ссылочка на доку — http://docs.libuv.org/en/v1.x/

screenshot_21_01

К этой библиотеки не надо обращаться каким то явным образом, она написана на языке «С» и встроена в сервер Node.JS. Библиотека LibUV отвечает за две принципиально важные вещи. Первое это кроссплатформенные операции ввода-вывода — работа с файлами, работа с сетью. Мы через JavaScript даем команду Node.JS — просчитай такой то файл или отправь такие данные по сети, а Node.JS, чтобы это сделать, внутри себя использует библиотеку LibUV. Таким образом LibUV отвечает за кроссплатформенную реализацию этих функций. Именно она уже знает как работать с Windows, работать с Linux и так далее.
Вторая область ответственности LibUV это поддержка основного событийного цикла Node.JS. Оказывается, когда мы запускаем какой то скрипт. то он запускается в режиме цикла. Этот цикл чередует выполнение JavaScript, который обеспечивается виртуальной машиной V8, с ожиданием различных событий ввода-вывода, срабатывания таймеров, за которые так же отвечает библиотека LibUV. И этот цикл будет продолжаться до тех пор, пока возможно появление каких то новых событий, ввода-вывода или таймеров которые нужно будет обработать.

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

Вначале срабатывает JsvsScript. Он подключает модули ‘http’, ‘fs’, создает объект ‘server’, вешает обработчик — «server.on()», то что внутри обработчика пока не важно, он еще не сработал,  и наконец последняя строчка, это вызов команды «listen()». Команда «listen()» это уже команда с сетевыми соединениями. JavaScript команда server.listen(….) попадая в Node.JS проходит через его С++ код, превращается в вызов внутреннего метода TCPWrap::Listen(….), этот внутренний метод уже вызывает библиотеку LibUV, а именно ее метод uv_listen(….), который как раз осуществляет всю работу, то есть он, в зависимости от операционной системы, вешает обработчик соединений на данный порт. Для MacOS, для Unix систем используется системный вызов listen(….).

screenshot_21_02

Таким образом, LibUV назначило обработчик на соединение на этот порт. Этот обработчик, или в терминах LibUV он называется watcher, является внутренним, то есть мы к нему доступа не имеем, это именно LibUV его поставила и когда что то произойдет, например когда придет новое соединение, то watcher сработает, он вызовет соответствующие методы LibUV, Node.JS и в конечном счете даст нам какое то событие, например событие connection. Но это все будет потом, а пока что listen(….) просто повесила обработчик-watcher, результат этого действия подымается по цепочке, если все хорошо, то это приводит к событию listening в JavaScript, если обработчик повесить не удалось, то error.

На этой радостной ноте выполнение JavaScript завершается и LibUV проверяет, есть ли какие то watcher которые могут сработать, есть ли какие то внутренние обработчики, если их нет, то завершается весь процесс Node.JS,  завершается весь событийный цикл. Но в нашем случае, один такой watcher, а именно обработчик на порту 3000, был поставлен, именно по этому процесс Node.JS не завершится, а временно заснет и будет спать до какой то причины ему проснуться, например до появления новых событий ввода вывода. Рано или поздно такое событие скорее всего произойдет.

screenshot_21_03

Появится сигнал из операционной системы, что кто то присоединился к порту 3000, внутренний watcher LibUV вызовет соответствующий callback, этот callback передаст сигнал библиотеке LibUV, потом он перейдет в Node.JS, обертка Node.JS тут же сгенерирует событие connection и примется разбирать то, что нам присылают. Далее, если в процессе анализа данных установлено, что это http запрос, то будет сгенерировано событие request и наконец то сработает обработчик server.on(….).

Если получилось так, что url вот такой —  if(req.url == ‘/’), то опять же при помощи LibUV инициируется считывание вот этого файла

Мы отправляем команду в LibUV и JavaScript, на текущий момент закончил работу, все ‘request’ события обработано. Так как JavaScript закончил выполнение и есть внутренние обработчики LibUV, то процесс не прерывается, а снова переходит в состояние спячки. Из этой спячки его могут вывести события, какие? Первое это новый запрос, а второе это завершено чтение файла или какая то ошибка возникла, это нам не так важно сейчас. Когда что то из этого произойдет, то будет вызван соответствующий callback. Получается, что наш код на языке JavaScript, выступает в роли этакого рулевого, он говорит LibUV, инициируй какой то процесс, например чтение файла или получай соединение на таком то порту, LibUV умеет это правильно передать операционной системе, и дальше уже операционная система занимается всякими такими делами, а LibUV ждет пока та ответит, LibUV может делать много таких операций одновременно, уведомляет о результатах JS. Когда операционная система ответит, то LibUV опять же вызывает наш JavaScript код который разруливает ситуацию, возможно инициирует какие то новые процессы ввода-вывода и и дальше процесс опять переходит в состояние спячки и так по циклу.

Казалось бы все более менее ясно, на самом деле есть еще некоторые нюансы. Например представим себе, что первое событие которое здесь произошло

screenshot_21_04

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

screenshot_21_05

И будут обработаны последовательно по мере освобождения JavaScript. При этом не смотря на то что очередь событий одна, путаницы никогда не возникнет, потому что когда запускается соответствующий callback — » function(err, info){ …. });» то информацию о том, что это за запрос мы берем из замыкания джаваскриптового. То есть если запустился callback для Паши то тут — «server.on(‘request’, function(req, res){» будет req для Паши. Для Маши callback запустился, это уже другая функция, другое замыкание, соответственно будет продолжаться обработка и будет ответ прислан Маше.

С другой стороны получается так, что для того чтоб работа сервера была наиболее эффективной, JavaScript должен выполняться очень быстро, то есть чтобы никакие новые события не накапливались не ждали. Ну а что будет если JavaScript почему то затормозил. например есть какая то тяжелая вычислительная задача и Node.JS занят, события накапливаются? Такая ситуация называется «Event loop starvation» или по русски «Голодание событийного цикла». При этом обработка всех клиентов которые от них зависят притормозится, что конечно же не очень то хорошо. Для того чтобы обойти эту проблему, тяжелые вычисления обычно выделяют, или в отдельный процесс, или в отдельный поток, либо запускают сам сервер Node.JS в режиме множества процессов, например это можно сделать используя встроенный модуль «cluster», но не только. Еще один вариант, это разбить тяжелую вычислительную задачу на части, то есть например часть ответа можно сгенерировать вот в функции server.on(…), потом через setTimeout(10), отложить генерацию следующей части ответа, и так далее, и так далее. Соответственно при этом получится, что работа в сумме выполняется такая же, но вот выполнение JavaScript разрывается и в промежутке между этими вычислениями, сервер может делать что то еще, например обрабатывать других клиентов. Так или иначе, все эти решения добавляют сложности по этому Node.JS используется в первую очередь там, где тяжелых вычислений не нужно, а где требуется в первую очередь обмен данными. Практика показывает, что это большая часть задач связанных с веб разработкой.

Итак подведем итоги

screenshot_21_06

Сердцем Node.JS является библиотека LibUV. В этом и сила и слабость Node.JS. С одной стороны LibUV позволяет делать много операций ввода-вывода одновременно, то есть наш JavaScript код, может инициировать операцию и дальше заниматься другими делами. Таким образом множество операций ввода-вывода могут обрабатываться одновременно операционной системой, а JavaScript будет пересылать данные от одного клиента к другому, от базы данных клиенту и так далее, просто, эффективно. С другой стороны, это все требует асинхронной разработки, то есть не «readFileSync()», а «readFile()» например. Принятая в Node.JS система колбеков, с одной стороны достаточно проста, с другой стороны она все равно сложнее, чем просто последовательные команды синхронной разработки.  Кроме того, так как JS процесс должен обрабатывать кучу событий, то желательно,чтоб он не ждал, чтоб он все делал быстро, быстро, быстро. Чтоб очередь событий не накапливалась. Эту особенность работы Node.JS стоит иметь ввиду с самого начала разработки веб приложения, потому что казалось бы, ну какие у нас сложные вычислительные операции, что у нас  там заблокирует JavaScript? А например. Парсинг большого JSON или например подсчет MD5 суммы большого файла закаченного. Еще раз обращаю ваше внимание, эти задачи влияют на производительность сервера еще до того как JavaScript, который их выполняет, съест 100% процессора. То есть Node.JS может кушать 20%, но работать не достаточно эффективно просто из за того, что пока JavaScript занят другие задачи, даже те кому нужны другие ресурсы, скажем база данных, не могут продолжить выполнение. Для того чтобы как то защититься от этого, обычно запускают Node.JS приложение в режиме множества процессов. Как это сделать? Мы разберем в одной из следующих глав.

20. Введение в асинхронную разработку

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

используя модуль fs, при получении запроса на такой ‘/’ url, считывается файл ‘index.html’ и выводится посетителю. Обращаю ваше внимание, ‘fs’ здесь взято для примера, вместо вот такого запроса — «fs.readFileSync(‘index.html’);», здесь мог быть запрос к базе данных или какая то другая операция, которая потребует существенного времени ожидания, в данном случае, это ожидание ответа от диска. Если бы это был запрос к базе данных, это было бы ожидание ответа по сети от базы. Наш код с одной стороны будет работать, с другой стороны в нем есть проблема, связана с масштабируемостью, которая неизбежно проявится в  серьезной, промышленной эксплуатации. Например, Петя зашел по этому url — ‘/’ и запросил файл —  «fs.readFileSync(‘index.html’);», Петя ждет пока сервер ему ответит и сервер ждет пока файл прочитается и готов ему выслать данные. В это время заходит Вася, Маша и куча другого народу, которые тоже хотят, что то от сервера, например они хотят не вот этот файл, а они хотят вообще, что то другое, скажем получить текущую дату, которую по идеи можно взять и тут же вернуть

Но сервер не может это сделать, поскольку сейчас, его интерпретатор javascript занят, он ожидает ответа от диска — «fs.readFileSync(‘index.html’);». Когда этот ответ получен он  может продолжить выполнение, и выполнить следующую строчку — «res.end(info);», закончить наконец обработку запроса и тогда JavaScript освободится и сможет обработать какие то еще запросы. В результате мы имеем ситуацию, когда одна операция требующая долгого ожидания фактически парализует работу сервера, что конечно же неприемлемо. Только поймите меня правильно, сам по себе вызов, вполне нормальный и такие — «fs.readFileSync(‘index.html’);» — синхронные вызовы замечательно работают если нам нужно делать консольный скрипт. В котором, например, мы должны  прочитать файл, потом там с ним что то сделать, потом там его куда то записать и так далее. То есть когда мы должны последовательно сделать ряд задач, связанных с файлами, то такие вызовы, это замечательно, это просто и удобно. Проблемы с ними возникают лишь в серверном окружении, когда нужно делать много вещей одновременно. По этому здесь нужно воспользоваться другим методом, который тоже есть в модуле «fs» и который работает Асинхронно. Иначе говоря, асинхронный метод сразу ничего не возвращает обычно, но вместо этого он инициирует чтение файла, получает аргумент-функцию которой он этот файл передаст когда закончит процесс —

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

задачу Node.JS и после этого выполнение продолжается. При этом просчитывать мы будем конечно же вот здесь

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

Функцию которую Node.JS обязуется вызвать когда завершит процесс, называют функцией обратного вызова или по английски — callback function, или просто callback. Важный подводный камень состоит в том, что о возможности ошибки при таком вызове, можно легко забыть. Например, посмотрим что будет, если  файл ‘index.html’, почему то отсутствует. Либо была какая то ошибка при чтении скажем, или с правами, или с диском. В этой ситуации модуль «fs» вызовет callback с первым аргументом, с объектом ошибки, а второго аргумента вообще не будет. Если мы ошибку никак не обрабатываем, то получится что посетитель получит вообще пустую строку. Вот как будто вот такой вызов

что в принципе работает так же как и вот эта

Кошмар ситуации в том, что код просто, тихо сглючит, без всяких сообщений об ошибке. При этом, это во первых, может стать известным не сразу, то есть будут какие то жалобы и недовольные люди, а во вторых, будет достаточно сложно отладить это, найти причину, опять же, потому что все очень тихо. Соответственно, чтоб такого не происходило, нужно обязательно обрабатывать аргумент-ошибку. В крайнем случае если мы уж точно уверены, что ошибки, ну никогда не будет (ха ха ха — посмеялись поколения разработчиков, над этой мыслью), можно сделать вот так

Но в данном случае будет более правильным сделать вот такой вариант

 

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

Синхронный вариант

Асинхронный вариант

 

Начнем с синхронного. Синхронные вызовы, типа

Используются достаточно редко, они применяются в тех случаях, когда мы можем себе позволить заблокировать интерпретатор JavaScript. Как правило это значит, что нет параллелизма. Например консольный скрипт, сделай первое, второе, третье и так далее. Синхронный вызов заставляет наш интерпретатор ждать, потом он ответ пишет в «info», если какая то ошибка вышла, то это исключение и оно отлавливается при помощи try..catch.

Асинхронный вариант работает по другому. Тут видите другой вызов

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

в try..catch, не имеет особого смысла. То есть, конечно можно, но с другой стороны, при вызове «fs.readFile()», собственно ошибки никакой не возникнет.  Этот метод устроен так, что работает, что работает асинхронно и все ошибки передает в callback.  На эту тему в Node.JS есть соглашение, все встроенные модули ему следуют и мы тоже, что первый аргумент функции-обработчика является всегда ошибкой.

То есть вот эта функция, назову ее «cb» для наглядности

Будет при ошибке вызвана так

А если ошибки нет, то она будет вызвана так — «cb(null, ….)» первый аргумент будет null, а во втором уже будут какие то результаты.

Соответственно важное отличие между синхронным и асинхронным вариантом, здесь в том, что если мы в синхронном варианте, вдруг забыли try…catch, то при ошибке, это обязательно станет нам известным, исключение просто выпадет от сюда

и повалит процесс в данном коде.
А здесь

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

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

 

 

19. Логирование, модули debug и winston

Наша следующая тема логирование или иначе говоря отладочный вывод. Когда проект маленький, то вполне достаточно console.log для того, чтобы что-то вывести. Однако проект имеет свойство расти. Например, тот же server.js естественным образом разделяется на сервер и обработчик запроса — request. Со временем появляется работа с пользователем, база данных и так далее. Каждый файл может захотеть по ходу своего выполнения, что-то вывести. И этот вывод для нас очень важен, поскольку показывает, что происходит. Особенно, если что-то происходить не так. В текущем коде, везде используется console.log для вывода

Это означает, что перейдя по браузерному url, я получу однообразную кашу из всех записей, что делает скрипт.

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

Для этого используются специализированные модули. Рекордсмен по простоте, это модуль DEBUG.

Модуль debug

Давайте поставим его в наш проект. Для этого вспоминаем главу — 7. Введение в NPM — менеджер пакетов для Node.JS и в консоле, из директории проекта, вводим команду

получаем

screenshot_19_01

как видим NPM создал, как в общем то и ожидалось, директорию «node_modules» в корне нашего проекта

screenshot_19_02

куда и поставил новый модуль, «debug». Давайте теперь подключим его, добавив такую строку

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

Вот так, в данном случае, пусть это буде ‘server’. Теперь вместо console.log мы пишем debug

Аналогичную операцию я произвожу и с файлом request.js, только на этот раз идентификатором будет не ‘server’ a ‘server:request’. Обратим внимание на двоеточие в  ‘server:request’, это нам еще будет важно

Итак, запускаю, сейчас не из WebStorm а через терминал

screenshot_19_03

И мы видим, что ничего не выводит, потому что, чтоб  выводило, мне нужно указать, что выводить. Для возврата в исходный режим нажимаем сочетание клавиш «Ctrl + C». И пробуем еще раз, но теперь для передачи информации о том, что выводить, нужно создать переменную окружения с названием «DEBUG» и дать ей значение, для начала, «server»

screenshot_19_04

На эту тему, для пользователей windows, есть интересная статья —

Команда SET — работа с переменными среды Windows

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

 

У нас ведь есть еще один файл, который тоже должен выводить, нужную нам, информацию в консоль. Но в фале request.js другой идентификатор, а именно — ‘server:request’, давайте добавим его в нашу переменную. Вводим в консоле

Давайте чуть отвлечемся и проговорим вот такой момент, который сейчас важен. Переменная «DEBUG» имеет значение, это мы уже поняли. Это значение это строка. И если до последней команды в консоле, эта строка была равна «DEBUG=server», то после она стала равной  «DEBUG=server:request,server». Как видим последней командой мы дописали значение в начало этой строки.

От сюда делаем два вывода —

  • во первых принципиально важно не забыть поставить запятую после выражения которое мы хотим добавить к нашей строке, потому что именно по запятым парсится значение переменной DEBUG, и отделяя запятыми мы перечисляем идентификаторы которыми помечены интересующие нас логи, прямо как точка с запятой — «;» в переменной «PATH».
  • во вторых если у нас вдруг появится файл «user.js» и в нем мы подключим модуль «debug» которому укажем идентификатор, скажем, «user», то нам нужно будет опять проделать такую операцию

    если мы хотим выводить логи из этого файла.

К стати проверить значение переменной можно в любой момент, введя в консоле такую команду

Просто set и имя переменной, вот такая многогранная команда «set».

И так возвращаемся к нашему серверу. Мы добавили значение в переменную DEBUG

screenshot_19_06

теперь посмотрим какое значение имеет переменная DEBUG

screenshot_19_07

Судя по значению должно выводить логи из обоих файлов, проверим

screenshot_19_08

запустили сервер и он сразу вывел лог помеченный идентификатором ‘server’. Теперь перезагрузим открытую в браузере страницу по адресу — «http://127.0.0.1:1337/echo?message=TEST«, смотрим в консоль

вот и второй файл и логи в нем отработали.

К стати если мы уже решили выводить абсолютно все логи, то можно значение переменной  DEBUG установить равной «*». Давайте установим, убедимся чему равно и запустим сервер. И не забываем, чтоб прервать работу сервера из консоли, достаточно нажать сочетание клавиш «Ctrl + C». Итак смотрим

screenshot_19_10

Теперь перезагрузим открытую в браузере страницу по адресу — «http://127.0.0.1:1337/echo?message=TEST» и смотрим в консоль

Все работает по прежнему.

Модуль winston

Модуль DEBUG это с одной стороны простое и гибкое решение задачи логгинга, с другой стороны он иногда уж слишком прост например в файле request.js у нас дебагом обозначены сообщения, важность которых совершенно различная. Скажем эта информация

может быть средней важности при отладке. Эта информация

может быть неважной. Эта информация может быть очень важной

поскольку, это ошибка — url не найден.

При помощи DEBUG задать важность, каким то образом нельзя. Кроме того DEBUG все пишет в стандартный поток вывода, а мы можем захотеть писать в файл или базу данных. Если такая потребность возникла или планируется, что она возникнет, тогда имеет смысл взглянуть на более навороченный модуль для логирования, который называется «Winston».

Ставим его

screenshot_19_12

и заменяем в коде require(‘debug’) на require(‘winston’), который возвращает объект «log».

Для того чтобы логировать, я должен вызвать соответствующий метод этого объекта

  • log.debug — Маленькой важности
  • log.info — Сообщение средней важности.
  • log.error — это ошибка.

Все ошибки считаются очень важными, кроме того логгер настроен так, что по умолчанию он выводит сообщения только уровня info и более важные. Сейчас мы это увидим, запускаю в WebStorm и в браузере перехожу на соответствующий url — «http://127.0.0.1:1337/echo?message=TEST«.

screenshot_19_13

Как и говорилось ранее log.debug вообще не выводится, выводятся только сообщения уровня info и более важные, такие как error.

В модуле «DEBUG» можно было ограничить вывод, только интересующими нас модулями указав их в переменной окружения DEBUG. К сожалению в самом winston такой функциональности нету, по этому ее придется реализовать самим.

 

Просто сделать обертку над winston, которая будет находиться в отдельном модуле и добавлять интересующею нас функциональность. Назовем этот модуль «log», он будет принимать текущий объект модуля  и возвращать по сути тот же winston, но по разному настроенный, в зависимости от того какой именно модуль мы ему передаем, для каких то будем логировать так, для каких то можно логировать по другому. Вот пример такого модуля