Node 学习一、基础理论

本文探讨了Node.js如何利用轻量架构、异步IO和事件驱动实现高性能Web服务,涉及Natives modules、Builtin modules、V8引擎和Libuv库的作用,以及其在前后端同构、工程化和应用场景中的优势。
摘要由CSDN通过智能技术生成

Nodejs 可以做什么?

  • 轻量级、高性能的 Web 服务
  • 前后端 JavaScript 同构开发
  • 便捷高效的前端工程化

Nodejs 架构

在这里插入图片描述

Nodejs 核心分为三个部分:

  • Natives modules:内置核心模块
    • 暴露了响应的 JS 功能接口,供开发者进行调用
  • Builtin modules “胶水层”:帮助找到对应功能的 C/C++ 具体实现
    • 这个过程需要 V8 引擎配合实现
  • 具体的功能模块,如 V8、libuv 等
    • V8 引擎除了构建 Nodejs 运行环境,还负责 JS 代码的最终执行
    • libuv 处理具体的 Node 环境下代码执行环境的许多细节

Natives modules

  • 当前层内容都是由 JS 实现
  • 提供了应用程序可以直接调用的库,也就是常说的内置核心模块,例如 fs、path、http 等

Builtin modules “胶水层”

JS 语言无法直接操作底层硬件设置,因此 Nodejs 核心模块与硬件设备想要通信,还需要有一个桥梁。

Builtin modules 就是这个桥梁,通过这个桥梁,就可以让 Nodejs 的核心模块获取具体的服务支持,从而完成更底层的操作,例如文件的读写行为。

在这一层中,除了内置的模块之外,还有很多第三方的模块充当桥梁。

在这一层的内容主要是由 C/C++ 代码编写而成的,例如 socket、zlib、http。这些模块并不是真正代码级别上 JS 函数的功能实现,它们更像是一个功能调用的对照表。

它们的具体实现被放置在另一个地方,Builtin modules 的作用就是帮助调用这些功能的具体实现。

具体的功能模块

V8

V8 引擎的主要功能还有两个:

第一个功能类似于虚拟机,主要负责完成 JS 代码的执行,这里被执行的代码一般被分为三种情况:

  • 用户自己编写的 JS 代码
  • 内置的 JS 代码
  • 第三方的 JS 代码

第二个功能就是提供桥梁接口,接口就是通过 JS 调用那些由 C/C++ 具体实现的功能,这中间的转换和调用的具体实现需要由 V8 引擎完成,也就是负责 JS 与 C/C++ 之间的转换。

简单的说就是,V8 为 Nodejs 提供了初始化操作,它创建了执行上下文环境和作用域。

Libuv

有了 V8 之后,Nodejs 就具备了执行和调用功能的大前提,而最终 Nodejs 代码在执行的过程中还存在着很多的细节,例如事件循环、事件队列、异步IO等要处理的问题。

这些就需要 Libuv 库参与和进行实现了。

第三方模块

有了 V8 和 Libuv,Nodejs 就已经非常强大了,余下的就是一些具体的第三方功能模块,用于完成相对应的功能,如 zlib、http、c-ares 等

总结

如此之后,就可以使用 JS 在 Node 平台下,完成 fs、http、tcp 等在浏览器环境下使用 JS 无法实现的功能。

所谓 Node,只是一个平台或运行时,它扩展了 JS 的功能。

Nodejs 的崛起

Nodejs 在诞生之初是为了实现高性能的 Web 服务器。

后来经过长时间的发展时候,Nodejs 慢慢演化成一门可用的服务端**“语言”**(比喻成“语言”,实际上是个平台或运行时)。

这样就是实现了 JavaScript 在浏览器之外的平台进行工作的场景。


以 B/S 架构为基础用户从发起请求到获取数据的过程:

在这里插入图片描述

  1. 用户通过客户端向服务端发送请求,获取目标数据
  2. 服务端接收到请求后,依据业务逻辑返回数据

从图上可以得知,在忽略掉网络、硬件性能等客观条件之后,真正影响用户获取数据速度的实际上就是 IO 的时间消耗。

IO 是计算机操作过程中最缓慢的环节,访问 RAM 级别设备的 IO 时间消耗是纳秒级别的,而在磁盘和网络中访问数据的 IO 时间消耗是毫秒级别的。

也就是说数据的读写操作终归是要有时间消耗的。

