当前位置: 首页 > 后端技术 > Node.js

Node.js学习笔记深入浅出(三)

时间:2023-04-03 19:33:41 Node.js

异步I/O在众多高级编程语言或操作平台中,Node是最先以异步为主要编程方式和设计理念的。Node的主旨:异步I/O、事件驱动、单线程。Nginx是用纯C语言编写的。Nginx具有强大的客户端连接能力,但受各种同步编程语言的限制。Node不仅可以作为服务器处理来自客户端的大量并发请求,还可以作为客户端向网络中的各种应用程序发出并发请求。1、为什么要异步I/O1.1用户体验前端可以通过异步消除UI阻塞,但是前端获取资源的速度也取决于后端的响应速度。I/O很贵,分布式I/O更贵。只有后端能够快速响应资源,才能提升前端的体验。1.2资源分配假设业务场景中有一组互不相关的任务需要执行,主流方案包括:单线程串行执行;多线程并行补全;增加硬件资源是提高服务质量的一种方式,但不是唯一方式。单线程同步编程模型会因为阻塞I/O导致硬件资源得不到最佳利用;多线程编程模型也因编程中的死锁和状态同步等问题而受到批评。Node的解决方案:使用单线程,避免多线程死锁、状态同步等问题;使用异步I/O让单线程远离阻塞,更好地利用CPU;为了弥补单线程不能使用多核CPU的缺点,Node在前端浏览器中提供了一个类似WebWorks的子进程,通过worker进程可以高效的利用CPU和I/O。2.异步I/O实现现状2.1异步I/O与非阻塞I/O操作系统内核对于I/O只有两种方式:阻塞;非阻塞;阻塞I/O特性:必须等到系统内核层完成所有操作后调用结束;阻塞I/O导致CPU等待I/O,浪费等待时间,不能充分利用CPU的处理能力。为了提高性能,内核提供了非阻塞I/O。非阻塞I/O和阻塞I/O的区别在于它在调用后立即返回。非阻塞I/O的缺点:由于没有完成完整的I/O,所以立即返回的数据不是业务层期望的数据,而只是当前调用的状态。为了获取完整的数据,应用程序需要反复调用I/O操作来确认是否完整。(Polling)轮询:读取:最原始,性能最低,通过反复调用检查I/O的状态来完成完整数据的读取;select:read的改进,通过文件描述符事件的状态判断;poll:select的改进方案使用链表,避免了数组长度的限制,二来可以避免不必要的检查。但是当文件描述符很多的时候,性能还是很低;epoll:Linux下最高效的I/O事件通知机制,如果进入轮询时没有检查到I/O事件,就会休眠,直到有事件将其唤醒。真正用事件通知和执行回调的方式代替遍历查询,不会浪费CPU,执行效率高;kqueue:类似于epoll,只存在于FreeBSD系统;轮询只能看作是应用程序同步的一种工具。2.2理想的非阻塞异步I/O理想中的完美异步I/O应该是应用发起一个非阻塞的调用,不需要通过遍历或者事件唤醒进行轮询,可以直接处理下一个任务,只需要在I/O完成后通过信号或回调将数据传递给应用程序。2.3RealisticasynchronousI/O多线程异步I/O:通过让部分线程进行阻塞I/O或非阻塞I/O加轮询技术完成数据获取,让一个进程进行计算处理,并在线程间传递通讯将I/O获得的数据进行传输,实现异步I/O。Windows的IOCP:调用异步方法,I/O完成后等待通知,执行回调,用户不需要考虑轮询,但内部是线程池的原理,不同的是这些线程池是由系统管理的核心。Node的libuv:Node提供了libuv作为抽象封装层,使得所有的平台兼容性判断都由这一层来判断,保证了上层Node和下层自定义线程池和ICOP的独立性。3、Node的异步I/O完成了整个异步I/O环节,包括事件循环、观察者、请求对象。3.1事件循环事件循环:进程启动,创建循环,每次执行循环体的过程称为Tick;Tick进程检查是否有事件需要处理,如果有则取出事件及其相关的回调函数;如果有关联的回调函数,则执行;进入下一个循环,如果没有事件处理,则退出流程;3.2Observer观察者:每个事件循环都有一个或多个观察者,判断是否有事件需要处理的过程就是向这些观察者询问是否有事件需要处理。在Node中,事件主要来自网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者和网络I/O观察者。观察者对事件进行分类,事件循环是典型的生产者/消费者模型。异步I/O、网络请求等都是事件生产者,源源不断地为Node提供不同类型的事件。这些事件被传递给相应的观察者,事件循环从观察者那里提取和处理事件。3.3请求对象请求对象是异步I/O过程中的重要中间产物。所有的状态都保存在这个对象中,包括发送到线程池执行,以及I/O操作完成后的回调处理。3.4执行回调I/O观察者回调函数的行为是将request对象的result属性作为参数,将oncomplete_sym属性作为方法,然后调用并执行,达到调用回调函数的目的在JavaScript中传递。整个I/O流程:事件循环、观察者、请求对象、I/O线程池共同构成了Node异步I/O模型的基本元素。4.非I/O异步API非I/O异步API:setTimeout()、setInerval()、process.nextTick()、setImmediate()。4.1定时器setTimeout()和setInerval()与浏览器中的API一致,分别用于单个和多个定时执行任务。它们的实现原理类似于异步I/O,但不需要I/O线程池的参与。通过调用setTimeout()或setInerval()创建的计时器将被插入到计时器观察器内部的红黑树中。每次Tick执行时,都会从红黑树中迭代timer对象,检查是否超过计时时间。超过则形成事件,立即执行其回调函数。定时器的缺点:定时器不精确。事件循环虽然很快,但是如果一次循环耗时很多,下次循环可能会超时很长时间。4.2process.nextTick()每次调用process.nextTick()方法,只将回调函数放入队列,下一轮取出执行。定时器中使用红黑树的操作时间复杂度为O(lg(n)),nextTick()的时间复杂度为O(1)。相比之下,process.nextTick()效率更高。4.3setImmediate()setImmediate()方法和process.nextTick()方法类似,都是延迟回调函数的执行。但是process.nextTick()中的回调函数优先级高于setImmediate()。原因是事件循环顺序检查观察者,process.nextTick()属于空闲观察者,setImmediate()属于检查观察者。在每一轮循环检查中,空闲观察者在I/O观察者之前,I/O观察者在检查观察者之前。具体实现上,process.nextTick()的回调函数存储在一个数组中,setImmediate()的结果存储在一个链表中;从行为上来说,process.nextTick()会在每次循环中将保存在数组中的回调函数全部执行,而setImmediate()会在每次循环中执行链表的每个回调函数。5、事件驱动和高性能服务器事件驱动的本质:通过主循环加事件触发器来运行程序。使用Node搭建web服务器流程图:服务器模型对比:同步,一次只能处理一个请求,其余请求处于等待状态;perprocess/perrequest,每个请求启动一个进程,可以处理多个请求。不可扩展,因为系统资源有限;每个线程/每个请求,为每个要处理的请求启动一个线程。线程虽然比进程轻,但是由于每个线程都占用一定的内存,当大并发请求到来时,内存会很快被用完,导致服务器变慢。Node性能高的原因:Node以事件驱动的方式处理请求,不需要为每个请求额外创建对应的线程,可以节省创建和销毁线程的开销。同时操作系统调度任务因为线程少,上下文切换成本很低。