1. 非阻塞I/O和事件驱动架构
- 非阻塞I/O和事件驱动架构(异步编程):Node.js采用非阻塞I/O操作,使用事件驱动模型来处理并发请求,大大提高了性能和可扩展性。
- Node.js 使用非阻塞I/O操作意味着当执行I/O操作(如读写文件、网络请求等)时,不会阻塞主线程。相反,它会立即返回,允许程序继续执行其他任务。
- 当I/O操作完成时,会触发一个事件。Node.js使用回调函数来处理这些事件,这就是事件驱动模型的核心。
- 底层以来Event Loop;Event Loop依赖Libuv。
2. 单线程事件循环
- 单线程事件循环:Node.js运行在单线程中,通过事件循环机制处理异步操作,这使得它可以高效地管理大量并发连接。
- node处理网络请求的过程
- OS:操作系统接收到这个请求,然后通知到对应的进程
- libuv:libuv断轮询操作系统的事件;libuv将网络事件封装成任务后,事件循环会在适当的阶段(通常是poll阶段)处理这个任务。
- JavaScript代码层面:调用与该请求相关联的JavaScript回调函数。
- 响应发送过程:Node.js会将响应数据交给libuv;
- libuv会将响应数据放入操作系统的网络发送缓冲区;
- 操作系统负责将响应数据通过网络发送给客户端
- 与Java对比
- 并发连接处理:
- 在处理大量并发轻量级连接时,Node.js 通常可以用较少的资源处理更多的并发连接。
- Java对于每一个请求都需要一个单独的线程来处理,这个是会消耗系统内存资源的。
- 响应时间:
- 对于简单的I/O操作,Node.js可能会有更低的延迟,因为它不需要线程切换。
- 可扩展性:
- Node.js的单线程模型使得水平扩展(增加更多实例)变得相对简单。
- Java 则可以通过垂直扩展(增加更多线程)和水平扩展来提高性能。
- 并发连接处理:
- node处理网络请求的过程
3. 模块化系统
Node.js使用CommonJS模块系统,通过require引入模块,促进代码的重用和组织。
4. 高性能(高效处理I/O密集型任务):
Node.js特别适合I/O密集型应用,如实时聊天、数据流处理等,其高效的I/O处理能力源于非阻塞I/O和事件驱动模型。
5. child_process:
- 创建node子进程,控制子进程执行脚本或者命令。
- 核心方法:
- spawn()
- 启动一个新进程来执行命令
- 适合长时间运行的进程和需要实时处理大量输出的场景。
- exec()
- 运行一个命令,并缓冲输出。
- exec() 会等待整个命令执行完成,然后一次性返回所有输出。
- 适合执行简短的命令并一次性获取其输出。
- 运行一个命令,并缓冲输出。
- execFile()
- 直接执行一个可执行文件
- 适合直接执行可执行文件,不通过 shell 解释。
- fork()
- 区别与C语言的fork()
- 专门用于创建 Node.js 子进程的特殊方法
- 专门用于创建 Node.js 子进程,并提供了方便的 IPC 通道。
- spawn()
6. Cluster(集群模式)
- 底层基于child_process
- 模型:primary 负责接受请求,然后分发给 worker:
- Windows 模型:在 Windows 上,主进程(primary)确实负责接受所有连接,然后通过轮询(Round-Robin)算法将连接分发给 worker 进程。
- Unix(Linux)模型:
- 现代模式:
- 这种模式被称为 "共享句柄" 模式;所有进程(包括主进程和 worker 进程)都会调用 listen() 函数。
- 操作系统内核负责在这些进程之间分发新的连接。
- 这种模式效率更高,因为它减少了进程间通信的开销。
- 传统模式:主进程接受连接,然后将 socket 的文件描述符传递给选定的 worker 进程。
- 现代模式:
7. worker_threads
- worker_threads 模块允许使用线程并行执行 JavaScript 代码。
- worker_threads 允许在一个 Node.js 进程内创建多个并行执行的 JavaScript 线程。这些线程共享同一个 V8 实例,但有独立的执行上下文和事件循环,可以并行处理 CPU 密集型任务,同时通过消息传递机制进行通信。
- 共享底层资源:所有的 worker 线程共享同一个 V8 实例和底层的系统资源。
- 独立的事件循环:每个 worker 线程有自己独立的事件循环,允许并行执行 JavaScript 代码。
- 内存隔离:虽然共享同一个 V8 实例,但每个 worker 线程有自己的内存空间,默认情况下不与其他线程共享。
- 通信机制:worker 线程之间通过消息传递进行通信,而不是直接共享内存(除非显式使用 SharedArrayBuffer)。
- 性能优势:相比创建多个 V8 实例,这种方式更轻量,启动更快,占用资源更少。
- 它们对
执行 CPU 密集型的 JavaScript 操作非常有用,但对 I/O 密集型的工作帮助不大。Node.js 内置的异步 I/O 操作比 Workers 更高效。 - 内存共享
- 与
child_process或cluster不同,worker_threads可以共享内存。 - 通过传递
ArrayBuffer实例或共享SharedArrayBuffer实例来实现内存共享。
- 与
8. Buffer
- Buffer分配的是原始内存空间,位于V8引擎堆内存之外。Buffer不受V8的垃圾回收机制直接管理。
- Buffer使用C++层面的malloc()函数在进程的内存空间中分配内存。这是直接在操作系统层面分配的内存。
- Buffer的内存分配有两种方式:
- 对于小型Buffer,使用预分配的内存池
- 对于大型Buffer,直接在堆上分配
- 应用
- 数据流处理:在处理网络数据流时,Buffer可以作为临时存储区。当接收到的数据还不完整时,可以将其存储在Buffer中,等待更多数据到达后再进行处理。这对于实现高效的数据流处理非常有用。
- 当操作系统内核接收到网络数据(通常是一个数据包或数据流的一部分)时,它会通知 Node.js。Node.js 随后可以使用 Buffer 来接收和存储这些数据。在应用层,开发者通常需要实现逻辑来累积这些 Buffer 数据,直到接收到完整的请求或消息,然后再进行处理。
- 数据流处理:在处理网络数据流时,Buffer可以作为临时存储区。当接收到的数据还不完整时,可以将其存储在Buffer中,等待更多数据到达后再进行处理。这对于实现高效的数据流处理非常有用。
9. Stream
- 作用:
- Stream 是处理大数据量、实时数据传输的核心工具。
- 通过事件和流水线机制,确保高效、低内存占用的数据处理。
- Node.js中的stream的底层原理主要包括以下几个方面:
- 事件驱动模型:Stream继承自EventEmitter,采用事件驱动的方式工作。通过触发和监听事件(如'data'、'end'等)来实现数据流动和状态变化的通知。
- 内部缓冲区:Stream内部维护了一个缓冲区,用于临时存储待处理的数据。缓冲区大小由highWaterMark参数控制。
- 数据分块处理:Stream将大量数据分成小块(chunk)逐个处理,而不是一次性加载到内存。这样可以高效处理大文件。
- 背压(Backpressure)机制:当下游消费速度跟不上上游生产速度时,会产生背压。Stream通过暂停数据生产、缓冲等方式来平衡读写速度。
- 在处理文件流时,如果下游(如数据处理或写入操作)的速度跟不上上游(文件读取)的速度,Stream 的背压机制会自动启动。这时,Stream 会暂停从文件读取新的数据,等待下游处理完当前的数据。一旦下游准备好接收更多数据,Stream 会恢复文件的读取操作。这个过程是动态的,会根据处理速度的变化不断调整,以确保内存使用效率和整体性能的平衡。
const fs = require('fs'); const stream = require('stream'); const util = require('util'); const readStream = fs.createReadStream('bigfile.txt'); const writeStream = fs.createWriteStream('output.txt'); // 创建一个转换流,模拟慢速处理 const slowTransform = new stream.Transform({ transform(chunk, encoding, callback) { // 模拟慢速处理 setTimeout(() => { this.push(chunk); callback(); }, 100); // 每块数据处理需要100ms } }); // 使用 pipeline 来处理错误和正确关闭流 const pipeline = util.promisify(stream.pipeline); async function run() { try { await pipeline( readStream, slowTransform, writeStream ); console.log('Pipeline succeeded.'); } catch (err) { console.error('Pipeline failed.', err); } } run(); // 监听 readStream 的暂停和恢复 readStream.on('pause', () => console.log('Read stream paused.')); readStream.on('resume', () => console.log('Read stream resumed.')); - 管道(pipe)机制:通过pipe()方法可以轻松地将多个Stream连接起来,形成数据处理管道。
- 内部状态机制:Stream内部维护了多个状态(如flowing、paused等),用于控制数据流动。
- 异步迭代器支持:新版Node.js支持通过for-await-of语法以同步方式消费Stream。
- libuv支持:Stream的一些底层I/O操作(如文件读写)依赖libuv库实现异步非阻塞。总的来说,Stream通过事件驱动、分块处理、缓冲区管理等机制,实现了高效的数据流式处理。这种设计使得Node.js能够轻松处理大规模I/O操作。
10. Koa和Express的中间件(gpt)
- 基本概念: 中间件本质上是一个函数,它可以访问请求对象、响应对象,以及应用程序的请求-响应周期中的下一个中间件函数。
- Express 中间件:
- 形式:function(req, res, next) {}
- 执行模型:线性栈模型
- 流程控制:通过调用 next() 来传递控制权
- 错误处理:使用 next(error) 传递错误到专门的错误处理中间件
- Koa 中间件:
- 形式:async function(ctx, next) {}
- 执行模型:洋葱模型(先进后出)
- 流程控制:使用 await next() 来暂停执行,等待下游中间件完成
- 错误处理:可以使用 try/catch 直接在中间件中处理错误
- 主要区别:
- 异步处理:Koa 原生支持 async/await,使异步代码更清晰
- 上下文对象:Koa 使用统一的 ctx 对象,而 Express 分别使用 req 和 res
- 灵活性:Koa 的中间件更加灵活,可以轻松实现前置和后置处理
- 性能考虑:
- Koa 的洋葱模型允许更精细的控制,潜在地提供更好的性能
11. Egg(gpt)
- 核心理念:Egg.js 遵循"约定优于配置"的原则,提供了一套规范化的开发流程和最佳实践。这使得团队协作更加高效,也便于项目的长期维护。
- 基于 Koa:Egg.js 构建在 Koa 之上,继承了 Koa 的优秀特性,如洋葱模型中间件系统。这使得 Egg.js 在保持高度可扩展性的同时,也能提供更多开箱即用的功能。
- 插件机制:Egg.js 的插件系统是其核心特性之一。它允许将应用中的独立功能拆分成插件,实现了高度的模块化和可重用性。这不仅提高了开发效率,也使得应用更易于维护和扩展。
- 加载器(Loader):Egg.js 的加载器机制自动加载应用中的各种文件(如 controller、service 等),大大简化了开发流程,提高了开发效率。
Midday(gpt)
- 核心理念:Midway 致力于为开发者提供一个更现代化、更强大的 Node.js 开发体验。它采用了 TypeScript 作为主要开发语言,充分利用了 TypeScript 的类型系统和装饰器特性。
- IoC 容器:Midway 的核心是一个强大的依赖注入(DI)容器。这使得代码更加模块化、可测试和可维护。开发者可以通过装饰器轻松实现依赖注入,大大降低了代码的耦合度。
- 装饰器:Midway 大量使用装饰器来简化开发。例如,@Controller、@Get、@Post 等装饰器可以轻松定义路由,@Inject 用于依赖注入。这种方式使得代码更加声明式和易读。
- 多框架支持:Midway 支持多种 Web 框架,包括 Express、Koa 和 Egg.js。这种灵活性允许开发者根据项目需求选择最适合的底层框架。
- 全栈开发: Midway 提供了全栈解决方案,包括前端(Midway FaaS)和后端开发。这使得使用同一个框架进行全栈开发成为可能,提高了开发效率。
- 函数式编程支持:Midway 支持函数式编程范式,特别是在 Serverless 场景下。这为开发者提供了更多的编程模式选择。