假设当前是一个串行的模式,对于一个服务器来说,如果当前正在处理的请求中,包含一个需要长时间等待的 IO 行为,后续的任务就不能得到及时的响应,这样显然很不友好。

我们当下在开发过程中使用到的一些服务器,大多具备并发事务的能力。

在并发的实现上,传统的做法或者说其它高级编程语言的实现方式采用的是**多线程(进程)**的方式。

当前有几个请求从客户端发送到服务端,服务端就分配几个线程(进程)接收并处理这些请求,这样的服务对用户的体验是非常友好的,但问题也很明显。

如果同一时间客户端发送了很多请求,服务端不可能无限增加线程的数量,这样额外的请求就要等待服务器处理完前面的请求,才会将线程分配给它们,这就发生请求无响应的情况。

其实大部分的时间消耗都来自于线程等待处理请求结果的过程,在这个过程中,线程是处于空闲状态的。

基于这种情况,就有了 Reactor 模式,也被叫做应答者模式。

它的核心思想就是只保留一个主线程,主线程只负责调度任务,如接收请求后交给服务器其它线程,服务器只管去处理请求,当服务器处理完成后,再分配给这个主线程。

在请求处理过程中,主线程还可以继续接收其它请求。

这样就相当于使用单线程完成了多线程的工作,并且它是非阻塞的。

这样也避免了在多个线程之间在进行上下文切换的时候需要去考虑的状态保存、时间消耗等问题。


Nodejs 正是基于 Reactor 模式,再结合 JS 语言本身所具备的一些单线程、事件驱动的架构和异步编程这些特性,使单线程可以远离阻塞,从而通过异步非阻塞的 IO 更好的使用 CPU 的资源,并且实现高并发请求的处理。

这也是为什么历史上尝试将 JS 移植到其它平台的实现方案有很多,而 Nodejs 最出彩的原因。

Nodejs 更适用于 IO 密集型高并发请求,而不是大量且复杂的业务逻辑处理,如 CPU 密集型(请求无需太多等待)高并发请求。

Nodejs 异步 IO

异步的好处

在这里插入图片描述

同步模式下处理多个任务,会依次处理,总消耗时间大于所有任务处理的时间和。

异步模式下处理多个任务,会分配给多个线程同时处理,总消耗时间仅大于耗时最长的任务的时间。

异步模式下程序执行效率更高。

异步 IO 并不是 Nodejs 原创,但是它在 Nodejs 中拥有广泛的应用。

IO

对于操作系统来说,IO 分为阻塞 IO非阻塞 IO,区别就是是否可以立即获取调用后返回的结果。

当采用非阻塞 IO 后,CPU 的时间片就可以空出来去处理其它的事务。

这对于性能有所提升,但也存在一些问题,它立即返回的并不是业务层真正期望得到的实际数据,而仅仅是当前的调用状态。

而操作系统为了获取完整的数据,就会让应用程序重复调用 IO 操作,从而判断 IO 是否完成。

这种通过重复调用 IO 操作判断 IO 是否完成的技术叫做**“轮询”**。

常见的**“轮询”**技术如:read、select、poll、kqueue、event ports。

虽然轮询能够确认 IO 是否完成,并将 IO 完成后产生的数据返回回去,但是对代码而言,它还是同步的效果。

因为在轮询的过程中,程序仍然是在等待着它的结果。

我们期望的 IO 应该是代码可以直接发起非阻塞的调用,又无需通过遍历或唤醒的方式来轮询的判断当前的 IO 是否完成,而是可以在调用发起之后,直接进行下一个任务的处理,然后等待 IO 的处理结果完成之后,再通过某种信号(或回调的方式)将数据传回到当前的代码进行使用。

这个时候 Nodejs 核心中的 libuv 库就该出场了。


在这里插入图片描述

我们可以将 libuv 库看成几种不同的异步 IO 实现方式的抽象封装层。

例如在类 Unix 系统下的 epoll 接口、windows 系统下的 IOCP、sunOS 下的 event ports。

当我们运行了一段 JS 代码之后,最终是会走到 libuv 库的环境里来,它就可以对当前的平台进行判断,然后根据平台调用相应的异步 IO 处理方法。

Node 中具体实现异步 IO 的过程

在这里插入图片描述

此处仅从 Nodejs 代码执行的周期角度介绍异步 IO 的实现,而不过多介绍 Node 环境的事件循环(event loop)。

假设使用 node 或 nodemon 运行一段 JS 脚本(此处省略 V8 的工作),这段脚本中如果存在着异步的请求,libuv 就会开始工作。

