В сочетании с анализом исходного кода загрузка модуля Node.js и принцип работы

Node.js JavaScript

Появление Node.js освободило JavaScript от оков браузеров и открыло широкие возможности для серверной разработки. Введение Node.js в модульную спецификацию CommonJS сделало JavaScript языком, который действительно можно адаптировать к крупномасштабным проектам.

Использование модулей в Node.js очень просто, и у нас почти есть этот опыт в нашей ежедневной разработке: написать фрагмент кода JavaScript, потребовать несколько нужных пакетов, а затем экспортировать экспорт продукта кода. Однако знаем ли мы что-нибудь о принципах загрузки и работы, лежащих в основе модульности Node.js? Сначала задайте следующие вопросы:

  • Какие типы файлов поддерживаются модулями в Node.js?
  • В чем разница между процессом загрузки и запуска основных модулей и сторонних модулей?
  • Как написать модуль расширения C/C++, отличный от модуля JavaScript?
  • ...

В этой статье я объединим исходный код Node.js, чтобы изучить ответы на эти вопросы.

1. Тип модуля Node.js

В Node.js модули в основном можно разделить на следующие типы:

  • Базовый модуль: содержится в исходном коде Node.js и скомпилирован в исполняемый двоичный файл Node.js Модуль JavaScript, также называемый собственным модулем, например широко используемый http,
    фс и т.д.
  • Модули C/C++, также называемые встроенными модулями, обычно не вызываются напрямую, а вызываются в собственных модулях, и тогда нам требуется
  • Нативные модули, такие как buffer, fs, os и другие нативные модули, которые мы обычно используем в Node.js, все вызывают встроенные модули внизу.
  • Сторонние модули: Модули, которые поставляются с исходным кодом, отличным от Node.js, могут в совокупности называться сторонними модулями, такими как экспресс, веб-пакет и т. д.
    • Модули JavaScript, это самые распространенные, обычно мы пишем модули JavaScript при разработке
    • Модуль JSON, это очень просто, просто файл JSON
    • Модуль расширения C/C++, написанный на C/C++, с суффиксом .node после компиляции.

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

2. Краткий обзор структуры исходного кода Node.js

Здесь мы используем исходный код версии Node.js 6.x в качестве примера для анализа. Перейдите на github, чтобы загрузить соответствующую версию исходного кода Node.js, вы можете увидеть, что общая структура кода выглядит следующим образом:

├── AUTHORS
├── BSDmakefile
├── BUILDING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── GOVERNANCE.md
├── LICENSE
├── Makefile
├── README.md
├── android-configure
├── benchmark
├── common.gypi
├── configure
├── deps
├── doc
├── lib
├── node.gyp
├── node.gypi
├── src
├── test
├── tools
└── vcbuild.bat

в:

  • ./libПапка в основном содержит различные файлы JavaScript, и все наши часто используемые нативные модули JavaScript находятся здесь.
  • ./srcПапка в основном содержит исходные файлы C/C++ для Node.js, многие из которых являются здесь встроенными модулями.
  • ./depsПапка содержит различные библиотеки, от которых зависит Node.js, обычно v8, libuv, zlib и т. д.

Релизная версия, которую мы используем в разработке, на самом деле представляет собой исполняемый файл, скомпилированный из исходного кода. Если мы хотим внести некоторые персонализированные настройки в Node.js, мы можем изменить исходный код, а затем запустить и скомпилировать, чтобы получить настроенную версию Node.js. Взяв за пример платформу Linux, вот краткое введение в процесс компиляции Node.js.

Прежде всего, нам необходимо знать организационные инструменты, используемые для составления, а именноgyp. В исходном коде Node.js мы видимnode.gyp, содержимое этого файла представляет собой некоторую JSON-подобную конфигурацию, написанную на python, которая определяет ряд задач строительного проекта. Давайте возьмем пример, где одно из полей выглядит следующим образом:

{
      'target_name': 'node_js2c',
      'type': 'none',
      'toolsets': ['host'],
      'actions': [
        {
          'action_name': 'node_js2c',
          'inputs': [
            '<@(library_files)',
            './config.gypi',
          ],
          'outputs': [
            '<(SHARED_INTERMEDIATE_DIR)/node_natives.h',
          ],
          'conditions': [
            [ 'node_use_dtrace=="false" and node_use_etw=="false"', {
              'inputs': [ 'src/notrace_macros.py' ]
            }],
            ['node_use_lttng=="false"', {
              'inputs': [ 'src/nolttng_macros.py' ]
            }],
            [ 'node_use_perfctr=="false"', {
              'inputs': [ 'src/perfctr_macros.py' ]
            }]
          ],
          'action': [
            'python',
            'tools/js2c.py',
            '<@(_outputs)',
            '<@(_inputs)',
          ],
        },
      ],
    }, # end node_js2c

Основная роль этой задачи исходит из названияnode_js2cВидно, что это преобразование JavaScript в код C/C++. Мы обратимся к этой задаче ниже.

Сначала скомпилируйте Node.js, вам нужно заранее установить некоторые инструменты:

  • gcc и g++ 4.9.4 и выше
  • лязг и лязг++
  • Python 2.6 или 2.7, здесь следует отметить, что это могут быть только эти две версии, а не python 3+
  • GNU MAKE 3.81 и выше

С помощью этих инструментов, чтобы войти в исходный каталог Node.js, нам просто нужно последовательно выполнить следующие команды:

./configuration
make
make install

Вы можете скомпилировать и сгенерировать исполняемый файл и установить его.

3. Отnode index.jsНачинать

Начнем с самого простого случая. Предположим, есть файл index.js, содержащий только одну простую строку.console.log('hello world')код. при входеnode index.jsjs, как Node.js компилирует и запускает этот файл?

При вводе команды Node.js вызывается основная функция в исходном коде Node.js.src/node_main.ccсередина:

// src/node_main.cc
#include "node.h"

#ifdef _WIN32
#include <VersionHelpers.h>

int wmain(int argc, wchar_t *wargv[]) {
    // windows下面的入口
}
#else
// UNIX
int main(int argc, char *argv[]) {
  // Disable stdio buffering, it interacts poorly with printf()
  // calls elsewhere in the program (e.g., any logging from V8.)
  setvbuf(stdout, nullptr, _IONBF, 0);
  setvbuf(stderr, nullptr, _IONBF, 0);
  // 关注下面这一行
  return node::Start(argc, argv);
}
#endif

Этот файл предназначен только для записи и различает среды Windows и Unix. Возьмем для примера Unix, в основной функции последний вызовnode::Start, это вsrc/node.ccВ файле:

// src/node.cc

int Start(int argc, char** argv) {
  // ...
  {
    NodeInstanceData instance_data(NodeInstanceType::MAIN,
                                   uv_default_loop(),
                                   argc,
                                   const_cast<const char**>(argv),
                                   exec_argc,
                                   exec_argv,
                                   use_debug_agent);
    StartNodeInstance(&instance_data);
    exit_code = instance_data.exit_code();
  }
  // ...
}
// ...

static void StartNodeInstance(void* arg) {
    // ...
    {
        Environment::AsyncCallbackScope callback_scope(env);
        LoadEnvironment(env);
    }
    // ...
}
// ...

void LoadEnvironment(Environment* env) {
    // ...
    Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
                                                        "bootstrap_node.js");
    Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
    if (try_catch.HasCaught())  {
        ReportException(env, try_catch);
        exit(10);
    }
    // The bootstrap_node.js file returns a function 'f'
    CHECK(f_value->IsFunction());
    Local<Function> f = Local<Function>::Cast(f_value);
    // ...
    f->Call(Null(env->isolate()), 1, &arg);
}

Весь файл относительно длинный.В приведенном выше сегменте кода перехватывается только тот сегмент процесса, на который нам нужно обратить внимание больше всего.Отношения вызова следующие:
Start -> StartNodeInstance -> LoadEnvironment.

существуетLoadEnvironmentЭто требует нашего внимания, главное, чтобы вытащитьbootstrap_node.jsСтрока кода в , преобразованная в функцию и, наконец, переданнаяf->Callвыполнить.

Хорошо, вот и настал момент, мы наконец-то видим первый файл JavaScript с момента запуска Node.js.bootstrap_node.js, мы также можем видеть из имени файла, что это файл начального уровня. Итак, давайте посмотрим, путь к файлуlib/internal/bootstrap_node.js:

// lib/internal/boostrap_node.js
(function(process) {

  function startup() {
    // ...
    else if (process.argv[1]) {
      const path = NativeModule.require('path');
      process.argv[1] = path.resolve(process.argv[1]);

      const Module = NativeModule.require('module');
      // ...
      preloadModules();
      run(Module.runMain);
    }
    // ...
  }
  // ...
  startup();
}

// lib/module.js
// ...
// bootstrap main module.
Module.runMain = function() {
  // Load the main module--the command line argument.
  Module._load(process.argv[1], null, true);
  // Handle any nextTicks added in the first tick of the program
  process._tickCallback();
};
// ...

Здесь мы все же уделяем внимание основному процессу, как видите,bootstrap_node.js, выполняетstartup()функция. пройти черезprocess.argv[1]получить имя файла, в нашемnode index.jsсередина,process.argv[1]очевидноindex.js, а затем позвонитеpath.resolveРазберите путь к файлу. в конце,run(Module.runMain)для компиляции и выполнения нашегоindex.js.

а такжеModule.runMainфункция определена вlib/module.js, в конце приведенного выше фрагмента кода указана эта функция, и видно, что в основном она вызываетсяModule._loadзагрузить и выполнитьprocess.argv[1].

Когда мы анализируем требования модуля ниже, мы также придем кlib/module.js, также будет анализироватьModule._load.Таким образом, мы видим, что процесс Node.js, запускающий файл, фактически в конце такжеrequireПроцесс работы с файлом можно понимать как немедленное требование файла.Разберем принцип require.

4. Ключ к принципу загрузки модуля: требуется

Идем дальше, полагая, что нашаindex.jsИмеет следующее:

var http = require('http');

Так что же происходит, когда эта строка кода выполняется?

Определение require все еще существуетlib/module.jsсередина:

// lib/module.js
// ...
Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, /* isMain */ false);
};
// ...

requireМетоды определены в цепочке прототипов модуля. Вы можете видеть, что в этом методе вызовModule._load.

Мы снова здесь так скороModule._loadДавайте посмотрим, что делает этот ключевой метод:

// lib/module.js
// ...
Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

  var filename = Module._resolveFilename(request, parent, isMain);

  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }

  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  tryModuleLoad(module, filename);

  return module.exports;
};
// ...

Поток этого кода относительно ясен, в частности:

  1. По имени файла вызовитеModule._resolveFilenameРазобрать путь к файлу
  2. Кэш просмотраModule._cacheЕсть ли этот модуль в, если да, вернитесь напрямую
  3. пройти черезNativeModule.nonInternalExistsОпределите, является ли модуль основным модулем, если это основной модуль, вызовите метод загрузки основного модуля.NativeModule.require
  4. Если это не основной модуль, создайте новый объект модуля и вызовитеtryModuleLoadфункциональный загрузочный модуль

Давайте сначала посмотрим наModule._resolveFilename, понимание этого метода очень полезно для понимания принципа разбора пути к файлу в Node.js:

// lib/module.js
// ...
Module._resolveFilename = function(request, parent, isMain) {
  // ...
  var filename = Module._findPath(request, paths, isMain);
  if (!filename) {
    var err = new Error("Cannot find module '" + request + "'");
    err.code = 'MODULE_NOT_FOUND';
    throw err;
  }
  return filename;
};
// ...

существуетModule._resolveFilenameпозвонил вModule._findPath, логика оценки загрузки модуля фактически сосредоточена в этом методе.Поскольку этот метод длинный, непосредственно прикрепите код метода github:

GitHub.com/node будет /node…

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

  • Сначала сгенерируйте cacheKey, определите, существует ли соответствующий кеш, и вернитесь напрямую, если он существует.
  • если последний символ пути не/:
    • Если путь является файлом и существует, вернуть путь к файлу напрямую
    • Если путь является каталогом, вызовитеtryPackageфункция для анализа каталогаpackage.json, а затем удалитеmainПуть к файлу, в который записывается поле
      • Если путь суждения существует, вернитесь напрямую
      • Попробуйте добавить три суффикса .js, .json, .node к пути, чтобы определить, существует ли он, и вернуться, если он существует.
      • Попробуйте добавить index.js, index.json, index.node после пути, чтобы определить, существует ли он, и вернуться, если он существует.
    • Если это не удалось, попробуйте добавить суффикс .js, .json, .node непосредственно к текущему пути.
  • Если последний символ пути/:
    • перечислитьtryPackage, процесс синтаксического анализа аналогичен описанному выше
    • Если это не удалось, попробуйте последовательно добавить index.js, index.json, index.node к пути, чтобы определить, существует ли он, и вернуться, если он существует.

