nodejs结构

我们可以看到,Node.js 的结构大致分为三个层次:

Node.js 标准库,这部分是由 Javascript 编写的,即我们使用过程中直接能调用的 API。在源码中的lib目录下可以看到。

Node bindings,这一层是 Javascript 与底层 C/C++ 能够沟通的关键,前者通过 bindings 调用后者,相互交换数据。实现在node.cc

这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。

V8:Google 推出的 Javascript VM,也是 Node.js 为什么使用的是 Javascript 的关键,它为 Javascript 提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的原因之一。

Libuv:它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此强大的关键。

C-ares:提供了异步处理 DNS 相关的能力。

http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。


Libuv

Libuv 是 Node.js 关键的一个组成部分,它为上层的 Node.js 提供了统一的 API 调用,使其不用考虑平台差距,隐藏了底层实现。

可以看出,它是一个对开发者友好的工具集,包含定时器,非阻塞的网络 I/O,异步文件系统访问,子进程等功能。它封装了 Libev、Libeio 以及 IOCP,保证了跨平台的通用性。

我们只要先知道它本身是异步和事件驱动的,记住这点,下面的问题就有了答案,我们一一来看。

与操作系统交互

举个简单的例子,我们想要打开一个文件,并进行一些操作,可以写下面这样一段代码:

varfs =require('fs');

fs.open('./test.txt',"w",function(err, fd){

            //..do something

});

这段代码的调用过程大致可描述为:lib/fs.jssrc/node_file.ccuv_fs

具体来说,当我们调用fs.open时,Node.js 通过process.binding调用 C/C++ 层面的Open函数,然后通过它调用 Libuv 中的具体方法uv_fs_open,最后执行的结果通过回调的方式传回,完成流程。在图中,可以看到平台判断的流程,需要说明的是,这一步是在编译的时候已经决定好的,并不是在运行时中。

总体来说,我们在 Javascript 中调用的方法,最终都会通过process.binding传递到 C/C++ 层面,最终由他们来执行真正的操作。Node.js 即这样与操作系统进行互动。

通过这个过程,我们可以发现,实际上,Node.js 虽然说是用的 Javascript,但只是在开发时使用 Javascript 的语法来编写程序。真正的执行过程还是由 V8 将 Javascript 解释,然后由 C/C++ 来执行真正的系统调用,所以并不需要过分担心 Javascript 执行效率的问题。可以看出,Node.js 并不是一门语言,而是一个平台,这点一定要分清楚。

异步、非阻塞I/O

通过上文,我们了解到,真正执行系统调用的其实是 Libuv。之前我们提到,Libuv 本身就是异步和事件驱动的,所以,当我们将 I/O操作的请求传达给 Libuv 之后,Libuv 开启线程来执行这次 I/O 调用,并在执行完成后,传回给 Javascript 进行后续处理。

这里面的 I/O 包括文件 I/O 和 网络 I/O。两者的底层执行略有不同。从上面的 Libuv 官网的图中,我们可以看到,文件I/O,DNS 等操作,都是依托线程池(Thread Pool)来实现的。而网络 I/O 这一大类,包括:TCP、UDP、TTY 等,是由epoll、IOCP、kqueue 来具体实现的。

总结来说,一个异步 I/O 的大致流程如下:

发起 I/O 调用

1.用户通过 Javascript 代码调用 Node 核心模块,将参数和回调函数传入到核心模块;

2.Node 核心模块会将传入的参数和回调函数封装成一个请求对象;

3.将这个请求对象推入到 I/O 线程池等待执行;

4.Javascript 发起的异步调用结束,Javascript 线程继续执行后续操作。

执行回调

1.I/O 操作完成后,会将结果储存到请求对象的 result 属性上,并发出操作完成的通知;

2.每次事件循环时会检查是否有完成的 I/O 操作,如果有就将请求对象加入到 I/O 观察者队列中,之后当做事件处理;

3.处理 I/O 观察者事件时,会取出之前封装在请求对象中的回调函数,执行这个回调函数,并将 result 当参数,以完成 Javascript 回调的目的。


这里面涉及到了 Libuv 本身的一个设计理念,事件循环(Event Loop),它是一个类似于while true的无限循环,其核心函数是uv_run,下文会用到。

从这里,我们可以看到,我们其实对 Node.js 的单线程一直有个误会。事实上,它的单线程指的是自身 Javascript运行环境的单线程,Node.js 并没有给 Javascript 执行时创建新线程的能力,最终的实际操作,还是通过 Libuv以及它的事件循环来执行的。这也就是为什么 Javascript 一个单线程的语言,能在 Node.js 里面实现异步操作的原因,两者并不冲突。

