认识 Node.js

Node.js 是当今网站开发中非常流行的一种技术,它以简单易学、开发成本低、高并发等特点而深受广大开发者欢迎,本节将对 Node.js 的基本概念、工作原理、优缺点,以及应用领域等进行介绍。

什么是 Node.js

Node.js(简称 Node)是一个开源的、基于 Chrome V8 引擎的服务器端 JavaScript 运行时环境,可以在浏览器环境以外的主机上解释和运行 JavaScript 代码,它发布于 2009 年 5 月,由谷歌工程师 Ryan Dahl 开发。Node.js 支持现在大部分的主流浏览器,包括 ChromeMicrosoft EdgeOpera 等。Node.js 主要由标准库、中间层和底层库这 3 部分组成,其架构如图1.1所示。

image 2024 04 08 21 07 48 486
Figure 1. 图1.1 Node.js架构图

下面分别对图 1.1 中的 Node.js 结构层进行介绍。

  • 标准库(Node standard library):提供了开发人员能够直接进行调用并使用的一些 API,如 http 模块、stream 流模块、fs 文件系统模块等,可以使用 JavaScript 代码直接调用。

  • 中间层(Node binding):由于 Node.js 的底层库采用 C/C++ 实现,而标准库中的 JavaScript 代码无法直接与 C/C++ 进行通信,因此提供了中间层,它在标准库和底层库之间起到了桥梁的作用,它封装了底层库中 V8 引擎和 libuv 等的实现细节,并向标准库提供基础 API 服务。

  • 底层库(C/C++ 实现):底层库是 Node.js 运行的关键,它由 C/C++ 实现,包括 V8 引擎、libuvC-aresOpenSSLzlib 等,它们的主要作用如下。

    • V8 引擎:Google 的一个开源的 JavaScriptWebAssembly 引擎,使用 C++ 语言编写,用于 Chrome 浏览器和 Node.js 等。V8 引擎主要是为了提高 JavaScript 的运行效率,因此它采用了提前编译的方式,将 JavaScript 编译为原生机器码,这样在执行阶段程序的执行效率可以完全媲美二进制程序。

    • libuv:一个专门为 Node.js 量身打造的跨平台异步 I/O 库,使用 C 语言编写,提供了非阻塞的文件系统、DNS、网络、子进程、管道、信号、轮询和流式处理机制。Node.js 会通过中间层将用户的 JavaScript 代码传递给底层库的 V8 引擎进行解析,然后通过 libuv 进行循环调度,最后再返回给调用 Node.js 标准库的应用。

    • C-ares:一个用来处理异步 DNS 请求的库,使用 C 语言编写,对应 Node.jsdns 模块提供的 resolve() 系列方法。

    • OpenSSL:一个通用的加密库,通常用于网络传输中的 TLSSSL 协议实现,对应 Node.js 中的 tlscrypto 模块。

    • zlib:一个提供压缩和解压支持的底层模块。

Node.js 中,libuv 发挥着十分重要的作用,具体如下:

  • libuv 使用各平台提供的事件驱动模块实现异步,这使得它可以支持 Node.js 应用的非文件 I/O 模块,并把相应的事件和回调封装成 I/O 观察者放到底层的事件驱动模块中。当事件触发时,libuv 会执行 I/O 观察者中的回调。

  • libuv 实现了一个线程池来支持 Node.js 中的文件 I/ODNS、用户异步等操作。

Node.js 的工作原理

通过上一节的讲解,我们了解了 Node.js 的基本技术架构,本节进一步讲解 Node.js 的工作原理。

事件驱动

Node.js 采用一种独特的事件驱动思想,将 I/O 操作作为事件响应,而不是阻塞操作,从而实现了事件函数的快速执行与错误处理。由于 Node.js 能够采用异步非阻塞的方式访问文件系统、数据库、网络等外部资源,因此,它能够高效地处理海量的并发请求,极大地提高了应用程序的吞吐量。

单线程

Node.js 采用单线程模型,只需要轻量级的线程即可处理大量的请求。与多线程模型相比,这种模型消除了线程之间的竞争,使得程序的稳定性大幅度提升。在 Node.js 的单线程模型中,所有的 I/O 操作都被放在事件队列中,一旦事件出现,Node.js 就会依次处理它们。事实上,大多数网站的服务器端都不会做太多的计算,它们接收到请求以后,把请求交给其他服务来处理(如读取数据库),然后等待结果返回,再把结果发给客户端。因此,Node.js 针对这一事实采用了单线程模型来处理,它不会为每个接入请求分配一个线程,而是用一个主线程处理所有的请求,然后对 I/O 操作进行异步处理,避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。

