Node.js内部:Node.js运行时和内部架构介绍

1,885 阅读6分钟

Node.js 是一个开源的跨平台 JavaScript 运行时环境,用于在浏览器外执行 JavaScript。它由谷歌的 V8 引擎提供支持,这使得它的性能非常出色。

异步事件驱动的runtime

​ 介绍 Node 时,我们遇到的最常见的语句之一是它在单线程上运行。 也就是说,每个人都可能想知道 Node 怎么可能成为构建快速且可扩展的 API 的最受欢迎的工具之一?

​ 从技术上讲,Node.js 使用单线程的事实并非 100% 正确。 Node 实际上使用了很多线程,但是event loop事件循环(我们将在后面提到)和用户代码在一个线程上运行。 如果我们仔细阅读文档,我们会看到 Node 使用了一个事件驱动的、非阻塞的 I/O 模型,这使它变得轻量级和高效。

​ 什么是事件驱动的非阻塞 I/O 模型?

​ 在Node 的指南中得知,阻塞方法同步执行,非阻塞方法异步执行。 假设我们必须编写一些代码来读取文件的内容并在控制台中打印出来。 在 Node 中有两种方法可以做到:同步和异步。 我们先看同步版本:

Read file sync

​ 上面的代码做了以下事情: 首先,它需要 FS module。 在第二行中,调用了 readFileSync 方法并将结果存储在 data 变量中。 Node 的主线程阻塞在这一行,直到文件的所有内容都被读取。 然后将内容记录在控制台上,最后将打印“完成”语句。 现在让我们看看异步执行的相同代码:

Read file async

​ 在此示例中,使用了 readFile 方法并且它异步执行。 一旦遇到这一行,控制权就会传递给 Libuv,在那里读取文件。 这不是在主线程上完成的。 相反,使用 Libuv 线程池中的工作线程(默认为 4 个线程)。 读取完成后,相应的回调会被推送到事件循环使用的队列中。 在事件循环的下一次迭代中,在回调执行阶段,这个回调将被推送到 V8 的调用堆栈并最终被执行。 所有这些工作都是在后台完成的,Node 的主线程只负责执行回调。 因此,回到上面的示例,“Done”语句将首先打印,然后将记录读取文件的结果。 这就是“非阻塞 I/O”的含义,这就是为什么在您阅读的每个 Node.js 指南中,人们都建议使用异步方法而不是他们的同步版本。

Node 的行为与其他 Web 服务器有何不同?

​ 与多线程服务器相比,Node 的事件驱动运行时的行为有很大不同。在多线程服务器中,每个连接都会产生一个新线程来处理请求,并且该线程内完成的所有工作都可以阻塞,而不会影响其他连接(即您可以查询数据库并等待结果,然后执行一些其他工作)。由于现在每个 CPU 都有许多内核,因此这种方法很好地利用了处理器能力。然而,也存在许多挑战。在多线程环境中,每个线程都会增加一些开销,因为它需要内存,这意味着可以使用的线程数量有限。如果达到此限制会发生什么?新连接最终会超时。除此之外,如果应用程序主要受 I/O 限制,则每个线程都会浪费大量时间等待来自网络或磁盘的结果。另一方面,Node 在单个线程中处理所有事情。与我们上面解释的文件操作类似,事件循环充当调度程序,不断侦听新事件并将工作委托给内核或其他工作线程。它从不阻塞(除非被告知这样做)。因此,服务器可以接受新的客户端连接,然后做一些其他工作,然后再次继续接受新的客户端连接。客户端连接不需要分配新线程,它只需要一个由内核管理的socket handler。这种方法快速、轻量级且可扩展性很强,这也是 Node 可以处理高并发的主要原因。

Node’s runtime 架构

​ Node 的运行时被设计为多层的,其中用户代码位于顶部,每一层都使用下面层提供的 API

Node.js runtime architecture

Node.js runtime architecture

  1. 用户代码:由程序员编写的 Javascript 应用程序代码。
  2. Node.js API: Node 提供的内置方法,可以在用户代码中使用(例如 用于使用 HTTP 方法的 HTTP modules、crypto module、用于文件系统操作的 fs module、用于网络请求的 net 等……)。 有关 Node 提供的方法的完整列表,您可以在此处查看文档。 此外,您可以在此处找到源代码实现。 Node 的 API 是用 Javascript 编写的。
  3. Bindings 和 C++扩展插件: 在阅读 Node 时,您会看到 V8 是用 C++ 编写的,Libuv 是用 C 编写的,等等。 基本上,所有模块都是用 C 或 C++ 编写的,因为这些语言在处理底层任务和使用 OS API 方面非常出色和快速。 但是上层的Javascript代码怎么可能用其他语言去写代码呢? 这就是 bindings 的作用。 它们充当两层之间的粘合剂,因此 Node 可以顺利使用 C 或 C++ 编写的低级代码。 那么,如果我们想自己添加一个C++模块应该怎么做呢? 我们首先用 C++ 实现模块,然后为此编写 bindings代码。 我们编写的这段代码称为扩展插件。 更多信息可以在这里找到。
  4. Node’s 依赖: 这一层代表 Node 使用的底层库。 最大的依赖是谷歌的 V8 引擎和 Libuv。 其他库包括 OpenSSL(用于 SSL、TLS 和其他基本加密功能)、HTTP 解析器(用于解析 HTTP 请求和响应)、C-Ares(用于异步 DNS 请求)和 Zlib(用于快速压缩和解压缩)。
  5. 操作系统: 这是表示上述库所使用的 OS API(系统调用)的最底层。 由于 OS-es 不同,这些库包括 Windows 和 Unix 变体的实现,这使得 Node 平台独立。

关于 V8 和 Libuv

​ Libuv 是一个用 C 语言编写的库,用于抽象非阻塞 I/O 操作。 它提供以下功能:

  • 事件循环
  • 异步 TCP 和 UDP sockets
  • 异步 DNS 解析
  • 异步文件和文件系统操作
  • 线程池
  • 子进程
  • High-resolution clock
  • Threading and synchronization primitives
  • 轮询
  • Streaming
  • Pipes

V8 是为 Node.js 提供 Javascript 引擎的库。 它是一个 JIT(Just-in-time)编译器,这意味着它会在编译和运行代码块之间连续切换。 编译和运行交替的好处是它可以在运行代码的同时收集信息,并根据收到的数据推测未来会发生什么。 这些推测对于编译更好的代码很有用。