15. Уроки Node.js. Асинхронная разработка. Введение.
В реальной жизни очень редко бывает так, что, получив запрос, сервер может тут же на него ответить. Обычно для того, чтобы ответить, серверу нужны какие-то данные. Эти данные он получает либо из базы, либо из какого-то другого источника, например, из файловой системы. В этом примере, используя модуль fs при получении запроса на url ‘/’, считывается файл index.html и выводится посетителю.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { var info; if (req.url == '/') { info = fs.readFileSync('index.html'); res.end(info); } }).listen(3000); |
Обращаем ваше внимание, fs здесь всего лишь для примера, вместо данного запроса, мог бы быть запрос к базе данных или какая-то другая операция, которая потребует существенного времени ожидания. В данном случае это ожидание ответа от диска:
1 | info = fs.readFileSync('index.html'); |
Если бы это был запрос к базе данных, это было бы ожидание ответа по сети от базы. Такой код, с одной стороны, будет работать, а с другой стороны, в нем есть проблема, связана с масштабируемостью, которая неизбежно проявится в серьезной промышленной эксплуатации. Например, Джон зашел по этому url ‘/’и запросил файл fs.readFileSync(‘index.html’). Джон ждет, когда сервер ему ответит. Сервер ждет, когда файл прочитается и готов выслать ему данные. В это время заходят Илон, Линус и много других, которые тоже хотят чего-то от сервера. Например, они хотят не данный файл, а что-то другое, скажем, получить текущую дату, которую, по идее, можно взять и тут же вернуть.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { var info; if (req.url == '/') { info = fs.readFileSync('index.html'); res.end(info); } else if (req.url == '/now') { res.end(new Date().toString()); } }).listen(3000); |
Но сервер не может этого сделать, поскольку сейчас его интерпретатор JavaScript занят, он ожидает ответа от диска. Когда этот ответ будет получен, он сможет продолжить выполнение строчки, закончить обработку запроса, и тогда JavaScript освободится и сможет обработать какие-то еще запросы.
В результате мы имеем ситуацию, когда одна операция, требующая долгого ожидания, фактически парализует работу сервера, что, конечно же, неприемлемо. Только поймите меня правильно, сам по себе вызов вполне нормальный, и такие синхронные вызовы замечательно работают, если нам нужно сделать консольный скрипт, в котором мы должны прочитать файл, потом с ним что-то сделать, куда-то записать и т.д. То есть, когда мы должны последовательно сделать ряд задач, связанных с файлами, то такие вызовы замечательно, просто и удобно. Проблемы с ними возникают лишь в серверном окружении, когда нужно делать много вещей одновременно. Поэтому здесь стоит воспользоваться другим методом, который тоже есть в модуле fs и который работает асинхронно. Иначе говоря, асинхронный метод обычно сразу ничего не возвращает, но вместо этого он инициирует чтение файла, получает аргумент функцию, которой он этот файл передаст, когда закончит процесс.
1 | fs.readFile('index.html', function(err, info){ |
Относительно этой функции есть следующее соглашение. Если чтение прошло успешно, то функция будет вызвана с первым аргументом null, а во втором аргументе будет содержимое файла. Но если произошла ошибка, то функция будет вызвана только с первым аргументом, в котором будет информация о ней. Таким образом, мы даем задачу Node.js и после этого выполнение продолжается. При этом, на этом этапе пока что ничего не просчитано, просчитано оно будет только вот здесь:
1 | res.end(info); |
Такое решение полностью снимает проблему блокировки, поскольку теперь интерпретатор JavaScript вовсе не будет ждать, пока файл прочитается. Он тут же продолжит выполнение и сможет заняться другими посетителями.
Функцию, которую Node.js обязуется вызвать, когда завершит процесс, называют функцией обратного вызова или по-английски callback function. То есть, модуль fs возвращает управление JavaScript интерпретатору и говорит, что ты сейчас поработай, а я, когда закончу чтение файла, тебе перезвоню (вызов callback).
Важный подводный камень состоит в том, что о возможности ошибки при таком вызове можно легко забыть. Например, посмотрим, что будет, если файл index.html почему-то отсутствует либо была какая-то ошибка при чтении, скажем, с правами, с диском. В этой ситуации модуль fs вызовет callback с первым аргументом объекта ошибки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { var info; if (req.url == '/') { fs.readFile('index.html', function (err, info) { // callback res.end(''); }); } }).listen(3000); |
А второго аргумента вообще не будет. Если мы ошибку никак не обрабатываем, то получится, что посетитель в итоге получит пустую строку. Кошмар ситуации здесь в том, что код просто тихо сглючит без всяких сообщений об ошибке. При этом, во-первых, это может стать известным не сразу, то есть, будут уже какие-то жалобы от недовольных людей, а во-вторых, будет достаточно сложно отладить это, то есть, найти причину. Соответственно, что бы такого не происходило, нужно обязательно обрабатывать аргумент ошибки. В крайнем случае, если мы точно уверенны, что ошибки никогда не будет, можно сделать вот так:
1 | if (err) throw err; |
Но в данном случае, будет, конечно, более правильным сделать вот такой вариант:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { if (req.url == '/') { fs.readFile('index.html', function(err, info) { if (err) { console.error(err); res.statusCode = 500; res.end("A server error occurred!"); return; } res.end(info); }); } }).listen(3000); |
Итак, в завершение этого выпуска сравним синхронный и асинхронный код, используя для примера реализацию сервера.
Ниже у нас синхронный вариант:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { var info; if (req.url == '/') { try { info = fs.readFileSync('index.html'); } catch(err) { console.error(err); res.statusCode = 500; res.end("A server error occurred "); return } res.end("info"); } else { /*404 */ } }).listen(3000); |
А здесь асинхронный:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { var info; if (req.url == '/') { fs.readFileSync('index.html', function(err, info) { if(err) { console.error(err); res.statusCode = 500; res.end("A server error occurred "); return } res.end("info"); }); } else { /*404 */ } }).listen(3000); |
Начнем с синхронного варианта. Синхронные вызовы типа readFileSync используются достаточно редко. Они применяются в тех случаях, когда мы можем позволить себе заблокировать интерпретатор JavaScript. Как правило, это значит, что нет параллелелизма. Например, консольный скрипт – делай а, в, с и т.д. Синхронный вызов заставляет наш интерпритатор ждать, потом он пишет ответ в info, если вышла какая-то ошибка, то это исключение, и оно отлавливается при помощи try catch:
1 2 3 4 5 6 7 8 | try { info = fs.readFileSync('index.html'); } catch(err) { console.error(err); res.statusCode = 500; res.end("A server error occurred "); return } |
Асинхронный вариант работает по-другому. Здесь, как видите, другой вызов:
1 | fs.readFileSync('index.html', function(err, info) { |
Для того, чтобы получить результат в асинхронном коде, используется функция обратного вызова:
1 2 3 4 5 6 7 8 9 10 | fs.readFileSync('index.html', function(err, info) { if(err) { console.error(err); res.statusCode = 500; res.end("A server error occurred "); return } res.end("info"); }); |
При этом, обертывать этот вызов в try catch не имеет особого смысла. Конечно, можно, но, с другой стороны, при этом вызове ошибки никакой не возникнет. Этот метод устроен так, что работает асинхронно и все ошибки передает callback. На эту тему в Node.js есть соглашение: все встроенные модули ему следуют, и мы тоже, что первый аргумент функции обработчика является всегда ошибкой. То есть, эту функцию для примера назовем сb.
1 | fs.readFileSync('index.html', function cb(err, info) { |
При ошибке будет вызвано так: cb(err), а если ошибки нет – cb(null, …). Соответственно, важное отличие между асинхронным и синхронным вариантом здесь в том, что если мы в синхронном варианте вдруг забыли try catch, то при ошибке нам обязательно станет это известным. Исключение выпадет и повалит процесс в данном коде. В асинхронном варианте, если мы забыли обработать ошибку, то оно будет глючить страшным образом. А мы не получим информации об этом. Соответственно, очень важно при асинхронной разработке обязательно хоть как-то обрабатывать ошибки. Конечно же, асинхронная разработка сложнее, нужно делать какие-то функции обратного вызова, но, вместе с тем, здесь есть свои способы упростить жизнь, которые мы рассмотрим в следующих статьях.
Код урока вы можете найти здесь
Материал урока взят из следующего скринкаста.
We are looking forward to meeting you on our website soshace.com
0 comments