используется при разборе файловtryPackageа такжеtryExtensionsссылка на github метода:
GitHub.com/node будет /node… GitHub.com/node будет /node…

Весь процесс можно представить на следующей картинке:

После завершения анализа пути к файлу проверьте, существует ли кеш в соответствии с путем к файлу, и вернитесь напрямую, если он существует.Если он не существует, перейдите к шагу 3 или 4.

Здесь сгенерированы две ветки 3, 4, то есть основной модуль и метод загрузки стороннего модуля отличаются. Так как мы предполагаем насindex.jsЧжунвэйvar http = require('http'), http — это основной модуль, поэтому давайте сначала проанализируем эту ветку, загруженную основным модулем.

4.1 Принцип загрузки основного модуля

Основной модуль черезNativeModule.requireЗагружен, NativeModule определен вbootstrap_node.js, прикрепите ссылку на гитхаб:
GitHub.com/node будет /node…

Как видно из кода,NativeModule.requireПроцесс выглядит следующим образом:

  1. Определить, был ли загружен кеш, и если да, вернуть экспорт напрямую
  2. Создайте новый объект nativeModule, затем кэшируйте, загрузите и скомпилируйте

Во-первых, давайте посмотрим, как скомпилировать, из кода это называетсяcompileметод, находясь вNativeModule.prototype.compileметод, сначала поNativeModule.getSourceПолучен исходный код загружаемого модуля, так как же получить исходный код? посмотриgetSourceОпределение метода:

// lib/internal/bootstrap_node.js
  // ...
  NativeModule._source = process.binding('natives');
  // ...
  NativeModule.getSource = function(id) {
    return NativeModule._source[id];
  };

Непосредственно изNativeModule._sourceПолучил, а где это присваивается? Он также перехватывается в приведенном выше коде черезNativeModule._source = process.binding('natives')полученный.

Вот введение в то, как хранится собственный код модуля JavaScript. Когда исходный код Node.js скомпилирован, инструмент js2c.py, входящий в состав v8, будет использоваться для преобразования кода модуля js в папке lib в массив на языке C, и будет создан заголовочный файл node_natives.h для запишите массив:

namespace node {
  const char node_native[] = {47, 47, 32, 67, 112 …}

  const char console_native[] = {47, 47, 32, 67, 112 …}

  const char buffer_native[] = {47, 47, 32, 67, 112 …}

  …

}

struct _native {const char name;  const char* source;  size_t source_len;};

static const struct _native natives[] = {

  { “node”, node_native, sizeof(node_native)-1 },

  {“dgram”, dgram_native, sizeof(dgram_native)-1 },

  {“console”, console_native, sizeof(console_native)-1 },

  {“buffer”, buffer_native, sizeof(buffer_native)-1 },

  …

  }

в то время как вышеNativeModule._source = process.binding('natives');Функция состоит в том, чтобы вынуть этот массив туземцев и назначить егоNativeModule._source, так что вgetSourceможно напрямую использовать имя модуля в качестве индекса для извлечения исходного кода модуля из массива.

Здесь мы вставляем обзор вышесказанного, когда мы представили компиляцию Node.js, мы представилиnode.gyp, и одна из задачnode_js2cВ то время автор упомянул, что, судя по названию, эта задача заключается в преобразовании JavaScript в код C, а код C в массиве нативов здесь является продуктом этой задачи построения. И вот, наконец, мы знаем роль этой задачи компиляции.

Знайте получение исходного кода, продолжайте смотреть внизcompileметод, чтобы увидеть, как компилируется исходный код:

// lib/internal/bootstrap_node.js
  NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
  };

  NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
  ];

  NativeModule.prototype.compile = function() {
    var source = NativeModule.getSource(this.id);
    source = NativeModule.wrap(source);

    this.loading = true;

    try {
      const fn = runInThisContext(source, {
        filename: this.filename,
        lineOffset: 0,
        displayErrors: true
      });
      fn(this.exports, NativeModule.require, this, this.filename);

      this.loaded = true;
    } finally {
      this.loading = false;
    }
  };
  // ...

