[node] 积累
自定义port
var express = require('express'),
app = express(),
port = process.env.PORT || 3000;//获取输入的PORT值作为服务器监听的端口号,如果没有默认是3000
app.listen(port);
Windows系统环境下(默认是永久情况):
set PORT=1234
node app.js
清空
set port=
In Windows PowerShell:
$env:PORT = 1234
linux系统环境(苹果的mac也是这种情况):
$ PORT=1234 node app.js //PORT必须大写
使用上面命令每次都需要重新设置,如果想设置一次永久生效,使用下面的命令。
$ export PORT=1234
$ node app.js
V8引擎中的垃圾回收机制
-
新生代
- 内存区域小,垃圾回收频繁,大多数变量刚被声明的时候都先进入新生代区域。当新生代区域内存满的时候会进行一次清理。
- 采用Scavenge算法进行回收,主要思想是将内存区分为 From 空间和 To 空间。变量一来时都先进入 From 空间,当开始清理的时候对 From 空间进行宽搜,将那些能够从ROOT节点搜索到的,并且还在新生代区域的变量放入 To 空间中,然后交换 From 和 To 空间。
-
晋升
- 在新生代区域进行回收的时候,查看这个变量是否已经经历过一次垃圾回收,如果已经经历过一次了就将它放入老生代区域。
- 对象从 From 空间复制到 To 空间时,如果To 空间已经被使用了超过25%,那么这个对象直接被复制到老生代。(这里我存在疑问)
-
老生代
- 内存区域大,垃圾回收频率较低,保存的对象大多数是生存周期很长的甚至是常驻内存的对象。
- 采用Mark-Sweep(标记清除)与 Mark-Compact(标记整理)结合的算法进行回收,主要思想是进行宽搜,搜索过程当中对对象进行染色,最后如果一个对象没有被染色那么就是垃圾(这里其实和Scavenge寻找垃圾的思路是一致的,就是看一个对象能不能从ROOT节点搜索到)。然后进行对象的清除,当空间不足以分配从新生代晋升过来的对象时,才使用标记整理(其实就是内存页面的整理)。
Events
Events中的emit是同步的,会按照注册顺序来触发监听器。
一个事件监听器中监听同一个事件,会导致死循环,其实就是无限的递归调用。
const EventEmitter = require('events');
let emitter = new EventEmitter();
emitter.on('myEvent', () => {
console.log('hi');
emitter.emit('myEvent');
});
emitter.emit('myEvent');
这种情况不会出现死循环,因为在执行的过程当中是把原监听数组拷贝一份出来执行监听。
当在监听器中监听同一个事件的时候,只会在原监听数组当中添加监听,而不会在这个拷贝后的数组添加。
const EventEmitter = require('events');
let emitter = new EventEmitter();
emitter.on('myEvent', function sth () {
emitter.on('myEvent', sth);
console.log('hi');
});
emitter.emit('myEvent');
Event Loop
任务队列
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 - 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。 - 所以异步任务永远是在同步任务之后开始执行的,不管他的代码位置如何。
- 排在任务队列前面的事件优先进入执行栈,但是有些事件会被设置定时器,如果现在的时间小于了定时器的时间,那么会一直等到到达定时器的时间才会将事件加入执行栈。如果现在的时间大于了定时器的时间,也不会立即将它加入执行栈,会等到同步任务与它前面的异步任务执行完成以后再将它加入执行栈。
process.nextTick
- 在当前"执行栈"的尾部,下一次Event Loop(主线程读取"任务队列")之前,触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。
- 如果有多个 process.nextTick 语句(不管它们是否嵌套),将全部在当前"执行栈"执行。
setImmediate
- 在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与 setTimeout(fn, 0) 很像。
- process.nextTick 和 setImmediate 的一个重要区别:多个 process.nextTick 语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加 setImmediate 方法的原因,否则像下面这样的递归调用 process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!
详细分析
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
- timers: 执行各种定时器预约的操作, setTimeout(callback) 和 setInterval(callback)
- I/O callbacks: 执行除了 close 事件的callback和属于 timers 的callback以外的callback事件。
- idle, prepare: 仅node内部使用。
- poll: 获取新的I/O事件, 适当的条件下node将阻塞在这里;(在我的理解中,这个阶段才是一个 Event Loop 的开始)
- check: 执行 setImmediate() 设定的callbacks;
- close callbacks: close 事件的回调会在该阶段执行。
- 而还有一个独立于 Event Loop 外的过程就是 process.nextTick() 它是在各个阶段的切换阶段进行调用。
例子:
A();
B();
C();
A();
process.nextTick(B);
C();
A();
setImmediate(B);
C();
Poll阶段
代码中没有设置 timer:
- poll quenue 不为空时,同步执行队列中的所有事件。直到队列为空,或执行的callback达到系统的上限。
- poll quenue 为空时,如果设定了 setImmediate() 则进入 check阶段,没有设定的话,就阻塞等待callback进入队列。
代码中设置 timer:
- poll queue进入空状态时,event loop 将检查timers,如果有1个或多个timers时间时间已经到达,event loop 将按循环顺序进入timers阶段,并执行timer queue.
Promise
Promise中的Event Loop 。
var promise = new Promise(function (resolve) {
console.log("inner promise"); // 1
resolve(42);
});
promise.then(function (value) {
console.log(value); // 3
});
console.log("outer promise"); // 2
// 执行顺序
//inner promise // 1
//outer promise // 2
//42 // 3
首先代码顺序执行,注册了一个Timer事件,将Timer事件放入Timer队列中。
然后有一个promise被放入 poll 队列中执行。
先是有一个输出2的同步事件,被放入 poll 队列执行,再就出现了 resolve() ,一个异步事件先放入i/o callback队列中,再有一个输出3的同步事件,被放入 poll 队列执行。
再是将输出5的同步事件放入 poll 执行,poll 空了以后,将i/o callback中的 resolve() 放入poll 执行,最后将Timer中的事件放入 poll 中执行。
setTimeout(function () {
console.log(1)//5
}, 0);
new Promise(function executor(resolve) {
console.log(2);//1
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(3);//2
}).then(function () {
console.log(4);//4
});
console.log(5);//3
// 执行顺序
//2
//3
//5
//4
//1
进程
Process
process.cwd() :返回运行当前脚本的工作目录的路径。
process.chdir() :
process.nextTick
标准流
console.log 的实现
exports.log = function() {
process.stdout.write(format.apply(this, arguments) + '\n');
};
- 通过实现来说,我觉得 console.log 是同步的。
- 同步输入的实现 (nodeJS 中从命令行等待并读入用户输入实现与用户交互的方法):
- 主要是思路是将 fs.readSync 的输入流从定向到 process.stdin.fd
- 我觉得还有一种方法是对输入流进行监听
- Linux 中关于进程管理的命令
- top :TOP 是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止.比较准确的说, top 命令提供了实时的对系统处理器的状态监视.它将显示系统中CPU最“敏感”的任务列表.该命令可以按CPU使用.内存使用和执行时间对任务进行排序;而且该命令的很多特性都可以通过交互式命令或者在个人定制文件中进行设定.
- ps:ps 命令就是最基本同时也是非常强大的进程查看命令.使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵尸、哪些进程占用了过多的资源等等。总之大部分信息都是可以通过执行该命令得到的。
- pstree: Linux pstree命令将所有行程以树状图显示,树状图将会以 pid (如果有指定) 或是以 init 这个基本行程为根 (root),如果有指定使用者 id,则树状图会只显示该使用者所拥有的行程。
Child Process
child_process.fork() 方法是 child_process.spawn() 的一个特殊情况,专门用于衍生新的 Node.js 进程。 跟 child_process.spawn() 一样返回一个 ChildProcess 对象。 返回的 ChildProcess 会有一个额外的内置的通信通道,它允许消息在父进程和子进程之间来回传递。 详见 subprocess.send()。
衍生的 Node.js 子进程与两者之间建立的 IPC 通信信道的异常是独立于父进程的。 每个进程都有自己的内存,使用自己的 V8 实例。 由于需要额外的资源分配,因此不推荐衍生大量的 Node.js 进程。
默认情况下,child_process.fork() 会使用父进程中的 process.execPath 衍生新的 Node.js 实例。 options 对象中的 execPath 属性可以替换要使用的执行路径。
使用自定义的 execPath 启动的 Node.js 进程,会使用子进程的环境变量 NODE_CHANNEL_FD 中指定的文件描述符(fd)与父进程通信。 fd 上的输入和输出期望被分割成一行一行的 JSON 对象。
注意,不像 POSIX 系统回调中的 fork,child_process.fork() 不会克隆当前进程。
child_process.fork 与POSIX的 fork 有什么区别
POSIX的 fork:
当程序调用 fork() 函数并返回成功之后,程序就将变成两个进程,调用 fork() 者为父进程,后来生成者为子进程。这两个进程将执行相同的程序文本,但却各自拥有不同的栈段、数据段以及堆栈拷贝。子进程的栈、数据以及栈段开始时是父进程内存相应各部分的完全拷贝,因此它们互不影响。如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
但是现在没有找到 child_process.fork 如果不是对当前父进程的拷贝,那他的具体实现原理是什么。
child.kill 与 child.send 的区别
实例化一个 spawn 后,会返回一个 ChildProcess 对象,该对象中包含了 stdin 、stdout 和 stderr 流对象。而实例化一个 fork ,默认是会继承父进程的stdin 、stdout 和 stderr 流(当然也可以打开),并且会单独建立一个IPC通道,使得父子进程之间可以通过监听 message 事件来进行通信,所以 fork 实际上是 spawn 的一个特例。
而 send 是需要依赖 IPC 通道进行通信的,所以只有通过 fork 的子进程才能与父进程之间使用 send 通信的。kill 是通过信号系统来进行通信,只要操作系统支持信号系统就行。
孤儿进程与僵尸进程
子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。
- 孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。 在 spawn 中可以通过 options.detached 指定父进程死亡后是否允许子进程存活。
孤儿进程在被init进程接手以后,init进程会循环地 wait() 它已经退出的子进程,来进行善后,所以孤儿进程不会有什么危害。 - 僵尸进程:
一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。 + 僵尸进程因为没有被父进程调用 wait ,进程号、退出状态、运行时间等都被保存在内存中,特别是进程号一直被占用着,而系统的进程号又是有限的,所以大量的僵尸进程是会带来非常大的威胁的。
Cluster
一个单一的 Node.js 实例运行在一个单独的线程上。 为了利用多核系统,用户有时会想启动一个 Node.js 进程的集群去处理负载。
cluster 模块可以轻松地创建一些共享服务器端口的子进程。
工作进程由child_process.fork()方法创建,因此它们可以使用IPC和父进程通信,从而使各进程交替处理连接服务。
cluster模块支持两种连接分发模式(将新连接安排给某一工作进程处理):
第一种方法(也是除Windows外所有平台的默认方法),是循环法。由主进程负责监听端口,接收新连接后再将连接循环分发给工作进程。在分发中使用了一些内置技巧防止工作进程任务过载。
第二种方法是,主进程创建监听socket后发送给感兴趣的工作进程,由工作进程负责直接接收连接。
理论上第二种方法应该是效率最佳的,但在实际情况下,由于操作系统调度机制的难以捉摸,会使分发变得不稳定。(有可能出现这种情况:8个进程中的2个,分担了70%的负载。)
因为server.listen()将大部分工作交给主进程完成,因此导致普通Node.js进程与cluster作业进程差异的情况有三种:
- server.listen({fd: 7})由于文件描述符“7”是传递给父进程的,这个文件被监听后,将文件句柄(handle)传递给工作进程,而不是文件描述符“7”本身。
- server.listen(handle) 明确监听句柄,会导致工作进程直接使用该句柄,而不是和父进程通信。
- server.listen(0) 正常情况下,这种调用会导致server在随机端口上监听。但在cluster模式中,所有工作进程每次调用listen(0)时会收到相同的“随机”端口。实质上,这种端口只在第一次分配时随机,之后就变得可预料。如果要使用独立端口的话,应该根据工作进程的ID来生成端口号。
由于各工作进程是独立的进程,它们可以根据需要随时关闭或重新生成,而不影响其他进程的正常运行。只要有存活的工作进程,服务器就可以继续处理连接。如果没有存活的工作进程,现有连接会丢失,新的连接也会被拒绝。Node.js不会自动管理工作进程的数量,而应该由具体的应用根据实际需要来管理进程池。
PM2使用 Node cluster 集群模块内建负载均衡。
round-robin
其实就是找下一个空闲的 worker,但是我们可以看看 Cluster 的进化过程,从最开始的自由竞争(将引起 惊群效率),将每个连接放到 worker 竞争到由 master 获取连接再进行分配。
进程间通信
Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)
- 从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,读入流和输出流都连接在同一个进程Process 1上。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (一个关闭读入流,一个关闭输出流),这样,剩下的连接就构成了PIPE。
- 由于管道只能在父子进程之间进行通信,为了解决这一问题就提供了FIFO方法连接进程,FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读®的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。
- 信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
- 内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。
- 内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。
守护进程
通过系统命令:Linux 守护进程的启动方法
- 首先要明确,通过使用 & 来启动后台任务的时候,实际上是不再继承当前session的 stdin ,会继续继承 stdout 和 stderr 。
- 然后需要明确当用户退出当前session的时候,系统会向session发出 SIGHUP 信号,session再将该信号转发给所有子进程,子进程收到后就会退出,所以普通的后台进程是不能实现守护进程的需求的。
- 那么实现的三个思路是:
- 不要让session将 SIGHUP 信号转发给后台任务;
- 将守护任务从后台任务列表当中移出。但是光是这样还是不行,因为后台任务还是在继承该session的 stdout 和 stderr,所以还需要将这两个流重定向到外面。那么第三个思路是与前两个截然不同的,第三个是重建session,将后台任务启动到这个新进程当中。
通过node:Nodejs编写守护进程
- 创建一个进程A。
- 在进程A中创建进程B,我们可以使用fork方式,或者其他方法。
- 对进程B执行 setsid 方法。
- Linux进程组和会话
- 该进程变成一个新会话的会话领导。
- 该进程变成一个新进程组的组长。
- 该进程没有控制终端。
- 进程A退出,进程B由init进程接管。此时进程B为守护进程。
Stream
stream 转 buffer
function streamToBuffer(stream) {
return new Promise((resolve, reject) => {
let buffers = [];
stream.on('error', reject);
stream.on('data', (data) => buffers.push(data));
stream.on('end', () => resolve(Buffer.concat(buffers)));
});
}
const readStream = fs.createReadStream(path.join(__dirname, 'xxx.txt'))
const buffers = [];
readStream.on('data', function(buffer) {
buffers.push(buffer);
});
readStream.on('end', function() {
const data = Buffer.from(buffers);
//...do your stuff...
// such as write to file:
fs.writeFile('xxx.txt', data, function(err) {
// handle error, return response, etc...
});
});
readStream.on('error', function(error) {
console.error('readStream error:', error.message);
})
buffer转 stream
let Duplex = require('stream').Duplex;
function bufferToStream(buffer) {
let stream = new Duplex();
stream.push(buffer);
stream.push(null);
return stream;
}
stream pipe
const readStream = fs.createReadStream(path.join(__dirname, './xxx_read.json'))
const writeStream = fs.createWriteStream(path.join(__dirname, './xxx_write.json'))
readStream.pipe(writeStream)
readStream.on('error', (error) => {
console.log('readStream error', error.message);
})
writeStream.on('error', (error) => {
console.log('writeStream error', error.message);
})
爬虫识别
function isCrawler(request) {
const userAgent = request.headers['user-agent'];
for (let i = 0; i < CrawlerUserAgents.length; i++) {
const keyword = CrawlerUserAgents[i];
if (userAgent.indexOf(keyword) >= 0) {
return true;
}
}
return false;
}
Nginx
- (在集群模式下)抛开所有业务层的东西,只单纯的接下请求再返回,缓解IO处理能力问题,增加并发性能。
- 后端没有跨域问题,前端也挂载在nginx下,ajax请求通过nginx转发到后端没有跨域问题。
可能导致内存泄漏的情况
// 文件 a
const obj = {
array: []
};
// 文件 b
const obj = require("./a"); // 文件a是被缓存的,不会被垃圾回收
function createUser(username) {
obj.array.push({
name: username
});
}
//如果一直操作b文件的createUser方法,就可能会导致内存泄漏
//a可能是一个第三方库
杂
module.filename:开发期间,该行代码所在的文件。
__filename:始终等于 module.filename。
__dirname:开发期间,该行代码所在的目录。
process.cwd():运行node的工作目录,可以使用 cd /d 修改工作目录。
require.main.filename:用node命令启动的module的filename, 如 node xxx,这里的filename就是这个xxx。
require()方法的坐标路径是:module.filename;
fs.readFile()的坐标路径是:process.cwd()。
可以使用process.stdin读取cmd输入的内容。
path
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
node删除文件夹
function deleteFolderRecursive(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach((file) => {
const curPath = path + "/" + file;
if (fs.statSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
};
运行
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"
后,进入chrome://inspect
进行调试.