它内部存在着一个事件循环机制,此时它会对相应的异步请求处理程序进行管理。

以 IO 操作为例,当前如果要处理的是网络 IO,则会调用相应操作系统底层的 IO 接口进行处理。

如果要处理的是文件 IO,就会被放入到 Nodejs 自行实现的线程池当中进行处理。

不论是哪一种处理方式,最终都会有一个返回结果,这个结果在出来之后就会通过 event loop 把它对应的回调函数(或处理程序)加入到具体的事件队列中,然后等待 JS 的主线程执行。

这个循环也并非是一直运转不停的,当它发现队列中完全没有等待执行的任务时,就会退出循环,此时当前程序的执行也就结束了,而对于 Nodejs 来说,它的异步 IO 也就完成了。

异步 IO 总结

  • IO 是应用程序的瓶颈所在
    • 它的处理肯定要消耗时间,这个时间和设备等客观条件是有很大关系的
  • 异步 IO 提高性能
    • 无须在原地等待结果返回,可以接着处理其它的任务,CPU 的利用率就会变高
  • IO 操作属于操作系统级别,平台都有对应实现
    • libuv 库就是对这些解决方法进行抽象的封装,实现跨平台的效果
  • Nodejs 配合 JS 单线程、事件驱动架构及 libuv 实现了非阻塞的异步 IO
    • 这样就不需要等待异步 IO 的返回,可以继续向下执行 JS 代码
    • 异步的代码执行完成后就会通知主线程,主线程就会执行相应的事件回调

Nodejs 事件驱动架构

事件驱动

事件驱动架构是软件开发中的通用模式。

事件驱动与发布订阅、观察者模式类似,都有共同的特征:主体发布消息,其它实例接收消息。

或者说发布者广播消息,订阅者监听订阅的消息,从而在订阅的事件发生之后执行相应的处理程序。

Nodejs 中事件驱动的具体使用

在之前介绍异步 IO 的时候说过,Nodejs 诞生之初的目的就是为了实现高性能的 web 服务。

而它实现高性能的主要表现手段就是拥有了一套单线程下的异步非阻塞的 IO 机制。

这个异步非阻塞 IO 的实现,让我们可以在从事 Nodejs 代码开发过程中编写很多异步代码。

因为它非阻塞的,程序代码在执行的过程中,业务层获取的并不是最终的目标数据,所以等到同步代码执行完成之后,底层的 libuv 库就开始工作了。


在这里插入图片描述

在 libuv 库中,有两个非常重要的内容:Event Loop(事件循环) 和 Event Queue(事件队列)。

再次以 IO 操作为例,当 libuv 库接收到一个异步操作的请求之后,多路分解器就会工作。

首先它会找到当前平台环境下可用的 IO 处理接口,然后等待 IO 操作结束之后将相应的事件通过事件循环或其它的方式添加到事件队列中。

在这个过程中,事件循环是一直工作的,最后它会按照一定的顺序,从事件队列中取出相应的事件,交给主线程进行执行。

在这个过程中,事件驱动的体现就是有人发布事件,订阅这个事件的人在将来接收到具体的事件消息发布之后,就会执行订阅时所注册的处理程序。

这样的架构很好的解决了在 Nodejs 中由异步非阻塞操作所带来的数据最终获取的问题。

具体的代码实现是 Nodejs 中内置的一个 events 模块,而且事件操作本身也是 Nodejs 中非常重要的组成部分。

在实现异步 IO 的前提下,配合事件驱动的操作,Nodejs 就可以很方便的进行异步编程,并且还可以很容易的获取异步编程返回的执行结果,这些特点为 Nodejs 实现高性能 web 服务提供了一个前提。

代码演示

通过代码演示在 Nodejs 中基于事件的操作行为。

// 导入 Nodejs 的 events 模块
const EventEmitter = require('events')

// 实例化对象
const myEvent = new EventEmitter()

// 订阅事件 并为其注册一个事件处理程序
myEvent.on('事件1', () => {
  console.log('事件1执行了')
})

myEvent.on('事件1', () => {
  console.log('另一个处理程序,按照注册顺序依次执行')
})

// 触发事件
myEvent.emit('事件1')

Nodejs 单线程

Nodejs 使用 JS 实现高效可伸缩的高性能 Web 服务,但是常见的 Web 服务都是由多线程实现的,那单线程的操作是如何支持高并发的?

单线程如何实现并发

