精华 Node.js挖掘之一:从两个角度,一个小例子浅析Node.js架构
发布于 4 年前 作者 LanceHBZhang 16327 次浏览 最后一次编辑是 3 年前 来自 分享

最近周播剧《盗墓笔记》挺火。而我最近在干两件事,一是追《盗墓笔记》,二是研究Node.js。Node.js研究了也有一段时间了,把我的研究结果发到博客上来,写成"Node.js挖掘"系列,虽然挖掘node没有盗墓那么惊心动魄,但收获确是一样的丰厚。

从学习方法来看,无论我们学习一个新平台,或者接触一个新的产品,一开始不需要着急看细节,而是要从架构,从全局的角度来俯视这个新事物,如果一开始就陷入细节中,就会让我们迷失方向。这就好像我们去一个陌生的城市,了解它的第一件事是先看地图全貌,然后再慢慢找街道,这样我们才不至于迷路。 当然细节非常重要,但细节的掌握是建立在对全局的把握之上,否则还是井底之蛙。

本文从两个角度查看Node.js的架构。一个是从模块直接的依赖关系角度,另一个是以一个建立一个TCP服务为例,从函数调用流程角度。 Node.js挖掘系列后面的几篇会详细地介绍各个方面。

1. 从模块依赖关系看Node.js架构

Node.js-architecture.jpg

图1:Node.js模块依赖关系

1.1 Host environment

如果我们自己去下载Node.js的源代码,并且编译的话,会发现node.js会最终生成一个可执行文件node-v0.12.4/out/Release/node。 既然是可执行文件,其运行时必然依赖执行环境。图1最下方标示的是Host environment,也即宿主环境, node 运行需要OS提供各种服务,如文件访问,socket编程,OpenSSL,多进程,多线程,锁,异步I/O库等等。

1.2 Node.js

图1中间的部分展示了Node.js的主要组成部分:V8 engine, libuv, builtin modules, natives modules以及其它辅助服务代码。 Node.js以源代码形式将V8 engine包含在自己的代码树里面。V8 engine在Node.js里面起到两个重要的作用:

  1. 作为虚拟机,执行JS代码。包括我们自己写的JS代码、第三方JS代码以及natives module
  2. 提供C++函数接口,为Node.js提供包括V8初始化、创建context, scope、为builtin module模拟生成JS prototype-based class、各种模版等服务 对于第2点,比如在 src\Tcp_wrap.cc中的TCPWrap::Initialize()函数内部,通过函数模版创建类似JS prototype-base class、类属性以及prototype method。

Node.js里面有一个大神,libuv,它是基于事件驱动的异步I/O模型库。在第2节可以看到,我们的JS代码发起的异步请求,最终由libuv完成,而我们所设置的回调函数则由libuv最初触发。也就是说,异步请求由我们的JS代码发起,由libuv完成,而回调函数的执行则由libuv最初触发。

Node.js提供了一些辅助函数,如String_byte.cc里面的base64编解码函数,它们存在的意义是为builtin modules提供服务。

V8 engine提供的函数接口,libuv提供的异步I/O模型库以及Node.js提供的其他辅助函数一起为builtin modules提供了生存的土壤。Builtin modules是由C++代码写成的各类模块,涵盖了crypto,zlib, file stream在内的各种基础功能。

Node.js还包含了Natives modules。它是由JS写成的,供我们的应用程序调用的库。同时,这些模块又依赖builtin module来获得相应的支持服务。例如我们在建立一个http server时所用到的http_server.js,其背后的支撑者是tcp_wrap.cc和libuv。

让我们再俯瞰一下node.js部分。如果把node.js看出一个黑盒子的话,其暴露给我们开发者的接口则是Native modules。 当我们的代码发起异步请求时, 请求自上而下,穿越Natvie module,通过builtin module将请求传递给V8, libuv和node.js辅助服务。 而当请求结束,则从下回溯而上,最终调用我们的请求回调函数。

需要强调的是,这里面的自上而下和自下而上的两个过程中间会相隔很远的时间。而这个时间差会被用来执行更多的其它代码。

1.3 我们的JS code

我们的JS code,依赖第三方模块以及Native modules完成自身业务需求。 假如我们的服务程序入口是 fooserv.js,那么通过 node fooserv.js启动我们的服务时,node会先做一些包括V8初始化,libuv启动在内的准备工作,然后交由V8 engine来执行Native module以及我们的JS代码。 这一部分的详细过程可以参考“Node.js挖掘之五:浅析模块加载流程,我们的应用如何启动以及如何加载依赖模块”。

2. 从函数调用流程看Node.js架构

让我们以建立一个http server时,重要的一步server.listen()为例,从函数调用流程来看看Node.js架构。 图中从左到右依次为V8 engine, Node.js wrapper以及 libuv。这3者组成了强大灵活的Node.js。 图2展示的是从JS代码呼叫server.listen()函数开始,开启的一段漫长旅程。旅程分为正、反两个方向。

1.jpg.png

图2:Node.js函数调用流程示例

V8 执行 JaveScript代码 server.listen(config.crypto.service.port, function () {}) 时会通过一些基础服务调到 TCPWrap::Listen()。TCPWrap是Node.js的内建模块。代码可以参考 src/Tcp_wrap.cc。 TCPWrap::Listen()是Node.js提供的wrapper函数,其通过调用libuv的API uv_listen() 的方式,由libuv来完成异步调用。 图中步骤1,2,3,4,5标明了调用和返回路径。这几步会很快结束。留下callback TCPWrap::OnConnection()等着所需要的数据准备好后被调用。

libuv在得到所需要的请求后,会调用callback TCPWrap::OnConnection(),在该函数最后通过 tcp_wrap->MakeCallback(env->onconnection_string(), ARRAY_SIZE(argv), argv) 调用V8 engine中的JavaScript callback

Node.js内建模块http其实是建立在模块net之上的。如果看net.js代码会发现,其通过 new TCP() 返回的类对象完成后续的TCP connect, bind, open等socket动作。

可以看到Node.js做的工作像是一座桥。左手V8,右手libuv,将2者有机连接在一起。例如HandleWrap::HandleWrap()中记录了V8 instance中的JavaScript对象以及TCPWrap对象。这样在TCPWrap::OnConnection()中可以拿到这两个对象,执行后续的callback调用。

转载本文请注明作者和出处,请勿用于任何商业用途。如需帮助,请QQ联系作者:229848501

31 回复

👍👍👍👍

From Noder

请问你是怎么自学nodejs的?看官方的api?有什么资料可以推荐的?

@pomodory 直接看源码和做项目

不明觉厉!

上面的图其实更侧重于描述函数调用时的流程,没有很好地把node里面的各路英雄如 v8, libuv, natives module, builtin module之间的依赖关系表现出来。 我最近在更新架构图,希望新图能让我满意。

请教, 这个图是用什么工具画出来的?

终于把第一篇更新完了。早看第一篇不爽了,一直被项目拖着没时间更新。现在搞完了舒服多了。 更新部分: 1) 更新标题 2)更改文章逻辑,从两个角度看Node.js架构。这样更清晰一点。 3)新加了一节关于模块依赖关系的讲解

@1340641314 希望啥时候你明白的时候,这篇挖掘能帮助到你。到时候,你可以把“不”去掉,变成 “明觉厉”,这样我就心安了 ^_^

@LanceHBZhang 要一个个阶段提升上去,才能慢慢领悟。

虽然看不懂,但是感觉很赞!

👍,学习

很详细,很受用

是不是可以将很多跟平台无关的模块写成builtin 模块?比如leftpad

回到顶部