nodejs简介
► 创建 Node.js 应用:package.json
-
首先,创建一个新文件夹以便于容纳需要的所有文件,并且在此其中创建一个 package.json 文件,描述你应用程序以及需要的依赖:
-
配合着你的 package.json 请运行 npm install。如果你使用的 npm 是版本 5 或者之后的版本,这会自动生成一个 package-lock.json 文件,它将一起被拷贝进入你的 Docker 镜像中。
{
"name": "docker_web_app",
"version": "1.0.0",
"description": "Node.js on Docker",
"author": "First Last <first.last@example.com>",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
► Node.js项目入口:server.js(可以自定义命名)
- 使用 node server.js 运行程序,访问 http://localhost:3000,你就会看到一个消息,写着“Hello World”。
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
nodejs的安全问题
► 请使用 Buffer.from()/Buffer.alloc()
- Buffer() 和 new Buffer() 构造函数对于有安全顾虑的人而言是不推荐使用的。请使用新的方法 Buffer.alloc(),Buffer.allocUnsafe() 或者是 Buffer.from() 构造函数。
► ESLint 规则(推荐)
ESLint 规则不使用缓存构造函数或 node/ 未废除的 Api 也会寻找到使用 Buffer() 废弃的函数。 这些规则预先已经包含了。
使用内存堆剖析器
► 时间轴的分配
在 Chrome 中连接开发工具实例,然后:
- 选择 memory 选项卡
- 选择 Allocation instrumentation timeline
- 开始剖析
► 采样内存堆剖析器信息
在 Chrome 中连接开发工具的实例,然后:
- 选择 memory 选项卡
- 选择 Allocation sampling
- 开始剖析
阻塞对比非阻塞一览
► 阻塞
- 阻塞 是指在 Node.js 程序中,其它 JavaScript 语句的执行,必须等待一个非 JavaScript 操作完成。这是因为当
阻塞
发生时,事件循环无法继续运行 JavaScript。在 Node.js 标准库中使用libuv
的同步方法是最常用的阻塞
操作。原生模块中也有阻塞
方法。
► 非阻塞
- 在 Node.js 标准库中的所有 I/O 方法都提供异步版本,
非阻塞
,并且接受回调函数。某些方法也有对应的阻塞
版本,名字以Sync
结尾。
► 代码比较
同步示例
看上去比异步示例
简单些,但是有一个缺陷:第二行
语句会 阻塞 其它 JavaScript 语句的执行直到整个文件全部读取完毕,也就是moreWork()在同步示例中要等待执行
,而在异步示例不需要等待
- 同步
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
moreWork(); // 在 console.log 之后执行
- 异步
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
moreWork(); // 在 console.log 之前执行
► 并发和吞吐量
-
在 Node.js 中 JavaScript 的执行是单线程的
,因此并发性是指事件循环
在完成其他工作后执行 JavaScript回调函数
的能力。任何预期以并行方式运行的代码必须让事件循环
能够在非 JavaScript 操作
(比如 I/O )执行的同时继续运行 -
让我们思考这样一种情况:每个对 Web 服务器的请求需要 50 毫秒完成,而那 50 毫秒中的 45 毫秒是可以异步执行的数据库 I/O。选择
非阻塞
异步操作可以释放每个请求的 45 毫秒来处理其它请求。仅仅是选择使用非阻塞
方法而不是 阻塞 方法,就能造成并发的显著差异。 -
事件循环不同于许多其他语言的模型,其它语言创建额外线程来处理并发工作。
► 混合阻塞和非阻塞代码的危险
- 处理 I/O 时应该避免一些模式。我们来看一个例子:
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
fs.unlinkSync('/file.md');
- 在以上的例子中,
fs.unlinkSync()
极有可能在fs.readFile()
之前执行,它会在实际读取之前删除file.md
。更好的写法是完全 非阻塞 并保证按正确顺序执行:以下代码在fs.readFile()
的回调中对fs.unlink()
进行了非阻塞
调用,这保证了正确的操作顺序。
const fs = require('fs');
fs.readFile('/file.md', (readFileErr, data) => {
if (readFileErr) throw readFileErr;
console.log(data);
fs.unlink('/file.md', (unlinkErr) => {
if (unlinkErr) throw unlinkErr;
});
});
事件循环(nodejs的核心机制)
- 事件循环是 Node.js 处理
非阻塞 I/O 操作
的机制——尽管JavaScript 是单线程处理
的——当有可能的时候,它们会把操作转移到系统内核中去
。 - 因为目前
大多数内核都是多线程的
,所以它们可以在后台处理多种操作。当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到 轮询 队列中等待时机执行。
► 事件循环机制解析
-
当 Node.js 启动后,它会
初始化事件循环
,处理已提供的输入脚本,它可能会调用一些异步的 API
、调度定时器
,或者调用process.nextTick()
,然后开始处理事件循环。
┌───────────────────────────┐
┌─>│timers (定时器阶段
)此阶段执行由 setTimeout() 和 setInterval() 排序)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │pending callbacks(挂起的回调阶段
)执行 I/O 回调推迟到下一个循环迭代
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │idle, prepare(空闲,准备阶段
)仅在内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │poll (轮询阶段
)
│检索新的 I/O 事件; 执行与 I/O 相关的几乎任何回调
│(由“计时器”或 “setImmediate()”所设的紧邻回调除外);
│node 将在适当时机在此处暂停
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │check(检查阶段
)setImmediate() 回调在此处被调用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤close callbacks(关闭回调阶段
)一些关闭的回调函数,如:socket.on('close', ...)
└───────────────────────────┘注意:每个框被称为事件循环机制的一个阶段。
► 定时器阶段解读
计时器
可以 在回调
后面 指定阈值
,而不是
用户希望回调
执行的确切时间
。因为在经过指定的一段时间间隔后,计时器回调将被尽可能早地运行
。但是,操作系统调度或其它正在运行的回调
可能会延迟
它们。- 从技术上讲,
轮询 阶段
控制执行计时器的时间。 - 为了防止
轮询 阶段
挤占事件循环
的执行,libuv(实现 Node.js 事件循环和平台上所有异步行为的 C 函数库)还设有一个最大硬性限制(取决于系统)
,以避免继续轮询更多事件。
► 轮询阶段解读
► 轮询过程
例如,您调度了一个在 100 毫秒后执行回调的定时器,并且您的脚本开始异步读取文件,这会耗费 95 毫秒:
- 计算应该阻塞和轮询 I/O 的时间(95毫秒)。
- 事件循环等待95毫秒,将需要10毫秒才能完成的回调添加到轮询队列,当回调完成时,队列中不再有回调
- 100毫秒达到时,事件循环机制将发现计时器最快的阈值(100ms)的已经达到,然后将回到 计时器 阶段,以执行定时器的回调
(一旦 轮询 队列为空,事件循环将检查 已达到时间阈值的计时器。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。)
- 在本示例中,您将看到调度计时器到它的回调被执行之间的总延迟将为 105 毫秒。`
► 轮询分析
- 如果
轮询队列
不是空的 ,事件循环将循环访问回调队列并同步执行
它们,直到队列已用尽,或者达到了与系统相关的硬性限制。 - 如果
轮询队列
是空的 ,还有两件事发生: -
- 如果脚本被
setImmediate()
调度,则事件循环
将结束 轮询 阶段
,并继续检查 阶段
以执行那些被调度的脚本。
- 如果脚本被
-
- 如果脚本
未
被setImmediate()
调度,则事件循环将等待回调
被添加到队列
中,然后立即执行。
- 如果脚本
- 注意:为了防止
轮询 阶段事件循环陷入吃不饱的状态
,libuv(实现 Node.js 事件循环和平台的所有异步行为的 C 函数库)在停止轮询以获得更多事件之前,还有一个硬性的最大值
(依赖于系统)。
► setImmediate() 对比 setTimeout()
- setImmediate() 是为了在 当前 所有
检测阶段
完成后执行脚本。
通常,在执行代码时,事件循环最终会进入轮询阶段,在该阶段它将等待传入连接、请求等。但是,如果回调已使用 setImmediate()调度过
,并且轮询阶段变为空闲状态
,则它将结束此阶段
,并继续到检查阶段
而不是继续等待轮询事件。 - setTimeout() 安排一个脚本,在已过期的
最小阈值
后运行。
► process.nextTick()
- 任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调
将在事件循环继续之前解析
- 这可能会造成一些糟糕的情况,因为它允许您通过
递归 process.nextTick()
调用来饿死
您的 I/O,阻止事件循环到达轮询阶段
。
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
- 允许传递参数给 process.nextTick(),这将允许它接受任何在回调函数位置之后的参数,并将参数传递给回调函数作为回调函数的参数,这样您就不必嵌套函数了。
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(
callback,
new TypeError('argument should be string')
);
}
- 通过使用process.nextTick(),我们保证 apiCall() 始终在用户代码的其余部分之后和在让事件循环继续进行之前,执行其回调函数。为了实现这一点,JS 调用栈被允许展开,然后立即执行提供的回调,允许进行递归调用 process.nextTick(),而不触碰 RangeError: 超过 V8 的最大调用堆栈大小 限制。
► 为什么要使用 process.nextTick()?
- 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
- 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。
► process.nextTick() 对比 setImmediate()
- process.nextTick() 在同一个阶段立即执行。
- setImmediate() 在事件循环的接下来的迭代或 ‘tick’ 上触发。
► EventEmitter
另一个例子是扩展 EventEmitter, 并在构造器内释放一个事件:
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {
constructor() {
super();
this.emit('event');
}
}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
你不能立即从构造函数中触发事件,因为脚本尚未处理到用户为该事件分配回调函数的地方。因此,在构造函数本身中可以使用 process.nextTick() 来设置回调,以便在构造函数完成后发出该事件,这是预期的结果
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {
constructor() {
super();
// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
► 不要阻塞你的事件循环(或是工作线程池)
- Node.js 通过事件
循环机制(初始化和回调)的方式运行
JavaScript 代码,并且提供了一个线程池
处理诸如文件 I/O 等高成本的任务。 - Node.js 是用很少量的线程来处理大量客户端请求的。 在 Node.js 中,有两种类型的线程:一个
事件循环线程(也被称为主循环,主线程,事件线程等)
。另外一个是在工作线程池里的k 个工作线程
(也被称为线程池)。 - 如果一个线程执行一个回调函数(事件轮询线程)或者任务(工作线程)需要耗费很长时间,我们称之为“阻塞”。 当一个线程在处理某一个客户端请求时被阻塞了,它就无法处理其它客户端的请求了。 这里给出两个不能阻塞事件轮询线程和工作线程的理由:
-
- 性能:如果你在任意类型的线程上频繁处理繁重的任务,那么你的服务器的 吞吐量(请求/秒)将面临严峻考验。
-
- 安全性:如果对于特定的输入,你的某种类型的线程可能会被阻塞,那么恶意攻击者可以通过构造类似这样的“恶意输入”,故意让你的线程阻塞,然后使其它客户端请求得不到处理。这就是拒绝服务攻击。
► Node.js 模块中有如下这些 API 用到了事件循环线程
- 事件轮询线程执行事件的回调函数,并且负责对处理类似网络 I/O 的非阻塞异步请求。
► Node.js 模块中有如下这些 API 用到了工作线程池
- I/O 密集型任务:
-
- DNS:dns.lookup(),dns.lookupService()。
-
- 文件系统:所有的文件系统 API。除 fs.FSWatcher() 和那些显式同步调用的 API 之外,都使用 libuv 的线程池。
- CPU 密集型任务:
-
- Crypto:crypto.pbkdf2()、crypto.scrypt()、crypto.randomBytes()、crypto.randomFill()、crypto.generateKeyPair()。
-
- Zlib:所有 Zlib 相关函数,除那些显式同步调用的 API 之外,都适用 libuv 的线程池。
► 事件循环线程和工作线程池
-
抽象来说,事件轮询线程和工作池线程分别为等待中的事件回调和等待中的任务维护一个队列。
-
而事实上,事件轮询线程本身并不维护队列,它持有一堆要求操作系统使用诸如 epoll (Linux),kqueue (OSX),event ports (Solaris) 或者 IOCP (Windows) 等机制去监听的文件描述符。 这些文件描述符可能代表一个网络套接字,一个监听的文件等等。 当操作系统确定某个文件的描述符发生变化,事件轮询线程将把它转换成合适的事件,然后触发与该事件对应的回调函数。
-
相对而言,工作线程池则使用一个真实的队列,里边装的都是要被处理的任务。 一个工作线程从这个队列中取出一个任务,开始处理它。当完成之后这个工作线程向事件循环线程中发出一个“至少有一个任务完成了”的消息。
-
Node.js有两种线程:一个事件循环和 k 工作者。 事件循环负责JavaScript 回调和非屏蔽I/O, 和一个工人执行对 C++ 代码的任务,完成异步请求,包括阻塞I/O 和 CPU密集工作。 两种类型的线程每次只能在一个活动上工作。 如果任何回调或任务需要很长时间,运行的线程会被屏蔽 。 如果您的应用程序阻止回调或任务,这最多可能导致通过量化(客户/秒),最坏情况下完全拒绝服务。
-
要写入一个高吞吐量,更多地预防 DoS 服务攻击的服务,您必须确保在良性和恶意输入中, 你的事件循环和你的 Worker 始终不会屏蔽。
► Node.js 中的定时器
- ► setTimeout() 可被用来在一段指定时间之后执行某个代码任务,唯一 保证的是定时器不会比声明的时间间隔 提早 执行。
function myFunc(arg) {
console.log(`arg was => ${arg}`);
}
setTimeout(myFunc, 1500, 'funky');
- ► setImmediate() 在此之后立即执行, 将在当前事件轮询的末尾处执行,setImmediate() 返回一个 Immediate 对象,它可以被用于取消安排的定时任务(见下面的 clearImmediate() )。
不要把 setImmediate() 和 process.nextTick() 相混淆。它们有一些主要的差别:第一, process.nextTick() 将在任何设置好的 Immediate 以及任何安排好的 I/O 前 执行。第二, process.nextTick() 是不可擦除的,换句话说,一旦有代码使用 process.nextTick() 执行,执行无法中断,这就像一个普通函数一样
console.log('before immediate');
setImmediate((arg) => {
console.log(`executing immediate: ${arg}`);
}, 'so immediate');
console.log('after immediate');
before immediate
after immediate
executing immediate: so immediate
- ► setInterval() 永远的轮询,setInterval() 接受一个函数作为其参数,该函数将被运行无限次,第二个参数便是一个给定的延时毫秒数。另外一个和 setTimeout() 类似的地方是延时不保证精确。
function intervalFunc() v.
console.log('Cant stop me !');
}
setInterval(intervalFunc, 1500);
- ► 清除定时器
const timeoutObj = setTimeout(() => {
console.log('timeout beyond time');
}, 1500);
const immediateObj = setImmediate(() => {
console.log('immediately executing immediate');
});
const intervalObj = setInterval(() => {
console.log('interviewing the interval');
}, 500);
clearTimeout(timeoutObj);
clearImmediate(immediateObj);
clearInterval(intervalObj);
- ► 让定时器在背后运行
你已经记住了 Timeout 对象是通过 setTimeout 和 setInterval 返回的。 Timeout 对象提供了两个针对 Timeout 行为的函数
(1)unref()
取消setTimeout
和setInterv
函數的調用
(2)ref()
恢復setTimeout
和setInterv
函數的調用
const timerObj = setTimeout(() => {
console.log('will i run?');
});
// if left alone, this statement will keep the above
// timeout from running, since the timeout will be the only
// thing keeping the program from exiting
timerObj.unref();
// we can bring it back to life by calling ref() inside
// an immediate
setImmediate(() => {
timerObj.ref();
});
下载安装nodejs
► 下载nodejs
- 点击下载nodejs,选择长期维护版
► 安装
- 直接点击next直接完成安装(记住这个路径:
C:\Program Files\nodejs
)
► 配置环境变量
► 新建文件夹如下
- D:\nodejs\node_global
- D:\nodejs\node_cache
► 更改文件夹权限
► 添加环境变量
► 验证是否安装成功
C:\Users\Administrator>node -v
v18.17.1
C:\Users\Administrator>npm -v
9.6.7
C:\Users\Administrator>
► 修改模块下载位置
C:\Users\Administrator>npm get prefix
C:\Users\Administrator\AppData\Roaming\npm
C:\Users\Administrator>npm get cache
C:\Users\Administrator\AppData\Local\npm-cache
C:\Users\Administrator>
C:\Users\Administrator>npm config set prefix "D:\nodejs\node_global"
C:\Users\Administrator>npm config set cache "D:\nodejs\node_cache"
C:\Users\Administrator>
► 测试默认位置是否更改成功
C:\Users\Administrator>npm install express -g
added 58 packages in 20s
8 packages are looking for funding
run `npm fund` for details
C:\Users\Administrator>
► 设置淘宝镜像,并安装cnpm
C:\Users\Administrator>npm config get registry
https://registry.npmjs.org/
C:\Users\Administrator>npm config set registry https://registry.npmmirror.com
C:\Users\Administrator>npm config get registry
https://registry.npmmirror.com
C:\Users\Administrator>npm install -g cnpm --registry=https://registry.npmmirror.com
added 399 packages in 18s
28 packages are looking for funding
run `npm fund` for details
C:\Users\Administrator>
npm config get registry
npm config set registry https://registry.npm.taobao.org
npm config set registry https://registry.npmmirror.com
npm config get registry
npm install -g cnpm --registry=https://registry.npm.taobao.org
npm install -g cnpm --registry=https://registry.npmmirror.com
► 查看cnpm模块
► 执行命令查看cnpm是否安装成功
C:\Users\Administrator>cnpm -v
cnpm@9.2.0 (D:\nodejs\node_global\node_modules\cnpm\lib\parse_argv.js)
npm@9.8.1 (D:\nodejs\node_global\node_modules\cnpm\node_modules\npm\index.js)
node@18.17.1 (C:\Program Files\nodejs\node.exe)
npminstall@7.9.0 (D:\nodejs\node_global\node_modules\cnpm\node_modules\npminstall\lib\index.js)
prefix=D:\nodejs\node_global
win32 x64 10.0.19044
registry=https://registry.npmmirror.com
C:\Users\Administrator>
► 附上所有命令
Microsoft Windows [版本 10.0.19044.3086]
(c) Microsoft Corporation。保留所有权利。
C:\Users\Administrator>node -v
v18.17.1
C:\Users\Administrator>npm -v
9.6.7
C:\Users\Administrator>npm get prefix
C:\Users\Administrator\AppData\Roaming\npm
C:\Users\Administrator>npm get cache
C:\Users\Administrator\AppData\Local\npm-cache
C:\Users\Administrator>npm config set prefix "D:\nodejs\node_global"
C:\Users\Administrator>npm config set cache "D:\nodejs\node_cache"
C:\Users\Administrator>npm install express -g
added 58 packages in 20s
8 packages are looking for funding
run `npm fund` for details
C:\Users\Administrator>npm config get registry
https://registry.npmjs.org/
C:\Users\Administrator>npm config set registry https://registry.npmmirror.com
C:\Users\Administrator>npm config get registry
https://registry.npmmirror.com
C:\Users\Administrator>npm install -g cnpm --registry=https://registry.npmmirror.com
added 399 packages in 18s
28 packages are looking for funding
run `npm fund` for details
C:\Users\Administrator>cnpm -v
cnpm@9.2.0 (D:\nodejs\node_global\node_modules\cnpm\lib\parse_argv.js)
npm@9.8.1 (D:\nodejs\node_global\node_modules\cnpm\node_modules\npm\index.js)
node@18.17.1 (C:\Program Files\nodejs\node.exe)
npminstall@7.9.0 (D:\nodejs\node_global\node_modules\cnpm\node_modules\npminstall\lib\index.js)
prefix=D:\nodejs\node_global
win32 x64 10.0.19044
registry=https://registry.npmmirror.com
C:\Users\Administrator>