NativeModule.prototype.compileПосле получения исходного кода он в основном делает:wrapМетод обрабатывает исходный код, затем вызывает runInThisContext для компиляции функции и, наконец, выполняет функцию. Метод обертывания заключается в добавлении головы и хвоста к исходному коду, что фактически эквивалентно обертыванию исходного кода в функцию, параметры которой — экспорт, требование, модуль и так далее. Вот почему нам не нужно определять экспорт, требование, модуль, когда мы пишем модули можно использовать напрямую.

До сих пор процесс загрузки основного модуля Node.js был в основном объяснен. Говоря об этом, у вас могут возникнуть сомнения: вышеприведенный процесс анализа, по-видимому, задействует только нативный модуль JavaScript в основном модуле, а как насчет встроенного модуля C/C++?

На самом деле для встроенных модулей они импортируются не через require, а черезprecess.binding('模块名')представил. Как правило, мы редко используем его непосредственно в собственном коде.process.bindingимпортировать встроенные модули, но черезrequireНа собственный модуль ссылаются, а встроенный модуль вводится в собственный модуль. Например, наш часто используемый буферный модуль C/C++ вводится в его внутреннюю реализацию. встроенный модуль, чтобы обойти ограничение памяти v8:

// lib/buffer.js
'use strict';

// 通过 process.binding 引入名为 buffer 的 C/C++ built-in 模块
const binding = process.binding('buffer');
// ...

Таким образом, мыrequire('buffer')Фактически встроенный модуль C/C++ используется косвенно.

Снова здесьprocess.binding! На самом деле метод process.binding определен вnode.ccсередина:

// src/node.cc
// ...
static void Binding(const FunctionCallbackInfo<Value>& args) {
  // ...
  node_module* mod = get_builtin_module(*module_v);
  // ...
}
// ...
env->SetMethod(process, "binding", Binding);
// ...

BindingКлючевым шагом в этой функции являетсяget_builtin_module. Здесь нам нужно снова вставить, чтобы представить метод хранения встроенных модулей C/C++:

В Node.js встроенные модули создаются с помощьюnode_module_structопределяется структурой. Таким образом, встроенные модули помещаются в файл с именемnode_module_listв массиве. а такжеprocess.bindingроль использованияget_builtin_moduleПолучите соответствующий встроенный код модуля из этого массива.

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

Вспоминая то, что мы представили в начале, нативный модуль хранится в каталоге lib/ в исходном коде, а встроенный модуль хранится в исходном коде в каталоге src/ На следующем рисунке сортируются нативные и построенные из перспектива компиляции Как -in модули компилируются в исполняемые файлы Node.js:

4.2 Принцип загрузки сторонних модулей

Перейдём ко второй ветке, предполагая, что нашаindex.jsТребуется не http, а пользовательский модуль, тогда в module.js мы перейдем к методу tryModuleLoad:

// lib/module.js
// ...
function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
    }
  }
}
// ...
Module.prototype.load = function(filename) {
  debug('load %j for module %j', filename, this.id);

  assert(!this.loaded);
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
};
// ...

посмотреть здесь,tryModuleLoadна самом деле называетсяModule.prototype.loadОпределенный метод, главное в этом методе - определить расширение имени файла, а затем вызвать разные расширения для разных расширений.Module._extensionsметод для загрузки и компиляции модулей. Тогда давайте посмотримModule._extensions:

// lib/module.js
// ...
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(internalModule.stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};


//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  return process.dlopen(module, path._makeLong(filename));
};
// ...

Видно, что всего поддерживается три типа загрузки модулей: .js, .json и .node. Среди них метод загрузки файла типа .json является самым простым, непосредственное чтение содержимого файла, а затемJSON.parseЗатем верните объект.

Давайте посмотрим на обработку .js, в первую очередь черезfsМодуль синхронно считывает содержимое файла, а затем вызываетmodule._compile, посмотрите на соответствующий код:

// lib/module.js
// ...
Module.wrap = NativeModule.wrap;
// ...
Module.prototype._compile = function(content, filename) {
  // ...

  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  // ...
  var result = compiledWrapper.apply(this.exports, args);
  if (depth === 0) stat.cache = null;
  return result;
};
// ...

