Все, что вам нужно знать о Node.js

Node.js внешний интерфейс
Все, что вам нужно знать о Node.js

Node.js в настоящее время является одной из самых популярных технологий для создания масштабируемых и эффективных REST API. Его также можно использовать для создания гибридных мобильных приложений, настольных приложений и даже в сфере Интернета вещей.

Мне очень нравится, работаю с Node.js уже 6 лет. Этот пост пытается стать исчерпывающим руководством для понимания того, как работает Node.js.

Мир до Node.js

многопоточный сервер

Веб-приложения написаны по шаблону клиент/сервер (клиент/сервер), где клиент будет запрашивать ресурсы у сервера, а сервер будет отвечать на основе этого ресурса. Сервер отвечает только тогда, когда клиент запрашивает его, и закрывает соединение после каждого ответа.

Этот шаблон работает, потому что каждый запрос к серверу требует времени и ресурсов (памяти, ЦП и т. д.). Сервер должен завершить предыдущий запрос, прежде чем принять следующий запрос.

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

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

Примечание: поток на запрос означает один поток на запрос..

Для одновременной обработки N запросов серверу требуется N потоков. Если теперь есть N+1 запросов, он должен ждать, пока любой из N потоков не станет доступным.

В примере с многопоточным сервером сервер допускает до 4 запросов (потоков) одновременно. Когда будут получены следующие 3 запроса, эти запросы должны ожидать, пока любой из этих 4 потоков не станет доступным.

Один из способов обойти это ограничение — добавить больше ресурсов на сервер (память, ядра ЦП и т. д.), но это, вероятно, совсем не лучшая идея...

Конечно, будут технические ограничения.

блокировка ввода/вывода

Здесь проблема не только в количестве потоков на сервере. Может быть, вам интересно, почему поток не может обрабатывать 2 или более запросов одновременно? Это связано с блокировкой операций ввода/вывода.

Предположим, вы разрабатываете приложение для интернет-магазина, и ему нужна страница, на которой пользователи могут просматривать все ваши товары.

Доступ пользователяyourstore.com/productsСервер будет извлекать все ваши продукты из базы данных для отображения HTML-файла, достаточно просто, верно?

Но что происходит дальше? ...

  • 1.Когда пользователь посещает /products, для удовлетворения запроса необходимо выполнить определенный метод или функцию, поэтому будет небольшой фрагмент кода для анализа URL-адреса этого запроса и поиска правильного метода или функции.Ветка работает. ✔️

  • 2.Метод или функция и первая строка будут выполнены.Ветка работает. ✔️

  • 3.Поскольку вы хороший разработчик, вы будете хранить все свои системные журналы в одном файле, чтобы убедиться, что маршрут выполняет правильный метод/функцию, добавьте в свой журнал строку «Выполняется метод X!!» (что-то, что выполняется методом) , что является блокирующей операцией ввода-вывода.Тема ждет. ❌

  • 4.Журнал сохранен, и следующая строка будет выполнена.Ветка работает. ✔️

  • 5.Пришло время обратиться к базе данных и получить все продукты, простой запрос вроде операции SELECT * FROM products, но знаете что? Это блокирующая операция ввода-вывода.Тема ждет. ❌

  • 6.Вы получите список всех продуктов, но обязательно запишите их.Тема ждет. ❌

  • 7.С этими продуктами пришло время отобразить шаблон, но вы должны прочитать его перед отрисовкой.Тема ждет. ❌

  • 8.Механизм шаблонов выполняет свою работу и отправляет ответ клиенту.Нить снова начинает работать. ✔️

  • 9.Потоки свободны (бездействуют), как птицы. 🕊️

Насколько медленны операции ввода-вывода? Это зависит от ситуации.

Давайте проверим следующую таблицу:

действовать Тактовые циклы процессора
регистры процессора 3 ticks
Кэш L1 (кеш первого уровня) 8 ticks
Кэш L2 (вторичный кеш) 12 ticks
ОЗУ (оперативное запоминающее устройство) 150 ticks
Диск 30,000,000 ticks
Сеть 250,000,000 ticks

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

Дисковые и сетевые операции выполняются слишком медленно. Сколько запросов или внешних вызовов API выполняется вашей системой?

Во время восстановления операции ввода-вывода заставляют потоки ждать и тратить ресурсы.

Проблема C10K

Еще в начале 2000-х серверные и клиентские машины были медленными. Проблема заключается в запуске 10 000 одновременных клиентских подключений на одном сервере.

Почему наша традиционная модель «поток на запрос» не может решить эту проблему? Теперь займемся математикой.

