本文内容主要分为两大部分,第一部分是 Node.js 的基础和架构,第二部分是 Node.js 核心模块的实现。


Node.js 基础和架构

  • Node.js 的组成
  • Node.js 代码架构
  • Node.js 事件循环
组成

Node.js 主要由 V8、Libuv 和第三方库组成。

  • v8:实现js解析、执行、自定义扩展之类的
  • Libuv:跨平台的异步 IO 库,但它提供的功能不仅仅是 IO,还包括进程、线程、信号、定时器、进程间通信,线程池等
  • 第三方库:http,Dns解析之类的。
结构

nodejs技术架构 nodejs项目架构_子进程


上图是 Node.js 的代码架构,Node.js的代码主要分为 JS、C++、C 三种:

  1. JS 是我们平时使用的那些模块(http/fs)。
  2. C++ 代码分为三个部分,第一部分是封装了 Libuv 的功能,第二部分则是不依赖于 Libuv ( crypto 部分 API 使用了 Libuv 线程池),比如 Buffer 模块,第三部分是 V8 的代码。
  3. C 语言层的代码主要是封装了操作系统的功能,比如 TCP、UDP。
事件循环

事件循环主要分为 7 个阶段

timer 阶段: 用二叉堆实现,最快过期的在根节点,处理定时器相关。

pending 阶段:处理 Poll IO 阶段回调里产生的回调

check、prepare、idle 阶段:每次事件循环都会被执行。

Poll IO 阶段:处理文件描述符相关事件。

closing 阶段:执行调用 uv_close 函数时传入的回调,比如关闭服务器


Node.js 核心模块的实现

  • 进程和进程间通信
  • 线程和线程间通信
  • Cluster
  • Libuv 线程池
  • 文件
进程和进程的通信

Node.js 中的进程是使用 fork+exec 模式创建的,fork 就是复制主进程的数据,exec 是加载新的程序执行。Node.js 提供了异步和同步创建进程两种模式。

1、异步方式
异步方式就是创建一个人子进程后,主进程和子进程独立执行,互不干扰。在主进程的数据结构中如图所示,主进程会记录子进程的信息,子进程退出的时候会用到

2、同步方式
创建子进程会导致主进程阻塞,具体的实现是:
主进程中会新建一个新的事件循环结构体,然后基于这个新的事件循环创建一个子进程。
然后主进程就在新的事件循环中执行,旧的事件循环就被阻塞了。
子进程结束的时候,新的事件循环也就结束了,从而回到旧的事件循环。

进程之间的通信方式:
在操作系统中,进程间的虚拟地址是独立的,所以没有办法基于进程内存直接通信,这时候需要借助内核提供的内存。进程间通信的方式有很多种,管道、信号、共享内存等等。

Cluster

Node.js 是单进程架构的,不能很好地利用多核,Cluster 模块使得 Node.js 支持多进程的服务器架构。Node.s 支持轮询(主进程 accept )和共享(子进程 accept )两种模式,可以通过环境变量进行设置。多进程的服务器架构通常有两种模式,第一种是主进程处理连接,然后分发给子进程处理,第二种是子进程共享 socket,通过竞争的方式获取连接进行处理。

Libuv线程池

为什么需要使用线程池?文件 IO、DNS、CPU 密集型不适合在 Node.js 主线程处理,需要把这些任务放到子线程处理。

线程池的实现:

  • 线程池维护了一个待处理任务队列,多个线程互斥地从队列中摘下任务进行处理。
  • 当给线程池提交一个任务的时候,就是往这个队列里插入一个节点。
  • 当子线程处理完任务后,就会把这个任务插入到事件循环本身维护到一个已完成任务队列中,并且通过异步通信的机制通知主线程。
  • 主线程在 Poll IO 阶段就会执行任务对应的回调。
文件

Node.js 中文件操作分为同步和异步模式,同步模式就是在主进程中直接调用文件系统的 API,这种方式可能会引起进程的阻塞,异步方式是借助了 Libuv 线程池,把阻塞操作放到子线程中去处理,主线程可以继续处理其他操作。

Node.js 中文件监听提供了基于轮询和订阅发布两种模式。

轮询模式比较简单,他是使用定时器实现的,Node.js 会定时执行回调,在回调中比较当前文件的元数据和上一次获取的是否不一样,如果是则说明文件改变了。

第二种监听模式是更高效的 inotify 机制,inotify 是基于订阅发布模式的,避免了无效的轮询