<返回更多

Node.js架构剖析

2020-04-30    
加入收藏

一.Node.js 缔造的传奇

I have a job now, and this guy is the reason why I have that now. His hobby project is what I use for living. Thanks. —— Shajan Jacob

2009 年 Ryan Dahl 在 JSConf EU 大会上推出了 Node.js,最初是希望能够通过异步模型突破传统 Web 服务器的高并发瓶颈,之后愈渐发展成熟,应用越来越广,出现了繁荣的 Node.js 生态

借助 Node.js 走出浏览器之后,JAVAScript 语言也 一发不可收拾 :

Any Application that can be written in JavaScript, will eventually be written in JavaScript. —— Jeff Atwood

(摘自 The Principle of Least Power )

早在 2017 年,NPM 就凭借茫茫多的社区模块成为了 世界上最大的 package registry ,目前模块数量已经 超过 125 万 ,并且仍在快速增长中(每天新增900多个)

甚至 Node.js 工程师已经成为了一种新兴职业,那么, 带有传奇色彩的 Node.js 本身是怎么实现的呢?

二.Node.js 架构概览

Node.js架构剖析

 

JS 代码跑在 V8 引擎上,Node.js 内置的 fs 、 http 等核心模块通过 C++ Bindings 调用 libuv、c-ares、llhttp 等 C/C++类库,从而接入操作系统提供的平台能力

其中, 最重要的部分是 V8 和 libuv

三.源码依赖

V8

V8 is google’s open source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others.

一个用 C++写的 JavaScript 引擎,由 Google 维护,用于 Chrome 浏览器和 Node.js

libuv

libuv is cross-platform support library which was originally written for Node.js. It’s designed around the event-driven asynchronous I/O model.

为 Node.js 量身打造,用 C 写的跨平台异步 I/O 库,提供了非阻塞的文件系统、DNS、网络、子进程、管道、信号、轮询和流式处理机制:

Node.js架构剖析

 

对于无法在操作系统层面异步去做的工作,通过线程池来完成,如文件 I/O、DNS 查询等,具体原因见 Complexities in File I/O

P.S.线程池的容量可以配置,默认是 4 个线程,具体见 Thread pool work scheduling

此外, Node.js 中的事件循环、事件队列也都是由 libuv 提供的 :

Libuv provides the entire event loop functionality to NodeJS including the event queuing mechanism.

具体运作机制如下图:

Node.js架构剖析

 

其它依赖库

另外,还依赖一些 C/C++库:

P.S.关于 Node.js 源码依赖的更多信息,见 Dependencies

四.核心模块

像浏览器提供的 DOM/BOM API 一样,Node.js 不仅提供了 JavaScript 运行时环境,还扩展出了一系列平台 API,例如:

这些内置模块称为 核心模块,为迈出浏览器世界的 JavaScript 长上了手脚

五.C++ Bindings

在核心模块之下,有一层 C++ Bindings,将上层的 JavaScript 代码与下层 C/C++类库桥接起来

底层模块为了更好的性能,采用 C/C++实现,而上层的 JavaScript 代码无法直接与 C/C++通信,因而需要一个桥梁(即 Binding):

Bindings, as the name implies, are glue codes that “bind” one language with another so that they can talk with each other. In this case (Node.js), bindings simply expose core Node.js internal libraries written in C/C++ (c-ares, zlib, OpenSSL, llhttp, etc.) to JavaScript.

另一方面,通过 Bindings 也可以复用可靠的老牌开源类库,而不必手搓所有底层模块

以文件 I/O 为例,读取当前 JS 文件内容并输出到标准输出:

// readThisFile.js
const fs = require('fs')
const path = require('path')
const filePath = path.resolve(__filename);

// Parses the buffer into a string
function callback (data) {
  return data.toString()
}

// Transforms the function into a promise
const readFileAsync = (filePath) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, (err, data) => {
      if (err) return reject(err)
      return resolve(callback(data))
    })
  })
}

(() => {
  readFileAsync(filePath)
    .then(console.log)
    .catch(console.error)
})()

然而,其中用到的 fs.readFile 接口既不是 V8 提供的,也不是 JS 自带的,而是由 Node.js 以 C++ Binding 的形式借助 libuv 实现的:

// https://github.com/nodejs/node/blob/v14.0.0/lib/fs.js#L58
const binding = internalBinding('fs');
// https://github.com/nodejs/node/blob/v14.0.0/lib/fs.js#L71
const { FSReqCallback, statValues } = binding;

// https://github.com/nodejs/node/blob/v14.0.0/lib/fs.js#L297
function readFile(path, options, callback) {
  callback = maybeCallback(callback || options);
  options = getOptions(options, { flag: 'r' });
  if (!ReadFileContext)
    ReadFileContext = require('internal/fs/read_file_context');
  const context = new ReadFileContext(callback, options.encoding);
  context.isUserFd = isFd(path); // File descriptor ownership

  const req = new FSReqCallback();
  req.context = context;
  req.oncomplete = readFileAfterOpen;

  if (context.isUserFd) {
    process.nextTick(function tick() {
      req.oncomplete(null, path);
    });
    return;
  }

  path = getValidatedPath(path);
  const flagsNumber = stringToFlags(options.flags);
  binding.open(pathModule.toNamespacedPath(path),
              flagsNumber,
              0o666,
              req);
}

最后的 binding.open 是一个 C++调用,用来打开文件描述符,三个参数分别是文件路径, C++ fopen 的文件访问模式串(如 r 、 w+ ),以及八进制格式的文件读写权限( 666 表示每个人都有读写权限),和接收返回数据的 req 回调

其中, internalBinding 是个 C++ binding loader, internalBinding('fs') 实际加载的 C++代码位于 node/src/node_file.cc

至此,关键的部分差不多都清楚了,那么,一段 Node.js 代码究竟是怎样运行的呢?

六.运行原理

首先,编写的 JavaScript 代码由 V8 引擎来运行,运行中注册的事件监听会被保留下来,在对应的事件发生时收到通知

网络、文件 I/O 等事件产生时,已注册的回调函数将排到事件队列中,接着被事件循环取出放到调用栈上,回调函数执行完(调用栈清空)之后,事件循环再取一个放上去……

执行过程中遇到 I/O 操作就交给 libuv 线程池中的某个 woker 来处理,结束之后 libuv 产生一个事件放入事件队列。事件循环处理到返回事件时,对应的回调函数才在主线程开始执行,主线程在此期间继续其它工作,而不阻塞等待

Node.js 就像一家咖啡馆,店里只有一个跑堂的(主线程),一大堆顾客涌过来的时候,会排队等候(进入事件队列),到号的顾客订单会被传给经理(libuv),经理将订单分配给咖啡师(worker 线程),咖啡师用不同的原料和工具(底层依赖的 C/C++模块)来制作订单要求的各种咖啡,一般会有 4 个咖啡师值班,高峰时候可能会增加一些。订单传给经理后,不等咖啡做出来,而是接着处理下一个订单。一杯咖啡做完之后,放到出餐流水线(IO Events 队列),送达前台后,跑堂的喊名字,顾客过来取

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>