探索Node.js内部

探索Node.js内部

时间:2020-7-11 作者:gykj

自2009年11月8日Ryan Dahl在欧洲JSConf上介绍Node.js以来,它已在整个技术行业得到广泛使用。Netflix,Uber和LinkedIn等公司对Node.js可以承受大量流量和并发性的说法表示信服。

掌握了基本知识,Node.js的初学者和中级开发人员在很多事情上都苦苦挣扎:“这只是运行时!” “它具有事件循环!” “ Node.js像JavaScript一样是单线程的!”

尽管其中一些说法是正确的,但我们将更深入地研究Node.js运行时,了解它如何运行JavaScript,查看它是否实际上是单线程的,最后,更好地了解其核心依赖项V8和libuv之间的互连。 。

先决条件

  • JavaScript的基础知识
  • 熟悉Node.js语义(requirefs

什么是Node.Js?

假设许多人对Node.js抱有信念可能是很诱人的,最常见的定义是它是JavaScript语言运行时。考虑到这一点,我们应该理解导致这一结论的原因。

Node.js通常被描述为C ++和JavaScript的组合。C ++部分由运行低级代码的绑定组成,这些绑定使访问连接到计算机的硬件成为可能。JavaScript部分将JavaScript作为其源代码,并在一种流行的语言解释器V8引擎中运行它。

有了这种了解,我们可以将Node.js描述为一个独特的工具,该工具结合了JavaScript和C ++以在浏览器环境之外运行程序。

但是实际上我们可以称它为运行时吗?为了确定这一点,让我们定义什么是运行时。

对StackOverflow的回答之一中,DJNA将运行时环境定义为“执行程序所需的一切,但无需任何工具即可对其进行更改”。根据这个定义,我们可以放心地说,我们运行代码(无论使用任何语言)时发生的所有事情都在运行时环境中运行。

其他语言也有自己的运行时环境。对于Java,它是Java运行时环境(JRE)。对于.NET,它是公共语言运行库(CLR)。对于Erlang,它是BEAM。

但是,其中一些运行时具有依赖于它们的其他语言。例如,Java具有Kotlin,这是一种编程语言,可以编译为JRE可以理解的代码。Erlang有长生不老药。我们知道.NET开发有许多变体,它们都在CLR中运行,称为.NET Framework。

现在我们知道运行时是为程序成功执行提供的环境,并且我们知道V8和大量C ++库使Node.js应用程序得以执行。Node.js本身是实际的运行时,它将所有内容绑定在一起以使这些库成为一个实体,并且它仅理解一种语言-JavaScript,而与Node.js所使用的语言无关。

Node.Js的内部结构

当我们尝试index.js使用command从命令行运行Node.js程序(例如)时node index.js,我们正在调用Node.js运行时。如前所述,该运行时由两个独立的依赖项V8和libuv组成。

核心Node.js依赖项
核心Node.js依赖关系(大预览

V8是由Google创建和维护的项目。它需要JavaScript源代码,并在浏览器环境之外运行。当我们通过node命令运行程序时,Node.js运行时会将源代码传递给V8以执行。

libuv库包含用于对操作系统进行低级访问的C ++代码。V8默认不提供诸如联网,写入文件系统和并发之类的功能,而V8是运行我们的JavaScript代码的Node.js的一部分。借助其库集,libuv在Node.js环境中提供了这些实用程序以及更多功能。

Node.js是将两个库结合在一起的粘合剂,因此成为唯一的解决方案。在脚本的整个执行过程中,Node.js会了解将控制权传递给哪个项目以及何时传递。

服务器端程序有趣的API

如果我们研究一下JavaScript的一些历史,就会知道它的意思是向浏览器中的页面添加一些功能和交互。在浏览器中,我们将与构成页面的文档对象模型(DOM)的元素进行交互。为此,存在一组API,统称为DOM API。

DOM仅存在于浏览器中。它是用来渲染页面的解析文件,它基本上是用称为HTML的标记语言编写的。而且,浏览器存在于一个窗口中,因此存在于一个window对象中,该对象充当JavaScript上下文中页面上所有对象的根。该环境称为浏览器环境,它是JavaScript的运行时环境。

Node.js API为某些功能调用libuv
Node.js API与libuv交互(大预览

在Node.js环境中,我们没有页面,也没有浏览器,这使我们对全局window对象的了解无效。我们所拥有的是一组与操作系统交互的API,以为JavaScript程序提供其他功能。这些API对于Node.js的(fspathbuffereventsHTTP,等),因为我们有他们,只存在于Node.js,它们通过Node.js的(本身运行时),以便我们可以运行编写的程序提供Node.js。

实验:如何fs.writeFile创建新文件

如果V8是为了在浏览器之外运行JavaScript而创建的,并且Node.js环境与浏览器没有相同的上下文或环境,那么我们将如何做类似访问文件系统或制造HTTP服务器的事情?

例如,让我们看一个简单的Node.js应用程序,该应用程序将文件写入当前目录中的文件系统:

const fs = require("fs")

fs.writeFile("./test.txt", "text");

如图所示,我们正在尝试将新文件写入文件系统。JavaScript语言不提供此功能。它仅在Node.js环境中可用。如何执行?

为了理解这一点,让我们浏览一下Node.js代码库。

转到Node.js的GitHub 存储库,我们看到了两个主要文件夹srclib。该lib文件夹具有JavaScript代码,该代码提供了漂亮的一组模块,默认情况下,每个Node.js安装中都包含这些模块。该src文件夹包含libuv的C ++库。

如果我们查看lib文件夹并浏览fs.js文件,我们将看到它充满了令人印象深刻的JavaScript代码。在1880行,我们将注意到一条exports声明。该语句导出我们可以通过导入fs模块访问的所有内容,并且可以看到它导出了名为的函数writeFile

搜索function writeFile((定义函数的位置)将导致我们转到第1303行,在该行中,我们看到该函数定义了四个参数:

function writeFile(path, data, options, callback) {
  callback = maybeCallback(callback || options);
  options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' });
  const flag = options.flag || 'w';

  if (!isArrayBufferView(data)) {
    validateStringAfterArrayBufferView(data, 'data');
    data = Buffer.from(data, options.encoding || 'utf8');
  }

  if (isFd(path)) {
    const isUserFd = true;
    writeAll(path, isUserFd, data, 0, data.byteLength, callback);
    return;
  }

  fs.open(path, flag, options.mode, (openErr, fd) => {
    if (openErr) {
      callback(openErr);
    } else {
      const isUserFd = false;
      writeAll(fd, isUserFd, data, 0, data.byteLength, callback);
    }
  });
}

在第13151324行,我们看到writeAll在进行一些验证检查之后调用了一个函数。我们在同一文件的1278行中找到此功能。fs.js

function writeAll(fd, isUserFd, buffer, offset, length, callback) {
  // write(fd, buffer, offset, length, position, callback)
  fs.write(fd, buffer, offset, length, null, (writeErr, written) => {
    if (writeErr) {
      if (isUserFd) {
        callback(writeErr);
      } else {
        fs.close(fd, function close() {
          callback(writeErr);
        });
      }
    } else if (written === length) {
      if (isUserFd) {
        callback(null);
      } else {
        fs.close(fd, callback);
      }
    } else {
      offset += written;
      length -= written;
      writeAll(fd, isUserFd, buffer, offset, length, callback);
    }
  });
}

还有趣的是,该模块正在尝试自行调用。我们在1280行看到它正在调用fs.write。寻找write功能,我们将发现一些信息。

write函数从571行开始,大约运行42行。我们在此函数中看到一个重复出现的模式:binding如第594612行所示,它在模块上调用函数的方式。binding模块上的功能不仅在此功能中被调用,而且实际上在fs.js文件文件中导出的任何功能中都被调用。必须有一些特别之处。

在文件的最上方第58行binding声明该变量,然后在GitHub的帮助下单击该函数调用即可显示一些信息。

绑定变量的声明
绑定变量的声明(大预览

internalBinding功能可以在名为loaders的模块中找到。加载程序模块的主要功能是加载所有libuv库,并将它们通过V8项目与Node.js连接。它是如何实现的,这是很神奇的,但是要了解更多信息,我们可以仔细查看模块writeBuffer调用的函数fs

我们应该查看它与libuv的连接位置以及V8的输入位置。在加载程序模块的顶部,一些好的文档说明了这一点:

// This file is compiled and run by node.cc before bootstrap/node.js
// was called, therefore the loaders are bootstraped before we start to
// actually bootstrap Node.js. It creates the following objects:
//
// C++ binding loaders:
// - process.binding(): the legacy C++ binding loader, accessible from user land
//   because it is an object attached to the global process object.
//   These C++ bindings are created using NODE_BUILTIN_MODULE_CONTEXT_AWARE()
//   and have their nm_flags set to NM_F_BUILTIN. We do not make any guarantees
//   about the stability of these bindings, but still have to take care of
//   compatibility issues caused by them from time to time.
// - process._linkedBinding(): intended to be used by embedders to add
//   additional C++ bindings in their applications. These C++ bindings
//   can be created using NODE_MODULE_CONTEXT_AWARE_CPP() with the flag
//   NM_F_LINKED.
// - internalBinding(): the private internal C++ binding loader, inaccessible
//   from user land unless through `require('internal/test/binding')`.
//   These C++ bindings are created using NODE_MODULE_CONTEXT_AWARE_INTERNAL()
//   and have their nm_flags set to NM_F_INTERNAL.
//
// Internal JavaScript module loader:
// - NativeModule: a minimal module system used to load the JavaScript core
//   modules found in lib/**/*.js and deps/**/*.js. All core modules are
//   compiled into the node binary via node_javascript.cc generated by js2c.py,
//   so they can be loaded faster without the cost of I/O. This class makes the
//   lib/internal/*, deps/internal/* modules and internalBinding() available by
//   default to core modules, and lets the core modules require itself via
//   require('internal/bootstrap/loaders') even when this file is not written in
//   CommonJS style.

我们在这里了解到的是,对于从bindingNode.js项目的JavaScript部分中的对象调用的每个模块,该src文件夹的C ++部分中都有与之等效的模块。

从我们的fs浏览中,我们看到执行此操作的模块位于node_file.cc。文件中定义了可通过模块访问的每个功能;例如,我们在writeBuffer线2258。在C ++文件中该方法的实际定义在第1785行。同样,可以在第18091815行中找到对libuv进行实际写入文件的部分的调用,其中libuv函数uv_fs_write被异步调用。

我们从这种理解中学到什么?

就像许多其他解释型语言运行时一样,Node.js的运行时也可以被黑客入侵。有了更多的了解,我们就可以通过查看源代码来完成标准发行版无法完成的工作。我们可以添加库来更改某些函数的调用方式。但最重要的是,这种理解是进一步探索的基础。

Node.Js是单线程的吗?

坐在libuv和V8上,Node.js可以访问浏览器中运行的典型JavaScript引擎所没有的一些其他功能。

在浏览器中运行的任何JavaScript都将在单个线程中执行。程序执行中的线程就像一个黑匣子,位于执行该程序的CPU上。在Node.js上下文中,某些代码可以在我们的计算机可以承载的尽可能多的线程中执行。

为了验证这一特定要求,让我们探索一个简单的代码片段。

const fs = require("fs");
// A little benchmarking
const startTime = Date.now()
fs.writeFile("./test.txt", "test", (err) => {
    If (error) {
        console.log(err)
    }
    console.log("1 Done: ", Date.now() — startTime)
});

在上面的代码段中,我们尝试在当前目录的磁盘上创建一个新文件。为了了解这可能需要多长时间,我们添加了一个基准测试来监视脚本的开始时间,这为我们提供了创建文件的脚本的持续时间(以毫秒为单位)。

如果运行上面的代码,我们将得到如下结果:

在Node.js中创建单个文件所需时间的结果
在Node.js中创建单个文件所花费的时间(大预览
$ node ./test.js
    -> 1 Done: 0.003s

这非常令人印象深刻:仅0.003秒。

但是,让我们做一些非常有趣的事情。首先,让我们复制生成新文件的代码,并更新log语句中的数字以反映它们的位置:

const fs = require("fs");
// A little benchmarking
const startTime = Date.now()
fs.writeFile("./test1.txt", "test", function (err) {
     if (err) {
        console.log(err)
    }
    console.log("1 Done: %ss", (Date.now() — startTime) / 1000)
});

fs.writeFile("./test2.txt", "test", function (err) {
     if (err) {
        console.log(err)
    }
    console.log("2 Done: %ss", (Date.now() — startTime) / 1000)
});


fs.writeFile("./test3.txt", "test", function (err) {
     if (err) {
        console.log(err)
    }
    console.log("3 Done: %ss", (Date.now() — startTime) / 1000)
});

fs.writeFile("./test4.txt", "test", function (err) {
     if (err) {
        console.log(err)
    }
    console.log("4 Done: %ss", (Date.now() — startTime) / 1000)
});

如果我们尝试运行此代码,我们将感到不解。这是我的结果:

创建多个文件所需时间的结果
一次创建多个文件(大预览

首先,我们将注意到结果不一致。其次,我们看到时间增加了。发生了什么?

低级任务被委派

众所周知,Node.js是单线程的。Node.js的某些部分是用JavaScript编写的,而其他则是用C ++编写的。Node.js使用了我们在浏览器环境中熟悉的事件循环和调用堆栈的相同概念,这意味着Node.js的JavaScript部分是单线程的。但是需要与操作系统对话的低级任务不是单线程的。

低级任务通过libuv委托给OS
Node.js低级任务委托(大型预览

当Node.js将调用识别为针对libuv的调用时,它将此任务委托给libuv。在操作中,libuv的某些库需要线程,因此在需要时在执行Node.js程序时使用线程池。

默认情况下,libuv提供的Node.js线程池中有四个线程。我们可以通过process.env.UV_THREADPOOL_SIZE在脚本顶部调用来增加或减少该线程池。

// script.js
process.env.UV_THREADPOOL_SIZE = 6;

// …
// …

我们的文件制作程序会发生什么

看来,一旦我们调用代码创建文件,Node.js就会到达其代码的libuv部分,这将为该任务分配一个线程。libuv中的此部分在处理文件之前会获取有关磁盘的一些统计信息。

此统计检查可能需要一段时间才能完成;因此,线程将被释放以执行其他一些任务,直到完成统计检查为止。检查完成后,libuv节将占用任何可用线程,或者等待直到某个线程变得可用。

我们只有四个调用和四个线程,因此有足够多的线程可以使用。唯一的问题是每个线程处理任务的速度如何。我们将注意到,第一个进入线程池的代码将首先返回其结果,并且在运行其代码时会阻塞所有其他线程。

结论

现在我们了解了Node.js是什么。我们知道这是一个运行时。我们已经定义了什么是运行时。我们已经深入研究了由Node.js提供的运行时的组成部分。

我们已经走了很长一段路。然后,通过对GitHub上Node.js存储库的少量浏览,我们可以按照此处采取的相同过程来探索我们可能感兴趣的任何API。Node.js是开源的,所以我们可以肯定会进入源代码,不是吗?

即使我们已经谈到了Node.js运行时中发生的几个低级问题,我们也不能假设我们已经知道了一切。以下资源指向一些我们可以用来建立知识的信息:

  • Node.js简介
    作为一个官方网站,Node.dev解释了什么是Node.js及其包管理器,并列出了在其之上构建的Web框架。
  • “ JavaScript&Node.js ”,Node入门书Manuel Kiessling的
    这本书在警告浏览器中的JavaScript与Node.js中的JavaScript不一样之后,解释了Node.js做了出色的工作,即使两者都是用相同的语言编写。
  • 入门 Node.js这本入门书超出了对运行时的解释。它介绍了有关软件包和流以及如何使用Express框架创建Web服务器的知识。
  • LibUV
    这是Node.js运行时支持的C ++代码的正式文档。
  • V8
    这是JavaScript引擎的官方文档,它使使用JavaScript编写Node.js成为可能。
版权所有:https://www.eraycloud.com 转载请注明出处