Node.js 平台介绍


Node.js 最有特点的是它的异步特性,这使得它的代码风格大量使用回调(callback)和 promise。 Node.js 的另一个特型是它自身的准则。

Node.js 准则

Node.js 准则,也就是以 “符合Node 的方式” 来设计、编写代码。

核心功能小

Node.js 的核心(core)— 可以理解为 Node.js 的运行时和内置模块,它有自己的构建原则。

  • 拥有尽可能少的功能集,将更多的空间留给社区和使用者。

尽可能小的模块

Node.js 使用模块(module)作为构建程序代码的基本手段。Node.js 其中一项原则就是设计小的模块和包。 依赖于包管理工具 — 例如 npmyarn 等,它可以程序依赖大量小的、聚焦特定功能模块,而不会产生冲突。 小的模块、包具有一些优点:

  • 可重用性;
  • 易理解、使用;
  • 易于测试和维护;
  • 尺寸小,有利于在浏览器使用。

更小的接口

在Node.js中,定义模块的一个非常常见的模式是只公开一个功能,比如一个函数或一个类,因为它提供了一个单一的、明确无误的入口点。

Node.js 工作原理

I/O 操作很慢

I/O 操作,也就是基本的输入输出操作,它是计算机基本操作中最慢的。它在请求发送到设备以及操作完成的时刻都增加了延迟。另外还要考虑操作人的因素,例如点击鼠标、打字输入等操作。

I/O 阻塞

在传统的阻塞 I/O 编程中,调用 I/O 请求的函数会阻塞当前线程,直到 I/O 操作完成。

例如一个 Web 服务器,如果使用阻塞 I/O 的方式编写,那么无法在一个线程上同时处理多个请求。需要使用多线程来分别处理不同的请求。不幸的是,就系统资源而言,线程并不便宜——它消耗内存并导致上下文切换——因此,为每个连接设置一个长时间运行的线程,并且在大多数时间不使用它意味着浪费宝贵的内存和CPU。

非阻塞 I/O

在非阻塞 I/O模式下,系统调用总是立即返回,不必等待数据被读取或写入。如果在调用时没有可用的结果,则该函数将简单地返回一个预定义的常量,表明此时没有可用的数据。

处理非阻塞 I/O 的最基本模式是在循环中主动轮询资源,直到返回一些实际数据。这就是所谓的 bysy-waiting(忙等待)

事件解复用

忙等待的方式不是最佳的处理非阻塞 I/O 的方式。大多数现代操作系统都提供了另一种方式,叫做 synchronous event demultiplexer (同步事件解复用器),也可称为 event notification interface (事件通知接口)。

在电信中,多路复用(multiplexing)是指将多个信号组合为一个信号,这样可以在容量有限的介质中传播。解复用(Demultiplexing)指的是相反的操作,即信号再次被分割成其原始分量。

同步事件解复用器会监视多个资源,并在对其中一个资源执行读或写操作完成时返回一个新事件(或一组事件)。下面是一段伪代码:

watchedList.add(socketA, FOR_READ)
watchedList.add(fileB, FOR_READ)
while (events = demultiplexer.watch(watchedList)) {
	// 事件循环
	for (event of events) {
		// 读取不会阻塞,会马上返回 data
		data = event.resource.read()
		if (data === RESOURCE_CLOSED) {
			// 资源已经关闭,从监控列表中移除
			demultiplexer.unwatch(event.resource)
		} else {
			// 获取到了实际的数据,进行处理
			consumeData(data)
		}
	}
}

使用上面的模式就可以在单个线程中处理多个 I/O 操作。这么做的优势是最小化线程的空闲时间。

反应器模式

反应器模式(The reactor pattern)的主要思想是与每一个 I/O 模式关联一个处理函数。这个处理函数在 Node.js 中就是一个回调(callback)。

反应器模式的处理流程如下图所示:

  1. 应用程序向 事件解复用器(Event Demultiplexer) 提交一个请求来生成一个新的 I/O 操作。同时应用程序指定一个 处理函数(Execute Handler),当操作完成的时候调用。向事件解复用器提交一个请求是非阻塞的,它会立刻将控制权返回给应用程序。
  2. 当一系列的 I/O 操作完成之后,事件解复用器会将一系列的事件推入 事件队列(Event Queue)
  3. 此时,事件循环会遍历事件队列中的项。
  4. 对于每一个事件,相关联的处理函数会调用。
  5. 处理函数是应用程序代码的一部分,当它处理完成之后会叫控制权交还给事件循环(5a)。当处理程序执行时,它可以请求新的异步操作(5b),导致新项目被添加到事件解复用器(1)。
  6. 当处理完事件队列中的所有项后,事件循环再次阻塞事件解复用器,当有新事件可用时,事件解复用器触发另一个循环。

异步处理已经比较清晰了。应用程序在某个时间点(没有阻塞的)访问资源,并提供一个处理程序,当操作完成时,将在另一个时间点调用该处理程序。

当事件解复用器没有更多要处理的操作,事件循环没有待处理的事件时候,Node.js 应用程序将会退出。

Node.js 的 I/O 处理引擎 Libuv

Node.js 核心团队创建了一个名为 libuv 的本地库,目的是使 Node.js 与所有主要的操作系统兼容,并规范不同类型资源的非阻塞行为。 libuv 是 Node.js 的底层的 I/O 引擎,是 Node.js 的重要组成。

除了抽象底层系统调用之外,libuv 还实现了反应器模式,从而为创建事件循环、管理事件队列、运行异步 I/O 操作以及为其他类型的任务排队提供了 API。

uvbook - libuv 的介绍

Node.js 的组成

Node.js 的组成:

  • 反应器模式;
  • libuv;
  • 一组绑定(Bindings),负责将 libuv 和其他底层功能暴露给 JavaScript;
  • V8 引擎;
  • 一组 JavaScript 实现的核心库,用来实现 Node.js API。

Node.js 中的 JavaScript

Node.js 中的 JavaScript 和浏览器中有所不同。它没有 DOM,也没有 windowdocument。另外,Node.js 可以访问底层系统提供的一些服务,这些在浏览器中也没有。

放心使用最新的 JavaScript 特性

在 Node.js 中不需要考虑不同的浏览器所造成的特性不支持。因为我们明确知道代码运行的系统和 Node.js 的版本。同时 Node.js 附带了最新版本的 V8,意味着可以不需要额外的编译即可使用最新的 ECMAScript 版本。

模块系统

Node.js 自带模块系统,最初的模块系统是 CommonJS

目前,JavaScript 有了 ES Module。Node.js 继承了相同的语法,但是底层实现略有不同。

可实现操作系统的完全访问

Node.js为底层操作系统提供的所有主要服务提供绑定。例如 fs 模块访问文件系统;使用netdgram 模块,我们可以编写使用低级 TCP 或 UDP 套接字的应用程序,等等。

运行原生代码

提供了 N-API 接口,可以访问本机的 C/C++ 代码。 虽然 V8 处理 JavaScript 非常快,但是对于 CPU 计算密集型的任务,交给原生代码更合适。