первый звонокModule.wrapОберните исходный код, а затем вызовитеvm.runInThisContextМетод компилируется и выполняется и, наконец, возвращает значение экспорта. и изModule.wrap = NativeModule.wrapИз этого предложения видно, что метод переноса стороннего модуля такой же, как метод переноса базового модуля. Давайте вспомним только что упомянутый код ключа загрузки основного js-модуля:

// lib/internal/bootstrap_node.js
 NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
  };

  NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
  ];

  NativeModule.prototype.compile = function() {
    var source = NativeModule.getSource(this.id);
    source = NativeModule.wrap(source);

    this.loading = true;

    try {
      const fn = runInThisContext(source, {
        filename: this.filename,
        lineOffset: 0,
        displayErrors: true
      });
      fn(this.exports, NativeModule.require, this, this.filename);

      this.loaded = true;
    } finally {
      this.loading = false;
    }
  };

Сравнивая хэтчбеки, выясняется, что компиляция и выполнение исходного кода между ними практически идентичны. С точки зрения общего процесса, самая большая разница между основным модулем JavaScript и сторонним модулем JavaScript заключается в том, что исходный код основного модуля JavaScript передается черезprocess.binding('natives')извлекается из памяти, а исходный код стороннего модуля JavaScriptfs.readFileSyncспособ чтения из файла.

Наконец, давайте рассмотрим загрузку сторонних модулей C/C++ (суффикс .node). Интуитивно это очень просто, просто позвонитеprocess.dlopenметод. Этот метод определен вnode.ccсередина:

// src/node.cc
// ...
env->SetMethod(process, "dlopen", DLOpen);
// ...
void DLOpen(const FunctionCallbackInfo<Value>& args) {
  // ...
  const bool is_dlopen_error = uv_dlopen(*filename, &lib);
  // ...
}
// ...

на самом деле звонитDLOpenфункция, важнейшая из которых заключается в использованииuv_dlopenМетод открывает библиотеку динамической компоновки, а затем загружает модуль C/C++.uv_dlopenметод определен вlibuvв библиотеке.libuvБиблиотека представляет собой кроссплатформенную библиотеку асинхронного ввода-вывода. Для функции динамической загрузки модулей расширения под платформой *nix фактически вызывается метод dlopen(), определенный в dlfcn.h, а под Windows Ниже это метод LoadLibraryExW().Под двумя платформами они загружают файлы .so и .dll соответственно, в то время как в Node.js эти файлы имеют одинаковые имена с суффиксом .node, который скрывает различия платформ.

оlibuvБиблиотеки являются основной движущей силой асинхронного ввода-вывода в Node.js. Сама эта часть достойна изучения как отдельная тема, поэтому я не буду обсуждать ее здесь.

На данный момент мы прояснили процесс загрузки и компиляции трех сторонних модулей.

5. Сценарии разработки и применения модулей расширения C/C++

Выше был проанализирован процесс загрузки различных модулей в Node.js. Все должны быть знакомы с разработкой модулей JavaScript, но они могут быть незнакомы с разработкой модулей расширения C/C++. В этом разделе кратко представлена ​​разработка модуля расширения и рассказывается о сценариях его применения.

Что касается разработки модулей расширения Node.js, в документации официального сайта Node.js есть специальный раздел.Вы можете перейти на документацию официального сайта, чтобы просмотреть:узел будет .org/docs/latest…. Вот просто пример приветствия, чтобы представить некоторые из наиболее важных концепций написания модулей расширения:

Предположим, мы хотим расширить модуль, чтобы получить функцию, эквивалентную следующей функции JavaScript:

module.exports.hello = () => 'world';

Сначала создайтеhello.ccфайл, напишите следующий код:

// hello.cc
#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
}

void init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, init)

}  // namespace demo

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

Впервые представлен в началеnode.h, это заголовочный файл, который необходимо использовать при написании расширений Node.js.Он содержит почти все необходимые нам библиотеки и типы данных.

Во-вторых, я видел многоusing v8:xxxтакой код. Мы знаем, что Node.js основан на движке v8, а движок v8 написан на C++. Если мы хотим разработать модули расширения C++, нам нужно использовать многие типы данных, представленные в v8, и эта серия кодов заявляет, что нам нужно использовать эти типы данных в пространстве имен v8.

