nodejs学习心得

来源:互联网 发布:ec销售软件 编辑:程序博客网 时间:2024/05/21 19:38

文章引用了一些内容,出自node布道师朴灵所著<<nodejs 深入浅出>>一书,向前辈致敬。


node.js是基于谷歌浏览器javascript引擎建立的一个平台,用于搭建快速、易扩展的网络应用。node.js借助于事件驱动、非阻塞I/O模型变得轻量和高效,非常适合运行在分布式设备上的数据密集型的实时应用。


node的特点:

1、单线程

node.js保持了javascript在浏览器中的单线程特点。在大并发的场景下,我们通常使用多进程或者多线程的方式来应对,即一个请求对应一个进程或者一个请求对应一个线程。我们知道OS在创建进程和线程的时候需要为其开辟资源空间,线程的创建和销毁,线程间的上下文切换,这些对OS来说都是一笔很大的开销。而单线程占据OS资源很少,不用像多线程那样在意线程间的数据同步、数据共享、死锁,也没有多线程那样上下文切换所带来的性能上的开销。

当然,单线程也有缺陷,主要体现在以下3点:

* 无法利用多核cpu

* 健壮性不好,出现错误会引起整个应用退出

* 大计算量等耗时的应用会占据cpu导致无法执行其它操作,即线程阻塞。

不用担心,针对单线程的不足,node.js已经有了解决方案。

node.js子进程的出现,意味着node.js可以从容应对单线程在健壮性、无法利用多核cpu、大计算量线程阻塞的问题。nodejs主进程通过人为的控制可以将大计算量分发给子进程分解掉,然后通过进程间的消息事件来传递结果。多进程的出现,完善了node健壮性,一个进程崩掉,还有其它的进程在工作,也可以使用守护进程来保持进程的稳定。即使不用多进程、守护进程这类方案,针对健壮性问题,nodejs自身提供了异常捕获事件,保障线程不会中断退出,应用程序抛出的所有异常,都会被捕获住,应用依然可以酸爽的运行,代码如下

process.on('uncaughtException', function(err) {  console.log('Caught exception: ' + err);});

2、异步I/O

什么是I/O,软件开发中的I/O范畴主要是指网络通讯、文件操作、数据库操作。

目前市面上用于开发软件的语言,主要是以java、php、C#为主,这几类语言,都是I/O阻塞的。阻塞I/O是什么?拿一次完成的请求响应来举例,客户端发起请求,服务端响应请求,执行这次完整的响应,服务器需需要操作ABC三张表,最后将结果返回。这时候如果是阻塞I/O机制,它会先操作A表,等A表操作完成才能操作B表,C又需要等待B表的完成,只能是按顺序依次操作表,这个时候cpu不能去干其它的事情,一直再那耗着,从而造成了cpu资源浪费也延长了响应的客户端的时间,这就是阻塞I/O。假设阻塞I/O操作ABC三张表耗时分别是15ms、10ms、30ms,那么三张表耗时总和就是55ms。对于同样的场景,nodejs 异步非阻塞I/O,三张表并行操作,耗时取决于用时最长的那张表,也就是30ms,相比阻塞I/O节省了25ms。也许你会说阻塞I/O 耗时55ms毕竟是毫秒级的,是可以接受的。我们联想一下,如果并发量低的场景,server负载不大,阻塞I/O可以接受。如果是高并发场景呢?Server受制于I/O阻塞,cpu闲置着却无法响应大量的请求。这时候你可能会说,可以使用多线程来解决并发问题。是的,多线程可以充分利用cpu和内存来应对高并发,这是一种不错解决方案。多线程虽好,但是多线程对server系能开销也是很大的。同样的性能开销,非阻塞I/O在处理的请求数量上和用户体验上都远胜于多线程。

3、事件驱动模型

什么事件驱动?事件驱动编程是通过事件触发来决定程序执行流程的模型。事件由事件处理器和事件回调来处理,事件回调是在事件触发的时候才被调用的函数,比如一个操作数据事件触发的时候,通过回调函数返回数据。或者用户点击了一个按钮事件,该事件触发后会执行一些操作,这也是事件与事件回调。

事件驱动编程依赖于事件循环(EVENT LOOP),事件循环主要是事件监测和事件处理器触发这两种函数不断循环调用的的一个结构。在每次循环里,事件循环机制需要检测注册了哪些事件,当某一个事件触发时,找到与该事件相关联的回调函数,调用它。

下面介绍一下node.js如何实现异步机制的,分为4个模块:事件循环、观察者、请求对象,执行回调。

事件循环:

node 事件循环就像一个大的while(true)的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及相关联的回调函数执行,执行完毕后进入下个循环,如果不再有事件处理,则会退出循环。

流程图如下:

观察者:

在每个Tick的过程中,如何判断有事件需要处理呢?这里必须引入的概念是观察者。每个事件循环中有一个或者多个观察者,而判断是否有事件需要处理的过程就是向这些观察者询问需要处理的事件。在浏览器中,事件可能来自于用户的点击或者加载某些文件时候产生,这些事件都有相对应的观察者。在node中,事件主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等。

请求对象:

以node中的异步I/O调用举例,node与底层之间是如何执行异步I/O调用以及回调函数究竟是如何被调用执行的:

<span style="font-size:14px;">fs.read(path,function(data){    console.info(data);});</span>

流程是:js调用node的核心模块,核心模块调用c++内建模块,内建模块通过libuv进行系统调用。底层在调用事件的时候,会生成一个请求对象,从js层传入的事件(如上面fs.read事件)的方法、参数和回调函数都会被封装在请求对象中。

请求对象是异步I/O过程中的重要产物,事件所有的状态都保存在这个对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。

至此,主线程将事件封装后请求对象后压入线程池中等待执行,线程立刻返回去做其他的任务,现在I/O事件和node的主线程已经没有关系了,不管它是否阻塞I/O,都不会影响到node的主线程,这样就达到了异步的目的。到这,由js层面发起的I/O异步调用的第一阶段已经结束。

执行回调:

组装好请求对象、送入I/O线程池等待执行,实际上完成了I/O操作的第一部分,回调通知是第二部分。

线程池中的I/O操作完成之后,会将执行完成的状态存储在请求对象的result属性上并通知IOCP调用执行完成(IOCP是一个异步I/O的API,它可以高效地将I/O事件通知给应用程序。),然后归还线程。事件循环每次Tick的时候,会调用IOCP相关的方法检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到I/O观察者队列中,然后去请求对象执行的结果和之前注册的回调函数运行。

至此,整个异步I/O的执行流程结束,完整的流程图如下:




0 0
原创粉丝点击