事件驱动

说到,事件驱动,对于前端来说,并不陌生。事件,是一个在 GUI 开发时很常用的一个概念,常见的有鼠标事件,键盘事件等等。在异步的多种实现中,事件是一种比较容易理解和实现的方式。

说到事件,一定会想到回调,当我们写了一大堆事件处理函数后,Libuv 如何来执行这些回调呢?这就提到了我们之前说到的uv_run,先看一张它的执行流程图:


在uv_run函数中,会维护一系列的监视器:

typedefstructuv_loop_suv_loop_t;

typedefstructuv_err_suv_err_t;typedefstructuv_handle_suv_handle_t;

typedefstructuv_stream_suv_stream_t;typedefstructuv_tcp_suv_tcp_t;

typedefstructuv_udp_suv_udp_t;

typedefstructuv_pipe_suv_pipe_t;

typedefstructuv_tty_suv_tty_t;

typedefstructuv_poll_suv_poll_t;

typedefstructuv_timer_suv_timer_t;

typedefstructuv_prepare_suv_prepare_t;

typedefstructuv_check_suv_check_t;

typedefstructuv_idle_suv_idle_t;

typedefstructuv_async_suv_async_t;

typedefstructuv_process_suv_process_t;

typedefstructuv_fs_event_suv_fs_event_t;

typedefstructuv_fs_poll_suv_fs_poll_t;

typedefstructuv_signal_suv_signal_t;

这些监视器都有对应着一种异步操作,它们通过uv_TYPE_start,来注册事件监听以及相应的回调。

在uv_run执行过程中,它会不断的检查这些队列中是或有pending状态的事件,有则触发,而且它在这里只会执行一个回调,避免在多个回调调用时发生竞争关系,因为 Javascript 是单线程的,无法处理这种情况。

上面的图中,对 I/O 操作的事件驱动,表达的比较清楚。除了我们常提到的 I/O 操作,图中还表述了一种情况,timer(定时器)。它与其他两者不同之处在于,它没有单独开立新的线程,而是在事件循环中直接完成的。

事件循环除了维护那些观察者队列,还维护了一个time字段,在初始化时会被赋值为0,每次循环都会更新这个值。所有与时间相关的操作,都会和这个值进行比较,来决定是否执行。

在图中,与 timer 相关的过程如下:

更新当前循环的 time 字段,即当前循环下的“现在”;

1.检查循环中是否还有需要处理的任务(handlers/requests),如果没有就不必循环了,即是否 alive。

2.检查注册过的 timer,如果某一个 timer 中指定的时间落后于当前时间了,说明该 timer 已到期,于是执行其对应的回调函数;

3.执行一次 I/O polling(即阻塞住线程,等待 I/O 事件发生),如果在下一个timer 到期时还没有任何 I/O 完成,则停止等待,执行下一个 timer 的回调。如果发生了 I/O事件,则执行对应的回调;由于执行回调的时间里可能又有 timer 到期了,这里要再次检查 timer 并执行回调。

Node.js 会一直调用uv_run直到到循环不在 alive。

同步方法

虽然 Node.js 是以异步为主要模式的,但我们在实际开发中,难免会有一些情况是有时序性的,如果由异步来写,就会写出很丑的 Callback Hell,如下:

db.query('selectnicknamefromuserswhereid="12"', function() {

   db.query('select*fromxxxwhereid="12"', function() {

         db.query('select*fromxxxwhereid="12"', function() {

               db.query('select*fromxxxwhereid="12"', function() {

                          //...

                  });

            });

     });

});

这个时候如果有同步方法,就会方便很多。这一点,Node.js 的开发者也想到了,目前大部分的异步操作函数,都存在其对应的同步版本,只需要在其名称后面加上Sync即可,不用传入回调。

varfile= fs.readFileSync('/test.txt', {"encoding": "utf-8});

这写方法还是比较好用的,执行 shell 命令,读取文件等都比较方便。不过,体验不太好的一点就是这种调用的错误收集,它不会像回调函数那样,在第一参数中传入错误信息,它会将错误直接抛出,你需要使用try...catch来获取,如下:

var data; 

try{  

        data = fs.readFileSync('/test.txt');

} catch ( e )  {

        if(e.code =='ENOENT') {

               //...

         }

      //...

}

总结

Node.js 通过libuv来处理与操作系统的交互,并且因此具备了异步、非阻塞、事件驱动的能力。

Node.js 实际上是 Javascript 执行线程的单线程,真正的的 I/O 操作,底层 API 调用都是通过多线程执行的。

CPU 密集型的任务是 Node.js 的软肋。

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

推荐阅读更多精彩内容