Node内部工作原理解析

Node is a runtime environment for executing JavaScript Code.
Node 既不是一种语言,也不是一个框架,而是一个能执行 JavaScript 代码的运行时环境。

最初的时候,Javascript 只能运行在浏览器中,靠的是 JS 引擎把 JavaScript 代码转成浏览器能识别的机器码,并且不同的浏览器用的是不同的引擎,如 IE 使用 Charkra,Firefox 使用 SpiderMonkey,Chrome 使用 V8,因此,有时候 js 在不同的浏览器中运行会有不同的效果。

这些 Javascript 引擎都遵循 ECMAScript 标准,Javascript 则是 ECMAScript 的一个方言版本,能够被web浏览器和许多其它的应用支持。

浏览器就是一个能够运行 Javascript 代码的运行时环境,我们知道在 js 中,我们能够访问全局变量 windowdocument,这些变量能够让我们与代码所运行的环境进行交互。

之后,Node的创始人 Ryan Dahl 有了一个大胆的想法,想着如果 javascript 在浏览器以外的地方也能运行就好了,因此,他就把速度最快的 javascript 引擎——Google的 V8 引擎嵌入到了一个C++程序中,并且把这个程序叫做 Node。因此,跟浏览器类似,Node 也是一个 JavaScript 代码的运行时环境,它包含了一个JS引擎,能够执行 JavaScript 代码。但是,跟浏览器不同的是,它还提供了能够支持其它功能的一些对象,没有了能够获取 DOM 的 document 对象(document.getElementById()),但是能够进行文件或者网络操作(fs.createFile()http.createServer() 等),这些操作就需要借助于其它的一些库来实现,如 libuv。

一、Node 目录及架构体系

node目录

在 github 中,我们可以看到 node 库的目录,其中:

  • deps:包含了node所依赖的库;
  • lib:包含了我们在项目中引入的用 javascript 定义的函数和模块;
  • src:lib 库对应的C++实现。
Node architecture

在 Node 官方文档的 Dependencies 中,我们也可以看到一些具体的所依赖的库的作用。

  • v8 引擎的作用就是将 js 转成 C++。
  • libuv 用于在C++中处理并发和进程构建,具有跨平台和异步能力。
  • c-ares:提供了异步处理 DNS 相关的能力。
  • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

接下来,我们就主要看两个依赖库:V8 和 Libuv。

V8

谷歌开源的 JavaScript 引擎,目的是使 JavaScript 能够在浏览器之外的地方运行。前面说过,Javascript引擎是一个能够将 Javascript 语言转换成浏览器能够识别的低级语言或机器码的程序。

Libuv

C++的开源项目,使 Node 能够访问操作系统的底层文件系统(file system),访问网络(networking)并且处理一些高并发相关的问题。

那么问题来了,既然 V8 能够让我们使用 JavaScript,libuv 给了我们一些操作系统、网络等层面的访问能力,我们还需要 Node 干嘛呢?

V8 大约70%由 C++ 实现,30%由 JavaScript 实现。
Libuv 100% 由 C++ 实现。

原因不难理解:

(1)因为V8 和 libuv 都并非是用 JavaScript 写的,对于我们前端儿来说,写 C++ 是头疼的事情,而 Node 为我们提供了一个很好的接口,用来将 javascript 应用程序的 javascript 端与运行在我们计算机上的实际 c++ 关联起来,从而实际地解释和执行 javascript 代码。

(2)Node 封装了一系列的 API 供我们使用,并且提供了一致的接口。

二、模块实现

让我们用实际的例子来说明一下 Node 到底是如何运行的:

  1. 选择 Node standard libary 中的一个函数;
  2. 在 node 源码中找到它的实现;
  3. 看下 V8 和 Libuv 是如何被用来实现函数功能的,即 node 是如何在 V8 和 Libuv 中利用和包装功能的。

选择一个函数:scrypt.js

scrypt.js 是 Crypto 模块中的一个函数,Crypto 模块通常用于对密码进行hash化处理。

在源码中,我们主要关注两个文件夹:

  • Lib —— 包含了我们在项目中引入的所有函数和模块的 JS 定义——JS side of node project

  • Src —— 在这个文件夹里面是所有函数的 c++ 实现,是 Node 实际导入 Libuv 和 V8 项目的地方,也是我们正在使用的所有函数和模块实际实现的地方,比如FS模块、Http模块等等。