非阻塞I/O

在传统的 I/O 操作(例如,读取或写入磁盘文件,或者对远程服务器进行网络调用)中,当数据读取或写入操作发生时,程序会被阻塞,等数据读取或写入操作完成后才能进入下一步操作。但是,在 Node.js 中,所有的 I/O 操作都是非阻塞的,当某个 I/O 操作发生时,不是等待其执行完成才能进入下一步操作,而是直接回调相应的函数,从而实现了对外部资源的高效访问。

事件循环

Node.js 采用了一种特殊的设计方式—事件循环,它在工作线程池中维护一个任务队列,当接到请求后,将该请求作为一个事件放入这个队列中,然后继续接收其他请求,同时,Node.js 程序会不断地从工作队列中获取要执行的事件,并通过事件循环流程对其进行处理。图1.2给出了 Node.js 中事件循环的工作原理。

image 2024 04 08 21 19 15 672
Figure 2. 图1.2 Node.js中事件循环的工作原理

事件循环的主要工作阶段如下。

  1. 计时器:处理由 setTimeout()setInterval() 设置的回调。

  2. 回调:运行挂起的回调函数。

  3. 轮询:检索传入的 I/O 事件并运行与 I/O 相关的回调。

  4. 检查:完成轮询后立即运行回调。

  5. 关闭回调:关闭事件和回调。

无论是在 Linux 平台还是 Windows 平台上,Node.js 内部都是通过线程池来完成异步 I/O 操作的,而 libuv 针对不同平台的差异性实现了统一调用,因此 Node.js 的单线程仅仅是指 JavaScript 运行在单线程中,而并非 Node.js 是单线程的。

模块化设计

Node.js 中,采用了一种模块化的设计方式,按照功能模块将代码拆分成多个文件,使用 require 函数引入,从而提高了代码的复用率,同时也增强了代码的可维护性。另外,Node.js 提供了许多内置模块,如 http 模块、fs 模块等,能够帮助开发者快速搭建 Web 应用。

Node.js 的优缺点

作为一种能够同时进行前端和后端开发的 “年轻” 编程语言,Node.js 既有优点也有缺点,下面分别进行介绍。

Node.js 的优点如下。

  • 前后端一体化开发:Node.js 使用 JavaScript 作为开发语言,使得前端和后端都可以使用同一种语言进行开发,从而提高开发效率和代码的可维护性。

  • 丰富的模块库:Node.js 的生态系统非常丰富,拥有大量的第三方模块,使得开发者可以快速构建出各种类型的应用。

  • 轻量级:Node.js 采用模块化开发方式,使得应用程序可以轻松地分解成小模块,从而提高了可维护性和可扩展性。

  • 易部署:使用 Node.js 开发的应用程序可以轻松地部署到各种云端平台上。

Node.js 的缺点如下。

  • 缺少严格的类型检查:Node.js 是基于 JavaScript 的,它没有严格的类型检查,这既是它的优点,也是它的缺点,优点是开发自由度很高,但缺点是程序出现问题时,检查调试会比较困难。

  • 可靠性不如传统后端语言:由于 Node.js 的相对年轻和快速迭代,它在可靠性和稳定性方面,相对传统后端语言(如Java、C语言、C#等)还有一定的差距。

  • CPU 密集型任务表现不佳:由于 Node.js 的单线程模型,当需要进行大量的 CPU 密集型计算时,可能会出现性能瓶颈,导致程序的运行效率下降。

Node.js 能做什么

使用 Node.js 可以生成以下类型的应用程序。

  • HTTP Web 服务器。

  • 微服务或无服务器 API 后端。

  • 用于数据库访问和查询的驱动程序。

  • 交互式命令行接口。

  • 桌面应用程序。

  • 实时物联网(IoT)客户端和服务器端。

  • 适用于桌面应用程序的插件。

  • 用于文件处理或网络访问的 Shell 脚本。

  • 机器学习库和模型。

谁在使用 Node.js

前端最流行的 JavaScript 正在一步步走入后端,得益于 V8 引擎,Node.jsJavaScript 运行在后端提供了运行环境,因此,它正在吸引越来越多的公司来使用它,比如用它创建协作工具、聊天工具、社交媒体应用程序等。

据不完全统计,现在已经有越来越多的国际和国内知名公司在内部使用了 Node.js 技术,如流媒体视频网站 Netflix、在线支付平台 PayPal、社交平台 LinkedInNode.js 专业中文社区 CNode、购物平台淘宝网、腾讯官网等。