在传统的动态编程语言(如php)中,需要依赖外部的服务器来进行请求的接收,但在node.js中,则无需担心这个问题,node.js本身内置了一个http服务器,开发者可以直接使用node的模块来进行服务器的创建而无需依赖外部的服务器。
初识node应用
在之前的学习中,我们通过node运行了一个js程序,这实际上只是一个脚本,而非规范的node应用,实际上,一个规范的node应用包含以下几部分:
- require 指令:在 Node.js 中,使用 require 指令来加载和引入模块,引入的模块可以是内置模块,也可以是第三方模块或自定义模块。
- **创建服务器:服务器可以监听客户端的请求,类似于 Apache 、Nginx 等 HTTP 服务器。
- 接收请求与响应请求 服务器很容易创建,客户端可以使用浏览器或终端发送 HTTP 请求,服务器接收请求后返回响应数据。
require 指令
require指令用于加载不同的模块进行开发,其基础语法为:
const http=require('http')//加载http模块并赋给一个http对象
这样会将一个模块进行加载并赋给http变量
基础服务器的创建
在加载完http模块后,我们调用其createServer方法创建一个基础的服务器并监听8888端口:
var http = require('http');
//创建服务器,监听8088端口
http.createServer(function (request, response) {
//请求时的回调// 发送 HTTP 头部 // HTTP 状态值: 200 : OK// 内容类型: text/plainresponse.writeHead(200, {'Content-Type': 'text/plain'});// 发送响应数据 "Hello node"response.end('Hello node!!\n');
}).listen(8888);
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');
访问地址后,应该可以看到一个hello node的输出。
下面,我们来了解一些node.js相关的工作机制与相关架构。
Node.js 的工作机制
核心特点
node.js工作机制的核心特点是:单线程、事件循环、非阻塞I/O、跨平台
核心架构组成
Node.js 通过 V8 引擎执行 JavaScript 代码,使用 Node.js API 与操作系统交互,并通过 Libuv 处理异步 I/O 操作。事件循环和工作线程确保了 Node.js 的高效和非阻塞特性。
- V8 JavaScript Engine:这是 Node.js 的核心,负责执行 JavaScript 代码。V8 是 Chrome 浏览器的 JavaScript 引擎,它将 JavaScript 代码编译成机器码以提高执行效率。
- Node.js Bindings (Node API):这一层提供了一组 API,允许 JavaScript 代码与操作系统进行交互。这些 API 包括文件系统、网络、进程等操作。
- Libuv (Asynchronous I/O):Libuv 是一个跨平台的异步 I/O 库,它在 Node.js 下运行,用于处理文件系统、网络和进程等异步操作。Libuv 使用事件循环和工作线程来处理这些操作,而不会阻塞主线程。
- Event Loop:这是 Node.js 的核心概念之一。事件循环不断检查事件队列,处理事件和执行回调函数。它确保了 Node.js 的非阻塞和事件驱动的特性。
- Event Queue:事件队列用于存储即将处理的事件。当一个异步操作完成时,相关的回调函数会被放入事件队列中,等待事件循环处理
- Worker Threads:这些是用于处理阻塞操作的线程,如文件读写、网络请求等。它们允许 Node.js 在不阻塞主线程的情况下执行这些操作。
- Blocking Operation:这些是可能阻塞线程的操作,如同步的文件读写。在 Node.js 中,这些操作通常被放在工作线程中执行,以避免阻塞事件循环。
- Execute Callback:一旦一个异步操作完成,它的回调函数就会被执行。这是通过事件循环来管理的。
简而言之,Node.js通过V8引擎来执行Js代码,Libuv 管理异步 I/O,libuv中又通过事件循环与工作线程实现高性能、非阻塞的事件驱动架构。
node.js架构分层
node.js可以分为javascript层、C++绑定层、依赖层:
javascript层
面向开发者的层面,包括:
- 核心模块:目录操作fs、路径操作path、网络通信http等等
- npm提供的第三方模块
- 开发者本地开发的模块
c++绑定层
这一层将底层功能暴露给 JavaScript 层,包括:
- node API:- Node.js 核心 API 的 C++ 实现
- V8接口的封装
依赖层
这一层是最底层的依赖:
- V8引擎:用于解释运行javascript的引擎
- libuv:异步I/O库
- c-ares:异步 DNS 解析库
- zlib:压缩功能支持
- openssl:加密支持
事件循环
node.js一个比较核心的工作机制就是它的事件循环,它带来了良好的异步操作调度,它有以下几个阶段:
- timers 阶段:处理setimtout与setinterval定时器的回调,到达预设时间后,相关回调会进入该阶段执行。
- pending callbacks 阶段:执行某些系统操作(如 TCP 错误类型)的回调。
- idle, prepare 阶段:Node.js 内部使用,供系统做一些准备工作
- poll 阶段:执行I/O相关操作的回调,如果 poll 队列不为空,则同步地执行回调,直到队列为空或达到系统限制,队列为空时会根据定时器到期与其他情况进行阶段的切换。
- check 阶段:执行 setImmediate() 注册的回调。
- close callbacks 阶段:执行事件关闭的回调
非阻塞 I/O 原理
node.js的I/O效率非常高,这是因为采用了非阻塞I/O的设计,它的流程如下:
- 应用发起I/O的请求
- node.js将请求给底层的libuv进行处理
- libuv使用系统提供的异步接口进行处理
- 主线程继续执行其他的任务
- I/O 完成后,回调函数被放入事件队列
- 事件循环在适当阶段执行回调
工作机制带来的缺点
node.js的这一套工作机制与I/O模型提供了极高的高并发能力,但也带来了一些缺点:
- 单线程限制:所有 JavaScript 代码在主线程上运行,一旦出现cpu密集型计算任务(机器学习,大规模计算),事件循环会被阻塞,影响其他请求的处理。
- 异步编程导致回调嵌套:由于采用异步编程,在回调时可能会出现回调地狱,影响维护与错误排查。
- 多核利用率不足:node.js默认是单线程,无法自动利用多核 CPU,需要通过
cluster
或worker_threads
手动实现多进程/线程调度,增加开发复杂度。
单线程限制的优化
在上面我们提到了node.js是单线程的,这意味这它无法良好的利用多核性能,但也提供了一些方法来利用多核cpu:
worker threads(推荐)
node.js从v10.5起引入的特性,真正的创建多个线程来并行处理任务:
// main.js
const { Worker } = require('worker_threads');const worker = new Worker('./worker.js', {workerData: { number: 42 }
});worker.on('message', msg => console.log('来自子线程的结果:', msg));
worker.on('error', err => console.error('子线程报错:', err));
worker.on('exit', code => console.log('子线程退出,code:', code));
// worker.js
const { parentPort, workerData } = require('worker_threads');// 假设是个耗时计算
let result = workerData.number * 2;parentPort.postMessage(result);
Child Process(创建子进程)
使用 child_process
模块可以创建新的 Node.js 进程(不是线程),适合进行 多进程并行任务:
// main.js
const { fork } = require('child_process');const child = fork('child.js');child.send({ number: 100 });child.on('message', (msg) => {console.log('主进程收到:', msg);
});
// child.js
process.on('message', (data) => {let result = data.number + 1;process.send({ result });
});
Cluster 模块(主进程 + 多个工作进程)
利用 cluster
模块可以创建多个 Node.js 进程,共享服务器端口,实现类似多线程的服务负载:
// cluster_server.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');if (cluster.isPrimary) {const numCPUs = os.cpus().length;console.log(`主进程 ${process.pid} 正在运行`);// 创建多个工作进程for (let i = 0; i < numCPUs; i++) {cluster.fork();}cluster.on('exit', (worker) => {console.log(`工作进程 ${worker.process.pid} 已退出`);});
} else {http.createServer((req, res) => {res.end(`来自工作进程 ${process.pid}`);}).listen(3000);console.log(`工作进程 ${process.pid} 启动`);
}
// cluster_server.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');if (cluster.isPrimary) {const numCPUs = os.cpus().length;console.log(`主进程 ${process.pid} 正在运行`);// 创建多个工作进程for (let i = 0; i < numCPUs; i++) {cluster.fork();}cluster.on('exit', (worker) => {console.log(`工作进程 ${worker.process.pid} 已退出`);});
} else {http.createServer((req, res) => {res.end(`来自工作进程 ${process.pid}`);}).listen(3000);console.log(`工作进程 ${process.pid} 启动`);
}