在 lib 文件夹下找到 scrypt 的函数实现: node/lib/internal/crypto/scrypt.js

在这个 javascript 文件中,包含了对该函数的JS定义。这是 Node 标准库中的函数,就跟我们在任何 javascript 文件中编写的函数一样。

scrypt.js 文件中,你会发现 internalBinding() 函数,之前的版本是 Process.binding(),现在 node 团队将其改为了 internalBinding(),因为它们现在无法从用户空间访问,而只能从 NativeModule.require() 获得。

C++ binding Loaders

  1. process.binding(): 已成为历史的 c++ 绑定加载程序,可以从用户空间访问,因为它被附加到了全局对象上。这些 c++ 绑定是通过 NODE_BUILTIN_MODULE_CONTEXT_AWARE() 创建的,并且它们的 nm_flags 设置为 NM_F_BUILTIN。我们无法确保这些绑定的稳定性,因此需要时常处理它们引起的兼容性问题。
  2. process._linkedBinding(): 用于在其应用程序中添加额外的c++绑定。这些c++绑定可以通过带有 NM_F_LINKED 标志的 NODE_MODULE_CONTEXT_AWARE_CPP() 进行创建。
  3. internalBinding(): 私有的内部c++绑定加载程序,用户无法访问,只能通过NativeModule.require() 获得。这些c++绑定通过 NODE_MODULE_CONTEXT_AWARE_INTERNAL() 进行创建,并且它们的nm_flags 设置为 NM_F_INTERNAL

内部 JavaScript 模块加载器:NativeModule

该模块是用于加载 lib/**/*.jsdeps/**/*.js 中的JavaScript核心模块的最小模块系统。

所有核心模块都通过由 js2c.py 生成的 node_javascript.cc 编译成 Node 二进制文件,这样可以更快地加载它们,而不需要I/O成本。

这个类使 lib/internal/*deps/internal/* 模块和 internalBinding() 在默认情况下对核心模块可用,并且允许核心模块通过 require('internal/bootstrap/loaders') 来引用自身,即使这个文件不是用 CommonJS 风格编写的。

Process.binding / InternalBinding 实际上是C++函数,是用于将Node标准库中C++端和Javascript端连接起来的桥梁。

Process.binding() / internalBinding() 是如何工作的?

它们是 Node的 JS 端和 C++ 端之间的桥梁,也是 Node 为你实现大量内部工作的地方。你的很多代码最终都是依赖于c++代码的。

现在,让我们看一下在 src 文件夹中如何实现 Node 的 c++ 端:node/src/node_crypto.cc

node_crypto.cc —— crypto 模块所依赖的并位于 Node 的 c++ 部分的实际代码。

在该文件的最后,你会看到对 C++ setMethod() 的导出,这行代码最终将由internalBinding() / process.binding() 进行调用。

SetMethod of C++ implementation for Scrypt.

这在某种程度上是将Node的Javascript端与Node的c++端连接起来。

这是我们所写的函数实际实现的地方,100%纯c++代码。👊
现在我想你应该已经了解了,当我们运行Javascript代码,实际上它内部依赖的是c++代码。

现在,你可能对 V8 和 Libuv 是如何发挥作用的很好奇,那么,接下来,我们就一起来看一下。

在文件的顶部,我们可以看到有这么些代码:

使用 v8

这里引入了 V8::Array 等类型,可以看到,在 node 源码中使用 V8 的目的,本质上是作为一个完整的中介,允许在 JavaScript 中所定义的值被转换成等价的 C++ 值。

所有的 V8 语句都导入了 JS 概念的 C++ 定义。比如 C++ 对 JS 中的 False、Integer、null 或者 string 等的理解。

这就是实际的 V8 项目发挥作用的地方。V8 用于将我们在不同程序中的写的 JS 类型的值,如 Boolean值、false值、null值、object值等转换成C++中的值。

另一方面,Libuv 也在这里使用,但是不太容易被检测到,我们可以搜索 uv,从而找到使用的地方。在 node_crypto.cc 的例子中,Libuv 用于 C++ 端的并发和进程处理。

看到这里,我想大家对 Node 的内部工作原理应该有一些了解了。

最后,总结成一张图:

by Stephen Grider

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容