Тогда см.Methodметод, его тип параметраFunctionCallbackInfo<Value>& args, этот аргумент является параметром, переданным из JavaScript, в то же время, если вы хотитеMethodВ возвращаемой переменной JavaScript вам нужно вызватьargs.GetReturnValue().Setметод.

Далее вам нужно определить метод инициализации модуля расширения, вотInitфункция, только одно простое предложениеNODE_SET_METHOD(exports, "hello", Method);, что означает присвоение имени экспортуhelloметод, конкретное определение этого методаMethodфункция.

И, наконец, определение макроса:NODE_MODULE(NODE_GYP_MODULE_NAME, init), первый параметр — имя нужного модуля расширения, а второй параметр — метод инициализации модуля.

Чтобы скомпилировать этот модуль, нам нужно установить его через npm.node-gypинструмент сборки. Инструмент сочетает в себеgypИнструмент-оболочка для создания расширений Node.js. После установки этого инструмента мы добавляем файл с именемbingding.gypКонфигурационный файл, для нашего примера файл нужно только написать так:

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "hello.cc" ]
    }
  ]
}

Таким образом, бегиnode-gyp buildМодуль расширения может быть скомпилирован. Во время этого процессаnode-gypОн также отправится в указанный каталог (обычно ~/.node-gyp) для поиска некоторых заголовочных файлов и файлов библиотек нашей текущей версии Node.js.Если он не существует, он также поможет нам перейти к файлу Node. js официальный сайт для загрузки. Таким образом, при написании расширения передайте#include <>, мы можем напрямую использовать все заголовочные файлы Node.js.

Если компиляция прошла успешно, он будет в текущей папкеbuild/Release/увидеть путьaddon.node, это наш скомпилированный требуемый модуль расширения.

Из вышеприведенного примера мы можем примерно увидеть режим работы модуля расширения, он может получать параметры из JavaScript, а затем в середине можно вызывать возможности языка C/C++ для выполнения различных операций и обработки, и, наконец, результаты могут быть возвращены в JavaScript.

Стоит отметить, что разные версии Node.js зависят от разных версий v8, что приводит к различиям во многих API, поэтому в процессе разработки расширений с использованием нативного C/C++ также требуется обработка совместимости для разных версий Node. js. Например, чтобы объявить функцию, в версиях ниже v6.x и v0.12 нужно написать:

Handle<Value> Example(const Arguments& args); // 0.10.x
void Example(FunctionCallbackInfo<Value>& args); // 6.x

Видно, что объявление функции, в том числе и способ записи параметров в функцию, не совпадают. Это заставляет людей думать, что при разработке Node.js для написания ES6 также необходимо использовать Babel, чтобы помочь с преобразованием совместимости. Итак, в области разработки расширений Node.js есть ли такая библиотека, как Babel, которая помогает нам решать проблемы совместимости? Ответ — да, его имя — NAN (Native Abstraction for Node.js). По сути, это набор макросов, которые помогают нам обнаруживать разные версии Node.js и вызывать разные API. Например, с помощью NAN, объявляя функцию, нам больше не нужно об этом думать Node.js, вам просто нужно написать такой фрагмент кода:

#include <nan.h>

NAN_METHOD(Example) {
  // ...
}

Макрос NAN будет автоматически оцениваться во время компиляции, и разные результаты будут расширены в соответствии с разными версиями Node.js, что решит проблему совместимости. Для более подробного ознакомления с NAN заинтересованные студенты могут перейти на домашнюю страницу проекта на github:github.com/nodejs/nan.

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