Нативная реализация потоков выделяет около 1 МБ памяти на каждый поток, поэтому для 10 КБ потоков требуется 10 ГБ ОЗУ, помните, это было только в начале 2000-х! !

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

Спасение JavaScript?

Внимание, спойлер 🚨🚨🚨!!

Node.js решает эту проблему C10K... но почему?

Сервер JavaScript не был чем-то новым в 2000-х, у него были некоторые реализации поверх виртуальной машины Java, основанные на шаблоне «поток на запрос», например, RingoJS, AppEngineJS.

Но если это не решает проблему C10K, то почему Node.js? Ну, так как он однопоточный.

Node.js и циклы событий

Node.js

Node.js — это серверная платформа, построенная на основе механизма JavaScript Google Chrome (движок V8), который компилирует код JavaScript в машинный код.

Node.js основан на драйверах событий, неблокирующих моделях ввода / вывода, что делает его легким и эффективным. Это не кадр, ни библиотека, это время выполнения.

Простой пример:

// Importing native http module
const http = require('http');

// Creating a server instance where every call
// the message 'Hello World' is responded to the client
const server = http.createServer(function(request, response) {
  response.write('Hello World');
  response.end();
});

// Listening port 8080
server.listen(8080);

Неблокирующий ввод-вывод

Node.js — это неблокирующий ввод-вывод, что означает:

  • Основной поток не блокирует операции ввода-вывода.
  • Сервер продолжит участвовать в запросах.
  • Мы будем использовать асинхронный код.

Давайте напишем пример, в котором на каждый запрос /home сервер будет отвечать HTML-страницей, иначе сервер ответит текстом «Hello World». Чтобы ответить на HTML-страницу, сначала прочтите этот файл.

home.html

<html>
  <body>
    <h1>This is home page</h1>
  </body>
</html>

index.js

const http = require('http');
const fs = require('fs');

const server = http.createServer(function(request, response) {
  if (request.url === '/home') {
    fs.readFile(`${ __dirname }/home.html`, function (err, content) {
      if (!err) {
        response.setHeader('Content-Type', 'text/html');
        response.write(content);
      } else {
        response.statusCode = 500;
        response.write('An error has ocurred');
      }

      response.end();
    });
  } else {
    response.write('Hello World');
    response.end();
  }
});

server.listen(8080);  

Если URL-адрес этого запроса /home, мы используем собственный модуль fs для чтения файла home.html.

Функции, передаваемые в http.createServer и fs.readFile, называются обратными вызовами. Эти функции будут выполняться когда-то в будущем (первая функция будет выполняться при получении запроса, вторая функция будет выполняться после того, как файл будет прочитан и помещен в буфер).

При чтении файла Node.js все еще может обработать запрос и даже снова прочитать файл, все сразу в одном потоке... но как?!

Цикл событий

Цикл событий — это волшебство Node.js, в двух словах, цикл событий — это фактически бесконечный цикл, единственный доступный в потоке.

Libuv — это реализация этого режима библиотеки языка C, которая является частью основных модулей Node.js. Подробнее о Libuvhere.

Цикл событий должен пройти 6 этапов, выполнение всех этапов называется тиком.

  • таймеры: на этом этапе выполняются функции обратного вызова таймера setTimeout() и setInterval().
  • ожидающие обратные вызовы: Здесь выполняются почти все обратные вызовы, за исключением обратных вызовов закрытия, обратных вызовов в фазе таймеров и setImmediate().
  • бездействие, подготовка: Применяется только внутренне.
  • poll: получение новых событий ввода-вывода; Node блокирует их здесь, когда это необходимо.
  • Проверьте: Setimmediate () Функция обратного вызова будет выполнена здесь.
  • обратные вызовы закрытия: некоторые функции обратного вызова должны быть закрыты, например: socket.on('close', ...).

Итак, есть только один поток, и этот поток — EventLoop, но кто выполняет операции ввода-вывода?

Внимание 📢📢📢!!!

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

Разве это не было бы идеально?

Проблемы с интенсивной нагрузкой на ЦП

Node.js кажется идеальным, вы можете построить все, что вы хотите с ним.

Давайте создадим API для вычисления простых чисел.

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

Учитывая число N, API должен вычислить и вернуть N натуральных чисел в массиве.

primes.js

function isPrime(n) {
  for(let i = 2, s = Math.sqrt(n); i <= s; i++)
    if(n % i === 0) return false;
  return n > 1;
}

