За лаштунками Node.js: огляд на високому рівні

Якщо ви шукаєте "Що таке Node.js", ймовірно, ви знайдете таке визначення:

"Node.js — це безкоштовне, з відкритим вихідним кодом, кросплатформенне середовище виконання JavaScript, яке дозволяє розробникам створювати сервери, веб-додатки, інструменти командного рядка та сценарії."

Але що насправді означає "середовище виконання JavaScript"? Щоб зрозуміти це, давайте розберемо по кроках.

Розбір середовища виконання JavaScript

Уявіть, що у вас є рецепт для найсмачнішої страви. Це шедевр. Але рецепт сам по собі не може приготувати їжу — вам потрібно:

  • Шеф-кухар: Людина, яка розуміє рецепт і може приготувати страву
  • Кухня: Простір з інструментами та інгредієнтами для того, щоб рецепт став реальністю

pic

Photo by Benjamin Brunner on Unsplash

У контексті Node.js:

  • JavaScript код — це рецепт
  • Двигун V8 (розроблений Google) — це шеф-кухар — він компілює та виконує JavaScript код
  • Середовище виконання — це кухня — воно надає інструменти (як-от API та системні ресурси), які потрібні JavaScript для роботи поза браузером

Node.js розширює JavaScript поза браузер, дозволяючи розробникам створювати сервери, взаємодіяти з файловими системами та обробляти мережеві запити.

Що включає середовище виконання?

Середовище виконання надає все необхідне для виконання JavaScript коду. Це включає:

  1. Виконання коду:
    Інструменти для компіляції або інтерпретації коду в машинні інструкції
  2. Бібліотеки та API:
    Вбудовані модулі для виконання поширених завдань (наприклад, fs для операцій з файлами, http для мережі)
  3. Управління пам'яттю:
    Обробка розподілу пам'яті для змінних та функцій
  4. Взаємодія з системою:
    Взаємодія з операційною системою для роботи з файлами, мережевими запитами та іншими завданнями

Як працює Node.js: розбір екосистеми

pic

Ось детальніше про те, як Node.js працює "під капотом":

  1. Код додатку (JavaScript):
    Це точка відліку. Розробники пишуть JavaScript код для виконання завдань, таких як здійснення HTTP запитів, читання файлів або виконання обчислень. Код передається в двигун V8 для виконання.

  2. Двигун V8 (JavaScript Engine):

  • Двигун V8 компілює JavaScript у машинний код для швидкого виконання
  • Він обробляє синхронні завдання, такі як математичні обчислення або виклики функцій
  • Однак, сам по собі V8 не може обробляти асинхронні завдання, такі як операції з файлами або мережеві запити
  1. API Node.js:

Node.js надає API для серверних завдань, яких не підтримує JavaScript у браузері, таких як:

  • Операції з файловою системою (fs модуль)
  • Мережеві запити (http модуль)
  • Таймери (setTimeout, setInterval)
  1. libuv та Event Loop:

libuv — це бібліотека в Node.js, яка обробляє асинхронні операції. Вона надає:

  • Event Loop (Цикл подій): Механізм для неблокуючого виконання, що забезпечує ефективну обробку завдань
  • Thread Pool (Пул потоків): Набір фонових потоків для таких завдань, як операції з файлами або запити до бази даних

Приклад: Операції з файловою системою в Node.js

Щоб зрозуміти, як працює Node.js "під капотом", давайте розглянемо, як виконується звичайне завдання, таке як відкриття файлу, покроково.

pic

Проблема: чому нам потрібні API Node.js?

Двигун V8, який забезпечує роботу Node.js, має обмежену функціональність. Він призначений переважно для ефективного виконання JavaScript коду. Хоча він може обробляти базове асинхронне програмування, у нього немає інструментів для взаємодії з файловою системою, мережею або операційною системою безпосередньо — речі, які зазвичай не потрібні в браузерному середовищі.

Коли ви пишете такий код, як fs.open, це робить щось, що виходить за межі того, що може обробити V8.
Ось тут і вступають у гру API Node.js, прив’язки (bindings), libuv та операційна система.

Крок 1: Двигун V8 обробляє код

pic

Подорож починається, коли двигун V8 зустрічає JavaScript код:

const fs = require('fs');  
// Шлях до файлу, який ви хочете відкрити  
const filePath = './example.txt';  
// Відкрити файл у режимі читання  
fs.open(filePath, 'r', (err, fd) => {  
 if (err) {  
 console.error('Помилка при відкритті файлу:', err.message);  
 return;  
 }  
 console.log(`Файл успішно відкрито. Дескриптор файлу: ${fd}`);  
 // Завжди закривайте файл після використання  
 fs.close(fd, (err) => {  
 if (err) {  
 console.error('Помилка при закритті файлу:', err.message);  
 } else {  
 console.log('Файл успішно закрито.');  
 }  
 });  
});

Ось що відбувається:

  • Двигун V8 виконує код по черзі
  • Коли він досягає функції fs.open, V8 розуміє, що ця функція не є частиною рідного JavaScript. Вона надається Node.js, тому V8 передає завдання до шарів API Node.js

Крок 2: API Node.js

pic

Функція fs.open належить до основної бібліотеки JavaScript Node.js. Ця бібліотека надає API (як-от fs, http тощо), які розширюють функціональність JavaScript для серверних задач. Реалізацію fs.open можна знайти у файлі Node.js lib/fs.js:

function open(path, flags, mode, callback) {  
 path = getValidatedPath(path);  
 if (arguments.length < 3) {  
 callback = flags;  
 flags = 'r';  
 mode = 0o666;  
 } else if (typeof mode === 'function') {  
 callback = mode;  
 mode = 0o666;  
 } else {  
 mode = parseFileMode(mode, 'mode', 0o666);  
 }  
 const flagsNumber = stringToFlags(flags);  
 callback = makeCallback(callback);  
const req = new FSReqCallback();  
 req.oncomplete = callback;  
 binding.open(path, flagsNumber, mode, req);  
}

Ось що відбувається на цьому етапі:

  • Функція fs.open перевіряє шлях до файлу, прапори та режим
  • Вона створює об'єкт зворотного виклику (FSReqCallback), щоб обробити результат
  • Функція потім викликає binding.open, що забезпечує зв'язок між JavaScript і нижчим рівнем реалізації на C++

Ключове зауваження: API Node.js обробляє JavaScript частину операції, але для доступу до нижчої функціональності використовує прив’язки (bindings).

Крок 3: Прив’язки Node.js

pic

На цьому етапі виклик binding.open з'єднує API JavaScript з реалізацією функціональності на C++.
Прив’язки виконують роль перекладача, дозволяючи JavaScript викликати функціональність, написану на C або C++.

Реалізація fs.open на C++ можна знайти у файлі Node.js src/node_file.cc:

static void Open(const FunctionCallbackInfo& args) {  
 Environment* env = Environment::GetCurrent(args);  
const int argc = args.Length();  
 CHECK_GE(argc, 3);  
 BufferValue path(env->isolate(), args[0]);  
 CHECK_NOT_NULL(*path);  
 ToNamespacedPath(env, &path);  
 CHECK(args[1]->IsInt32());  
 const int flags = args[1].As()->Value();  
 CHECK(args[2]->IsInt32());  
 const int mode = args[2].As()->Value();  
 if (argc > 3) { // open(path, flags, mode, req)  
 FSReqBase* req_wrap_async = GetReqWrap(args, 3);  
 CHECK_NOT_NULL(req_wrap_async);  
 if (AsyncCheckOpenPermissions(env, req_wrap_async, path, flags).IsNothing())  
 return;  
 req_wrap_async->set_is_plain_open(true);  
 AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,  
 uv_fs_open, *path, flags, mode);  
 } else { // open(path, flags, mode)  
 // Синхронний шлях  
 }  
}

Ключові моменти:

  • Функція binding.open готує шлях до файлу, прапори та режим для подальшої обробки
  • Потім вона викликає uv_fs_open з libuv, що відповідає за виконання фактичної операції з файлом

Крок 4: libuv

pic

libuv — це бібліотека, яку використовує Node.js для обробки асинхронних операцій вводу/виводу. Вона надає цикл подій та пул потоків, що дозволяє Node.js виконувати неблокуючі операції.

Функція uv_fs_open в libuv виглядає так (знайдена в джерельному коді libuv):

int uv_fs_open(uv_loop_t* loop,  
 uv_fs_t* req,  
 const char* path,  
 int flags,  
 int mode,  
 uv_fs_cb cb) {  
 INIT(OPEN);  
 PATH;  
 req->flags = flags;  
 req->mode = mode;  
 if (cb != NULL)  
 if (uv__iou_fs_open(loop, req))  
 return 0;  
 POST;  
}

Ось що відбувається:

  • libuv отримує запит на операцію з файлом і готує його до виконання
  • Вона взаємодіє з операційною системою для відкриття файлу, безпосередньо або через пул потоків, якщо операція асинхронна
  • Як тільки операція завершується, libuv повідомляє цикл подій

Крок 5: Взаємодія з операційною системою

Зрештою, libuv безпосередньо взаємодіє з операційною системою для виконання операції з файлом. На Linux це включає виклик системного виклику open:

static ssize_t uv__fs_open(uv_fs_t* req) {  
 return open(req->path, req->flags | O_CLOEXEC, req->mode);  
}

Саме тут файл фізично відкривається, і дескриптор файлу повертається до Node.js.

Підсумок робочого процесу

  1. JavaScript код: Функція fs.open викликається у вашому JavaScript коді
  2. Двигун V8: V8 виконує код і передає виклик fs.open до API Node.js
  3. API Node.js: API обробляє частину на JavaScript та викликає C++ binding.open
  4. Прив’язки Node.js: З'єднують JavaScript і C++ світи
  5. libuv: libuv обробляє асинхронну операцію та взаємодіє з ОС
  6. Операційна система: Виконує операцію з файлом і повертає результат в libuv

Висновок

Розуміння того, як Node.js працює "під капотом", розкриває багаторівневу архітектуру, яка забезпечує його неблокуючі асинхронні операції. Від JavaScript до двигуна V8, API Node.js, прив’язок (bindings), libuv та операційної системи — кожен рівень грає важливу роль у забезпеченні ефективних та масштабованих додатків.

Розбираючи функцію fs.open, ми можемо оцінити складність і елегантність Node.js. Сподіваюся, ця стаття допоможе вам зрозуміти, як Node.js з'єднує JavaScript і системне програмування.

Перекладено з: Behind the Scenes of Node.js: A High-Level Overview