Автор грубо суммирует несколько типов сценариев, в которых применим C/C++:

  1. Приложения с интенсивными вычислениями. Мы знаем, что модель программирования Node.js представляет собой один поток + асинхронный ввод-вывод, в котором один поток делает его слабым местом в приложениях с интенсивными вычислениями.Большое количество вычислений блокирует основной поток JavaScript, что приводит к невозможности ответа на другие запросы. Для этого сценария вы можете использовать модуль расширения C/C++ для ускорения вычислений, ведь скорость выполнения движка v8 хоть и высока, но все же уступает C/C++. Кроме того, использование C/C++ также может позволить нам открыть многопоточность и избежать блокировки основного потока JavaScript.В сообществе уже есть несколько многопоточных решений Node.js, основанных на модулях расширения, и самые популярные из них можно назватьthread-a-gogo, вы можете перейти на github для получения подробной информации:GitHub.com/hardwork/node-Тянь Хайронг….
  2. Приложения, потребляющие много памяти. Node.js основан на v8, а v8 изначально разрабатывался для браузеров, поэтому он имеет строгие ограничения по памяти, поэтому для некоторых приложений, требующих большой памяти, может быть несколько бессильно напрямую основываться на v8. В настоящее время вам нужно чтобы использовать модуль расширения, чтобы обойти ограничение памяти версии 8. Наиболее типичным из них является наш часто используемый модуль buffer.js, нижний уровень которого также вызывает C++ и применяется для памяти на уровне C++, чтобы избежать узкого места памяти v8.

Что касается первого пункта, автор также использует собственные расширения Node.js и Node.js для реализации тестового примера для сравнения производительности вычислений. Тестовый пример представляет собой классический расчет последовательности Фибоначчи.Во-первых, используйте собственный язык Node.js для реализации функции для расчета последовательности Фибоначчи, названной какfibJs:

function fibJs(n) {
    if (n === 0 || n === 1) {
        return n;
    }
    else {
        return fibJs(n - 1) + fibJs(n - 2);
    }
}

Затем используйте C++ для написания функции расширения, которая реализует ту же функцию с именемfibC:

// fibC.cpp
#include <node.h>
#include <math.h>

using namespace v8;

int fib(int n) {
    if (n == 0 || n ==1) {
        return n;
    }
    else {
        return fib(n - 1) + fib(n - 2);
    }
}

void Method(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();

    int n = args[0]->NumberValue();
    int result = fib(n);
    args.GetReturnValue().Set(result);
}

void init(Local < Object > exports, Local < Object > module) {
    NODE_SET_METHOD(module, "exports", Method);
}

NODE_MODULE(fibC, init)

В тесте используйте эти две функции для вычисления последовательности Фибоначчи от 1 до 40:

function testSpeed(fn, testName) {
    var start = Date.now();
    for (var i = 0; i < 40; i++) {
        fn(i);
    }
    var spend = Date.now() - start;
    console.log(testName, 'spend time: ', spend);
}

// 使用扩展模块测试
var fibC = require('./build/Release/fibC'); // 这里是扩展模块编译产物的存放路径
testSpeed(fibC, 'c++ test:');

// 使用 JavaScript 函数进行测试
function fibJs(n) {
    if (n === 0 || n === 1) {
        return n;
    }
    else {
        return fibJs(n - 1) + fibJs(n - 2);
    }
}
testSpeed(fibJs, 'js test:');

// c++ test: spend time:  1221
// js test: spend time:  2611

После нескольких тестов среднее время, затрачиваемое на модули расширения, составляет около 1,2 с, а время, затрачиваемое на модули JavaScript, составляет около 2,6 с. Видно, что в этом сценарии производительность расширения C/C++ по-прежнему намного выше.

Конечно, эти пункты основаны только на понимании автора. В реальном процессе разработки, когда вы сталкиваетесь с проблемами, вы также можете попытаться подумать, можно ли решить проблему лучше, если вы используете модуль расширения C/C++.

Эпилог

Прочитав статью, вернемся назад и посмотрим на поставленные в начале вопросы, были ли на них даны ответы в ходе анализа статьи? Давайте еще раз рассмотрим логику этой статьи:

  • сначала сnode index.jsПринцип работы начинается с того, что с помощьюnodeЗапуск файла эквивалентен немедленному однократному выполнению.require.
  • Затем вводится метод require в узле, где выделяются основные модули, встроенные модули и неосновные модули, а также подробно описываются принципы процесса загрузки и компиляции. В этом процессе он также включает описание точек знаний, таких как разрешение пути к модулю, кэширование модуля и так далее.
  • Наконец, вводится разработка модулей расширения c/c++, с которыми вы не знакомы, и используется пример сравнения производительности, чтобы проиллюстрировать его применимые сценарии.

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

Изучение принципов Node.js — долгий путь. Читателям, которые понимают базовый модульный механизм, рекомендуется узнать больше о v8, libuv и т. д., что будет очень полезно для опытных пользователей Node.js.