Node.js详解(一):基础知识
作者:mmseoamin日期:2023-12-25

文章目录

  • 一、Node.js介绍
  • 二、Node.js的优势
  • 三、Node.js的特点
    • 1、V8虚拟机
    • 2、事件驱动
    • 3、异步、非堵塞I/O
    • 四、NodeJS带来的对系统瓶颈的解决方案
      • 1. 并发连接
      • 2. I/O阻塞
      • 五、NodeJS的优缺点
        • 1、优点:
        • 2、缺点:
        • 六、适合NodeJS的场景
          • 1、RESTful API
          • 2、统一Web应用的UI层
          • 3、大量Ajax请求的应用
          • 4、适合I/O密集型的应用
            • 扩展:那么什么是I/O密集型,CPU密集型呢?下面详细介绍下:
            • 总结

              一、Node.js介绍

              Node.js是一个javascript运行环境。它让javascript可以开发后端程序,实现几乎其他后端语言实现的所有功能,可以与PHP、Java、Python、.NET、Ruby等后端语言平起平坐。

              Node.js是基于V8引擎,V8是Google发布的开源JavaScript引擎,本身就是用于Chrome浏览器的js解释部分,但是Node.js 之父 Ryan Dahl把V8引擎搬到了服务器上,用于做服务器的软件。

              Node.js详解(一):基础知识,在这里插入图片描述,第1张

              二、Node.js的优势

              1、Node.js语法完全是js语法,只要你懂js基础就可以学会Node.js后端开发

              ​ Node打破了过去JavaScript只能在浏览器中运行的局面。前后端编程环境统一,可以大大降低开发成本。

              2、Node.js超强的高并发能力

              ​ NodeJs的首要目标是提供一种简单的、用于创建高性能服务器及可在该服务器中运行的各种应用程序的开发工具。

              ​ 首先让我们来看一下现在的服务器端语言中存在着什么问题。在Java、PHP或者.NET等服务器语言中,会为每一个客户端连接创建一个新的线程。而每个线程需要耗费大约2MB内存。也就是说,理论上,一个8GB内存的服务器可以同时连接的最大用户数为4000个左右。要让web应用程序支持更多的用户,就需要增加服务器的数量,而web应用程序的硬件成本当然就上升了。

              ​ NodeJs不为每个客户连接创建一个新的线程,而仅仅使用一个线程。当有用户连接了,就触发一个内部事件,通过非阻塞I/O、事件驱动机制,让Node.js程序宏观上也是并行的。使用Node.js,一个8GB内存的服务器,可以同时处理超过4万用户的连接。

              3、实现高性能服务器

              ​ 严格地说,Node.js是一个用于开发各种web服务器的开发工具。在Node.js服务器中,运行的是高性能V8 JavaScript脚本语言,该语言是一种可以运行在服务器端的脚本语言。

              ​ 那么,什么是V8 JavaScript脚本语言呢?该语言是一种被V8 JavaScript引擎所解析并执行的脚本语言。V8 JavaScript引擎是由Google公司使用C++语言开发的一种高性能JavaScript引擎,该引擎并不局限于在浏览器中运行。Node.js将其转用在了服务器中,并且为其提供了许多附加的具有各种不同用途的API。例如,在一个服务器中,经常需要处理各种二进制数据。在JavaScript脚本语言中,只具有非常有限的对二进制数据的处理能力,而Node.js所提供的Buffer类则提供了丰富的对二进制数据的处理能力。

              ​ 另外,在V8 JavaScript引擎内部使用一种全新的编译技术。这意味着开发者编写的高端的 JavaScript 脚本代码与开发者编写的低端的C语言具有非常相近的执行效率,这也是Node.js服务器可以提供的一个重要特性。

              4、开发周期短、开发成本低、学习成本低

              ​ Node.js能花费最小的硬件成本,追求更高的并发,更高的处理性能。

              三、Node.js的特点

              我们先来看看Node.js官网上的介绍:

              Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

              其特点为:

              1. 它是一个Javascript运行环境
              2. 依赖于Chrome V8引擎进行代码解释
              3. 异步事件驱动
              4. 非阻塞I/O
              5. 轻量、可伸缩,适于实时数据交互应用
              6. 单进程,单线程(这里指主线程)
              7. 性能出众

              接下来就对以上几点进行简单的阐述:

              1、V8虚拟机

              根据百度百科解释,Node.js是一套用来编写高性能网络服务器的JavaScript工具包。Node.js是一个可以快速构建网络服务及应用的平台,该平台的构建是基于Chrome’s JavaScript runtime,也就是说,实际上它是对GoogleV8引擎(应用于Google Chrome浏览器)进行了封装。V8引 擎执行Javascript的速度非常快,性能非常好。

              NodeJS并不是提供简单的封装,然后提供API调用,如果是这样的话那么它就不会有现在这么火了。Node对一些特殊用例进行了优化,提供了替代的API,使得V8在非浏览器环境下运行得更好。例如,在服务器环境中,处理二进制数据通常是必不可少的,但Javascript对此支持不足,因此,V8.Node增加了Buffer类,方便并且高效地 处理二进制数据。因此,Node不仅仅简单的使用了V8,还对其进行了优化,使其在各环境下更加给力。

              2、事件驱动

              首先,解释下“事件驱动”这个概念。所谓事件驱动,是指在持续事务管理过程中,进行决策的一种策略,即跟随当前时间点上出现的事件,调动可用资源,执行相关任务,使不断出现的问题得以解决,防止事务堆积。

              Nodejs设计思想中以事件驱动为核心,事件驱动在于异步回调,他提供的大多数api都是基于事件的、异步的风格。而事件驱动的优势在于充分利用系统资源,执行代码无须阻塞等待某种操作完成,有限的资源用于其他任务。事件驱动机制是通过内部单线程高效率地维护事件循环队列来实现的,没有多线程的资源占用和上下文的切换。

              Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。

              当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。

              这个模型非常高效可扩展性非常强,因为webserver一直接受请求而不等待任何读写操作。(这也被称之为非阻塞式IO或者事件驱动IO)

              在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。

              3、异步、非堵塞I/O

              Nodejs提供的很多模块中都是异步执行的。比如,文件操作的函数。

              一个异步I/O的大致流程:

              发起I/O调用

              ①用户通过js代码调用nodejs的核心模块,将回调函数和参数传入核心模块

              ②将回调函数和参数封装成

              执行回调

              ①操作完成将结果储存到请求对象的result属性上,并发出完成通知。

              ②循环事件,如果有未完成的,就在进入对象请求I/O观察者队列,之后当做事件处理;

              非阻塞 I/O,也叫异步 I/O,显然对应的就是阻塞式 I/O。

              传统的服务器语言大多是多线程、阻塞式 I/O。这也是 Node 与众不同的地方,对于传统的服务器语言,在与用户建立连接时,每一个连接都是一个线程。 当有十万个用户连接时,服务器上就会有十万个线程。而阻塞式 I/O 是指,当一个线程在执行 I/O 操作时,这个线程会阻塞,等待 I/O 操作完成后继续执行。

              举个例子可以更好理解,比如我们到一个餐馆吃饭,这个餐馆比较高级,服务员是一对一服务(每个用户都是一个线程),从我们坐下开始,服务员就把菜单给你,然后在旁边等你点菜(等待 I/O 操作),当你看完菜单,把要点的菜告诉服务员( I/O 操作结束后线程继续执行)。在你看菜单的过程中,服务员其实是被闲置的,如果你一直看,他就会一直等,直到你点完( I/O 操作结束)。这就是阻塞式 I/O。

              阻塞式 I/O 必须要多线程,就这个例子来看,如果只有一个服务员(单线程),那当店里同时有十个顾客时,他需要一个一个等待顾客看菜单然后点菜,那后面的顾客要等待的时间就会很长,显然这个餐馆的服务就会很差(服务器性能差)。

              所以传统的服务器都是多线程、阻塞式 I/O,也就是相当于有多个服务员(线程),每个进来的顾客都分配到一个服务员(线程),然后你看菜单时在旁边等候(阻塞式 I/O )。很明显这样的服务是让顾客最舒服的,但是对于餐馆老板来说很难受,因为要雇佣大量的服务员。因此传统的方式成本是比较大的。要有性能足够好的服务器才能支撑大量的线程。但他的优点也很明显,比如某一桌客人与服务员发生争吵(线程崩了!),不会影响到其他顾客,因为每一桌都有专门的服务员负责,因此只会对当前用户产生影响。

              上面的例子应该可以很好地理解多线程、阻塞式 I/O 。而 node 的特性是单线程、非阻塞时 I/O 。node 最大的优势就是性能强,同样的服务器性能使用 node 可以比传统的服务器语言多容纳一百倍的用户(对于不同的任务有不同的差别, I/O 操作越多,node优势越明显,如果都是 CPU 计算任务,那他俩几乎没有区别(上面的例子中,忽略顾客的看菜单时间)。

              还是用上面的例子再比喻一下单线程、非阻塞式 I/O 。这应该是个规模比较小的餐馆,或者说老板比较穷,雇不起大量的服务员,因此只能雇佣一个服务员。当有顾客来时,服务员把菜单送过去,顾客开始看菜单( I/O 操作),这个时候,服务员是被释放了的,他不用等待顾客看菜单,服务员说:“您先看着菜单,点好了叫我”(回调函数)。这个时候这个服务员就可以抽身去服务其他的顾客。用这种模式的话,一个服务员就可以服务多位顾客,而且不需要等待 I/O ,只需要随时监听就行了,顾客点完后会主动叫服务员(执行回调函数)。

              单线程、非阻塞式 I/O 的优势就是性能强,一个人服务员就可以解决大量顾客。但是他的缺点也很明显,比如有一桌顾客和服务员又吵架了(线程崩了!),那这些顾客就都完了,因为所有人都在等这一个服务员。也就是说,如果线程崩掉了,那与这个服务器连接的所有用户都会崩溃。

              四、单线程

              Nodejs跟Nginx一样都是单线程为基础的,这里的单线程指主线程为单线程,所有的阻塞的全部放入一个线程池中,然后主线程通过队列的方式跟线程池来协作。我们写js部分不需要关心线程的问题,简单了解就可以了,主要由一堆callback回调构成的,然后主线程在循环过在适当场合调用。

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

              nodejs所谓的单线程,只是主线程是单线程,所有的网络请求或者异步任务都交给了内部的线程池去实现,本身只负责不断的往返调度,由事件循环不断驱动事件执行。

              Nodejs之所以单线程可以处理高并发的原因,得益于libuv层的事件循环机制,和底层线程池实现。

              Event loop就是主线程从主线程的事件队列里面不停循环的读取事件,驱动了所有的异步回调函数的执行,Event loop总共7个阶段,每个阶段都有一个任务队列,当所有阶段被顺序执行一次后,event loop 完成了一个 tick。

              五、性能出众

              底层选择用c++和v8来实现的,上面第一点讲到过,nodejs的事件驱动机制,这意味着面对大规模的http请求,nodejs是凭借事件驱动来完成的,性能部分是不用担心的,并且很出色。

              四、NodeJS带来的对系统瓶颈的解决方案

              它的出现确实能为我们解决现实当中系统瓶颈提供了新的思路和方案,下面我们看看它能解决什么问题

              1. 并发连接

              举个例子,想象一个场景,我们在银行排队办理业务,我们看看下面两个模型

              (1)系统线程模型:

              Node.js详解(一):基础知识,在这里插入图片描述,第2张

              这种模型的问题显而易见,服务端只有一个线程,并发请求(用户)到达只能处理一个,其余的要先等待,这就是阻塞,正在享受服务的请求阻塞后面的请求了

              (2)多线程、线程池模型:

               这个模型已经比上一个有所进步,它调节服务端线程的数量来提高对并发请求的接收和响应,但并发量高的时候,请求仍然需要等待,它有个更严重的问题:

               回到代码层面上来讲,我们看看客户端请求与服务端通讯的过程:

              Node.js详解(一):基础知识,在这里插入图片描述,第3张

              服务端与客户端每建立一个连接,都要为这个连接分配一套配套的资源,主要体现为系统内存资源,以PHP为例,维护一个连接可能需要20M的内存

               这就是为什么一般并发量一大,就需要多开服务器

              那么NodeJS是怎么解决这个问题的呢?

               我们来看另外一个模型,想象一下我们在快餐店点餐吃饭的场景

              (3)异步、事件驱动模型

              Node.js详解(一):基础知识,在这里插入图片描述,第4张

              我们同样是要发起请求,等待服务器端响应;但是与银行例子不同的是,这次我们点完餐后拿到了一个号码,

               拿到号码,我们往往会在位置上等待,而在我们后面的请求会继续得到处理,同样是拿了一个号码然后到一旁等待,接待员能一直进行处理。

               等到饭菜做号了,会喊号码,我们拿到了自己的饭菜,进行后续的处理(吃饭)

               这个喊号码的动作在NodeJS中叫做回调(Callback),能在事件(烧菜,I/O)处理完成后继续执行后面的逻辑(吃饭),

               这体现了NodeJS的显著特点,异步机制、事件驱动

               整个过程没有阻塞新用户的连接(点餐),也不需要维护已经点餐的用户与厨师的连接

              基于这样的机制,理论上陆续有用户请求连接,NodeJS都可以进行响应,因此NodeJS能支持比Java、PHP程序更高的并发量

               虽然维护事件队列也需要成本,再由于NodeJS是单线程,事件队列越长,得到响应的时间就越长,并发量上去还是会力不从心

               

               总结一下NodeJS是怎么解决并发连接这个问题的:

               更改连接到服务器的方式,每个连接发射(emit)一个在NodeJS引擎进程中运行的事件(Event),放进事件队列当中,

               而不是为每个连接生成一个新的OS线程(并为其分配一些配套内存)

              2. I/O阻塞

              NodeJS解决的另外一个问题是I/O阻塞,看看这样的业务场景:需要从多个数据源拉取数据,然后进行处理

              (1)串行获取数据,这是我们一般的解决方案,以PHP为例

              Node.js详解(一):基础知识,在这里插入图片描述,第5张

              假如获取profile和timeline操作各需要1S,那么串行获取就需要2S

              (2)NodeJS非阻塞I/O,发射/监听事件来控制执行过程

              Node.js详解(一):基础知识,在这里插入图片描述,第6张

              NodeJS遇到I/O事件会创建一个线程去执行,然后主线程会继续往下执行的,

               因此,拿profile的动作触发一个I/O事件,马上就会执行拿timeline的动作,

               两个动作并行执行,假如各需要1S,那么总的时间也就是1S

               它们的I/O操作执行完成后,发射一个事件,profile和timeline,

               事件代理接收后继续往下执行后面的逻辑,这就是NodeJS非阻塞I/O的特点

              总结一下:

               Java、PHP也有办法实现并行请求(子线程),但NodeJS通过回调函数(Callback)和异步机制会做得很自然

              五、NodeJS的优缺点

              1、优点:

              1. 高并发(最重要的优点)
              2. 适合I/O密集型应用

              2、缺点:

              1. 不适合CPU密集型应用;CPU密集型应用给Node带来的挑战主要是:由于JavaScript单线程的原因,如果有长时间运行的计算(比如大循环),将会导致CPU时间片不能释放,使得后续I/O无法发起;

                   

                   解决方案:分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞I/O调用的发起;

              2. 只支持单核CPU,不能充分利用CPU

              3. 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃

                   原因:单进程,单线程

                   解决方案:(1)Nnigx反向代理,负载均衡,开多个进程,绑定多个端口;

                        (2)开多个进程监听同一个端口,使用cluster模块;

              4. 开源组件库质量参差不齐,更新快,向下不兼容

              5. Debug不方便,错误没有stack trace

              六、适合NodeJS的场景

              1、RESTful API

              这是NodeJS最理想的应用场景,可以处理数万条连接,本身没有太多的逻辑,只需要请求API,组织数据进行返回即可。它本质上只是从某个数据库中查找一些值并将它们组成一个响应。由于响应是少量文本,入站请求也是少量的文本,因此流量不高,一台机器甚至也可以处理最繁忙的公司的API需求。

              2、统一Web应用的UI层

              目前MVC的架构,在某种意义上来说,Web开发有两个UI层,一个是在浏览器里面我们最终看到的,另一个在server端,负责生成和拼接页面。

              Node.js详解(一):基础知识,在这里插入图片描述,第7张

              不讨论这种架构是好是坏,但是有另外一种实践,面向服务的架构,更好的做前后端的依赖分离。如果所有的关键业务逻辑都封装成REST调用,就意味着在上层只需要考虑如何用这些REST接口构建具体的应用。那些后端程序员们根本不操心具体数据是如何从一个页面传递到另一个页面的,他们也不用管用户数据更新是通过Ajax异步获取的还是通过刷新页面。

              Node.js详解(一):基础知识,在这里插入图片描述,第8张

              3、大量Ajax请求的应用

              例如个性化应用,每个用户看到的页面都不一样,缓存失效,需要在页面加载的时候发起Ajax请求,NodeJS能响应大量的并发请求。  总而言之,NodeJS适合运用在高并发、I/O密集、少量业务逻辑的场景。

              4、适合I/O密集型的应用

              ​ 如在线多人聊天,多人在线小游戏,实时新闻,博客,微博之类的。

              不适合的场景有:cpu密集型的应用

              ​ 如计算圆周率,视频解码等业务场景较多的。

              扩展:那么什么是I/O密集型,CPU密集型呢?下面详细介绍下:

              ​ CPU密集型(CPU-bound)

              CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

              在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。

              CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。

              ​ IO密集型(I/O bound)

              IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。

              I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。

              ​ CPU密集型 vs IO密集型

              我们可以把任务分为计算密集型和IO密集型。

              ​ 计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

              ​ 计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

              ​ 第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。

              ​ IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

              总之,计算密集型程序适合C语言多线程,I/O密集型适合脚本语言开发的多线程。

              总结

              Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。

              Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。

              简单的说 Node.js 就是运行在服务端的 JavaScript,利用JavaScript在服务端进行编程。

              nodejs开发的构成就成利用npm开发的社区提供的大量的第三方包加上基本的ECMAScript脚本语言以及node平台提供的一系列编程接口进行编程。