function nthPrime(n) {
  let counter = n;
  let iterator = 2;
  let result = [];

  while(counter > 0) {
    isPrime(iterator) && result.push(iterator) && counter--;
    iterator++;
  }

  return result;
}

module.exports = { isPrime, nthPrime };

index.js

const http = require('http');
const url = require('url');
const primes = require('./primes');

const server = http.createServer(function (request, response) {
  const { pathname, query } = url.parse(request.url, true);

  if (pathname === '/primes') {
    const result = primes.nthPrime(query.n || 0);
    response.setHeader('Content-Type', 'application/json');
    response.write(JSON.stringify(result));
    response.end();
  } else {
    response.statusCode = 404;
    response.write('Not Found');
    response.end();
  }
});

server.listen(8080);

Primes.js — это реализация функции простого числа, isPrime проверяет, является ли данный параметр N простым числом, если это простое число, nthPrime вернет n простых чисел.

index.js создает службу и использует эту библиотеку каждый раз, когда запрашивается /primes. Передать параметры через запрос.

Чтобы получить простые числа до 20, делаем запросhttp://localhost:8080/primes?n=2

Предположим, что к этому замечательному неблокирующему API обращаются 3 клиента:

  • Первое простое число на 5 запросов в секунду раньше.
  • Второй запрашивает первые 1000 простых чисел в секунду.
  • Третий запрос вводит сразу 10 000 000 000 простых чисел, но...

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

А как же Либув? Если вы помните, что эта библиотека использует системные потоки, чтобы помочь Node.js выполнять некоторые операции ввода-вывода, чтобы избежать блокировки основного потока, то вы правы, это может помочь нам решить эту проблему, но для использования библиотеки Libuv мы должны использовать язык С++.

Стоит отметить, что Node.js v10.5 представил рабочие потоки.

рабочий поток

какдокументация:

Рабочие потоки полезны для выполнения операций JavaScript, интенсивно использующих ЦП. Они малопригодны для работы с интенсивным вводом-выводом. Встроенные в Node.js операции асинхронного ввода-вывода более эффективны, чем рабочие потоки.

Изменить код

Теперь исправьте наш код инициализации:

primes-workerthreads.js

const { workerData, parentPort } = require('worker_threads');

function isPrime(n) {
  for(let i = 2, s = Math.sqrt(n); i <= s; i++)
    if(n % i === 0) return false;
  return n > 1;
}

function nthPrime(n) {
  let counter = n;
  let iterator = 2;
  let result = [];

  while(counter > 0) {
    isPrime(iterator) && result.push(iterator) && counter--;
    iterator++;
  }

  return result;
}

parentPort.postMessage(nthPrime(workerData.n));

index-workerthreads.js

const http = require('http');
const url = require('url');
const { Worker } = require('worker_threads');

const server = http.createServer(function (request, response) {                                                                                              
  const { pathname, query } = url.parse(request.url, true);

  if (pathname === '/primes') {                                                                                                                                    
    const worker = new Worker('./primes-workerthreads.js', { workerData: { n: query.n || 0 } });

    worker.on('error', function () {
      response.statusCode = 500;
      response.write('Oops there was an error...');
      response.end();
    });

    let result;
    worker.on('message', function (message) {
      result = message;
    });

    worker.on('exit', function () {
      response.setHeader('Content-Type', 'application/json');
      response.write(JSON.stringify(result));
      response.end();
    });
  } else {
    response.statusCode = 404;
    response.write('Not Found');
    response.end();
  }
});

server.listen(8080);

index-workerthreads.js будет создавать экземпляр Worker для каждого запроса, загружать и выполнять файл primes-workerthreads.js в рабочем потоке. Когда список простых чисел будет рассчитан, сообщение будет запущено, получит информацию и назначит ее результату. Поскольку задание завершено, событие выхода будет запущено снова, что позволит основному потоку отправить данные клиенту.

PRIMES-WORINGHTREADS.JS немного изменилось. Он импортирует работник (передача параметров из основного потока), ParentPort, который является тем, как мы отправляем сообщения в основной поток.

Давайте теперь сделаем три клиентских примера, посмотрим, что произойдет:

Основной поток больше не блокирует 🎉🎉🎉🎉🎉!!!!!

Это работает, как и ожидалось, но создание рабочих потоков не является лучшей практикой, а создание новых потоков недешево. Обязательно сначала создайте пул потоков.

в заключении

Node.js — мощная технология, которую стоит изучить.

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

Ребята, остановись здесь. Я надеюсь, что вы понимаете Node.js.

Спасибо за прочтение, увидимся в следующей статье. ❤️