在 Nodejs 底层是通过异步 IO、事件循环以及事件驱动的架构,通过回调通知的方式实现非阻塞的调用以及并发。

具体的表现就是程序的代码中如果存在多个请求的时候无需阻塞,它会由上向下执行,然后等待 libuv 库完成工作之后按照顺序通知相应的事件回调去触发执行。

这样单线程也就完成了多线程的工作。

这里所说的“单线程”实际上指的是 Nodejs 主线程是单线程的,而不是说 Nodejs 只有一个线程。

Node 平台下的代码最终都是由 V8 执行的,而在 V8 中只有一个主线程执行代码,这就是平时所说的单线程。

但是在 libuv 库中存在着一个线程池,默认会有 4 个线程。

可以将 Node 程序的异步操作行为分为三种:

  • 网络 IO
  • 非网络 IO
  • 非 IO 的异步操作

针对网络 IO libuv 库会调用当前平台对应的 IO 接口进行处理,而另外两种异步操作就会使用线程池中的线程完成处理。

如果觉得 4 个线程不够用,也可以修改相应的默认配置,增加默认的线程数,不过这个操作一般不需要执行。

单线程劣势

Nodejs 使用单线程实现了多线程的效果,这样提高了线程的安全,同样也减少了线程间切换存在的一些 CPU 开销和内存同步开销等问题。

但是单线程也存在着一些劣势,例如在处理一些 CPU 密集型的任务(最常用的就是计算)时,它就会过多的占用 CPU,这样一来后面的逻辑就必须等待。

而且单线程也无法体现多核 CPU 的优势。

当然这些问题在 Node 后续的版本中也都给出了一些解决方案,例如常见的 cluster 集群。

代码演示

通过代码演示单线程在处理 CPU 密集型任务时存在的阻塞问题

// 使用 http 模块开启一个服务
const http = require('http')

// 定义一个耗时函数
function sleepTime(second) {
  const sleep = Date.now() + second * 1000
  // 如果未到指定事件,就一直循环,阻塞后续代码的执行
  while (Date.now() < sleep) {}
}

// 等待4秒再执行后面的代码
sleepTime(4)

const server = http.createServer((req, res) => {
  // 向客户端返回信息
  res.end('server starting...')
})

server.listen(3000, () => {
  console.log('服务启动了')
})

Nodejs 应用场景

Nodejs 更加适合 IO 密集型任务,不适合处理大量的业务逻辑。

BBF

很多企业会在前端和后端之间搭建一个 BFF 层(Backend For Frontend 服务于前端的后端)。

Nodejs 在这种场景下不仅可以提高吞吐量,而且可以很方便的处理数据。

在这里插入图片描述

操作数据库提供 API 服务

如果将 Nodejs 作为后端语言看待,可以使用它来处理数据,在不关注大量业务逻辑的情况下,可以使用 Nodejs 直接操作数据库,这样可以很容易的搭建轻量高效的 API 服务。

其它

  • I/O 密集型应用,如实时聊天应用程序,多人在线游戏等
  • 前端工程化

Nodejs 全局对象

全局对象是 JavaScript 中的特殊对象,可以看作全局变量的宿主。

Nodejs 的全局对象是 global,与浏览器平台的全局对象 window 不完全相同。

Nodejs 常见全局变量

全局变量不需要 require,常见的全局变量如:

  • __filename:返回正在执行脚本的文件绝对路径
  • __dirname:返回正在执行脚本的文件所在目录绝对路径
  • timer 类函数:执行顺序与事件循环间的关系
    • 如 setTimeout 等
  • process:提供与当前进程互动的接口,本质上指向 Nodejs 内置的 process 模块
    • 通过 process 可以获取当前进程的 id、结束当前进程、在结束后执行一些任务等
  • require:实现模块的加载
  • module、exports:处理模块的导出

Node 环境下 this 指向

默认情况下,Nodejs 的this 是空对象,和 global 并不相等

而浏览器环境下 this === window

console.log(this) // {}
console.log(this === global) // false

// 这个自调用函数的调用者是全局对象
// 所以内部 this 指向 global
;(function () {
  // js 文件脚本
  console.log(this === global) // true
})()

Nodejs 模块

Node 环境下每一个js文件都是一个独立的模块。

模块与模块之间都是互相独立的空间。

可以理解为每个 js 文件都被包裹在一个自调用的函数中,在被调用或执行的时候 Nodejs 会像这个函数中传递一些全局变量,如 require module exports __filename __dirname 等。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值