在Node.js中,应用程序在单个线程上运行,关于此部分的讲解可以看 https://blog.youkuaiyun.com/qq_39263663/article/details/80335763 中关于Node.js事件驱动模型的介绍。Node.js对应用程序处理使用单个线程,使Node.js进程更加高效和快捷。但是大多数服务器均具有多个处理器,并且你可以利用这些处理器扩展你的Node.js应用程序。Node.js允许用户从主应用程序把工作派生为随后可以彼此并行和主应用程序一起处理的各个进程。
为了方便使用多个进程,Node.js提供了3个特定的模块。process(进程)模块提供了访问正在运行的进程。child_process模块使你可以创建子进程,并与它们通信。cluster(集群)某块提供了实现共享相同端口的集群服务器的能力,从而允许多个请求被同时处理。
1,了解process模块
process模块是一个无须使用require()就可以从你的Node.js应用程序进行访问的全局对象。该模块使你能够访问正在运行的进程以及底层硬件体系结构的信息。
1.1,了解进程I/O管道
process模块为进程stdin,stdout和stderr提供了对标志I/O管道的访问。stdin是进程的标准输入管道,它通常是控制台。这意味着,你可以通过使用下面的代码读取控制台输入:
process.stdin.on('data',function(data){
console.log('Console input:'+data);
});
效果显示如下:
process模拟的stdout和stderr属性是可以相应地处理Writable流。
1.2,了解进程的信号
process模块的一大特点是,它允许你注册监听器来处理操作系统发送给一个进程的信号。当你需要在一个进程停止或终止前执行某些动作,例如执行清理操作时,这是很有用的。下表列出了你可以为其添加监听器的进程事件。
为了注册一个进程信号,只需使用on(event,callback)方法,例如,要为SIGBREAK事件注册一个事件处理程序,你可以使用下面的代码:
process.on('SIGBREAK',function(){
console.log('Got a SIGBREAK');
});
事件 | 说明 |
SIGUSR1 | 当Node.js调试器启动时发出。你可以添加一个监听器,但你不能停止调试器的启动 |
SIGPIPE | 当进程试图写一个在另一端没有进程连接的管道时发出 |
SIGHUP | 在Windows上控制台关闭窗口和其他平台上发生各种类似的情况时发出。 |
SIGTERM | |
SIGINT | |
SIGBREAK | |
SIGWINCH | |
SIGKILL | |
SIGSTOP |
1.3,使用process模块控制进程执行
process模块使你能够对进程的执行施加一定的控制。特别是,它使你能够停止当前进程,杀死另一个进程,或安排工作在事件队列中运行。例如,退出当前Node.js进程,你可以使用:
process.exit(0)
下表列出了可在process模块中使用的进程控制方法。
方法 | 说明 |
abort() | 使当前的Node.js应用程序发出一个abort事件,退出,并产生一个内存核心转储文件 |
exit([code]) | 使当前的Node.js应用程序退出,并返回指定code |
kill(pid,[signal]) | 导致当前系统向指定的pid的进程发送一个kill信号。默认的signal(信号值)是SIGTREM,但你可以指定另一个信号 |
nextTick(callback) | 调度Node.js应用程序的队列中的callback函数 |
1.4,从process模块获取信息
process模块有丰富的与正在运行的进程和系统体系结构相关的信息。当你实现应用程序时,这些信息可能是有用的。例如,process.pid属性提供了随后你可以让应用程序使用的进程ID。
下表列出了你可以从process模块访问的属性和方法,并介绍了它们的返回值。
方法 | 说明 |
version | 指定Node.js的版本 |
versions | 提供了一个对象,它包含了本Node.js应用程序所需的模块和版本 |
config | 包含用于编译当前节点可执行程序的配置选项 |
argv | 包含用于启动Node.js应用程序的命令参数 |
execPath | 指定Node.js从中启动的绝对路径 |
execArgv | 指定用于启动应用程序的特定于节点的命令行选项 |
chdir(directory) | 更改应用程序的当前工作目录 |
cwd() | 返回进程的当前工作目录 |
env | 包含在该进程的环境中指定的键/值对 |
pid | 指定当前进程的ID |
title | 指定当前运行的进程的标题 |
arch | 指定进程正在运行的处理器体系结构 |
platform | 指定操作系统平台 |
memoryUsage() | 描述Node.js进程的当前内存使用情况。你需要使用util.inspect()方法读取对象。 |
maxTickDepth | 指定被nextTick()调度的在阻塞I/O被处理之前运行的事件最大数量。 |
uptime() | 包含Node.js处理器硬件运行的秒数 |
hrtime() | 在元祖数组[seconds,nanoseconds]中返回一个高精度的时间 |
getgid() | 在POSIX平台上,返回这个进程的数值型组ID |
setgid(id) | |
getuid() | |
setuid(id) | |
getgroups() | |
setgroups(groups) | |
initgroups(user,extra_group) |
为了帮助你了解使用process模块获取信息,下面清单中的代码进行了一系列的调用,并把结果输出到控制台。
var util = require('util');
console.log('Current directory:'+process.cwd());
console.log('Environment Setting: '+JSON.stringify(process.env));
console.log('Node Args: ' + process.argv);
console.log('Execution Path:' + process.execPath);
console.log('Execution Args:' + JSON.stringify(process.execArgv));
console.log('Node Version: ' + process.version);
console.log('Module Versions: ' + JSON.stringify(process.versions));
console.log('Process Id:' + process.pid);
console.log('Process Title ' + process.title);
console.log('Process Platform:' + process.platform);
console.log('Process Architecture: ' + process.arch);
console.log('Memory Usage: ' + util.inspect(process.memoryUsage()));
var start = process.hrtime();
setTimeout(function(){
var delta = process.hrtime(start);
console.log('High-Res timer took %d seconds and %d nanoseconds',delta[0],+ delta[1]);
console.log('Node has been running %d seconds',process.uptime());
},1000);
2,实现子进程
若要使你的Node.js应用程序能利用服务器的多个处理器的优势,就需要吧工作分包给子进程。chile_process模块使你可以在其他进程上产生,派生,并执行工作。以下各节讨论在其他进程上执行任务的过程。
#请注意,子进程不能直接访问彼此或父进程中的全局内存。因此,你需要设计自己的应用程序以并行运行。
2.1,了解ChildProess对象
child_process模块提供了一个被称为ChildProcess的新类,它作为可以从父进程访问的子进程的表示形式。这使你可以从启动子进程的父进程控制,结束,并将消息发送到子进程。
process模块也是一个ChildProcess对象。这意味着,当你从父模块访问process时,它是父ChildProcess对象;但是当你从子进程访问process时,它是ChildProcess对象。
下表列出了可以在ChildProcess对象上发出的事件。你可以为这些事件实现处理程序终止,或将消息发送回父进程的情况。
事件 | 说明 |
message | 当ChildProcess对象调用send()方法来发送数据时发出。在这个事件上的监听器实现一个回调函数,它随后可以 读出发送的数据。例如:child.on('send',function(message){console.log(message)}); |
error | 在工作进程中出现错误时发出。该处理程序接收一个错误对象作为唯一的参数。 |
exit | 当工作进程结束时发出,接收两个参数,code和signal |
close | 当工作进程的所有stdin流都已经终止的时候发出。它与exit不同,因为多个进程可以共享相同的stdio流 |
disconnect | 当disconnect()在一个工作进程上被调用时发出 |
下表列出了可以在一个子进程上被调用的方法。
方法 | 说明 |
kill([signal]) | 导致操作系统发送一个kill信号给子进程 |
send(message,[sendHandle]) | 将消息发送到句柄。该消息可以是字符串或对象。可选的sendHandle参数让你可以吧TCPServer或Socket对象发送到客户端。这允许客户端进程共享相同的端口和地址 |
disconnect() | 关闭父进程与子进程之间的进程间通信(或IPC)通道,并把父进程和子进程的连接标志都设置为false |
属性 | 方法 |
stdin | 输入Writable流 |
stdout | 标准输出Readable流 |
strerr | 用于输出错误的标准输出Readable流 |
pid | 进程的ID |
connected | 一个布尔值,在disconnect()被调用后,它设置诶false。当他是false时,不能将消息发送给子进程 |
2.2,使用exec()在另一个进程上执行一个系统命令
从一个Node.js进程中把工作添加到另一个进程的最简单方法是使用exec()函数在一个子shell中执行系统命令。exec()函数几乎可以执行能从控制台提示符下执行的任何东西,如二进制可执行文件,shell脚本,Python脚本或批处理文件。
执行时,exec()函数创建一个系统子shell,然后在那个shell中执行命令字符串,就好像你已经从一个控制台提示符下执行它。这给了你能够充分利用控制台shell的功能的优势,如在命令行上访问环境变量。
exec()函数返回一个ChildProcess对象,它的语法如下所示:
child_process.exec(command,[options],callback)
command参数是一个字符串,它指定了在子shell中执行的命令。options参数是一个对象,它指定执行命令时使用的设置,如在当前工作目录。下表列出了使用exec()和execFile()命令时可以指定的选项。
callback参数是接受error,stdout和stderr这3个参数的函数。如果在执行命令时遇到错误,error参数就传递一个错误对象。stdout和stderr都是包含执行命令的输出的Buffer对象。
选项 | 说明 |
cwd | 指定子进程执行的当前工作目录 |
enc | 一个对象,它指定property:value作为环境的键/值对 |
encoding | 指定存储命令的输出时输出缓冲区使用的编码 |
maxBuffer | 指定stdout和stderr输出缓冲区的大小。默认值是200*1024 |
timeout | 指定父进程在杀死子进程之前,如果子进程尚未完成,等待的毫秒数 |
killSignal | 指定终止子进程时使用的kill信号。默认值是SIGTERM |
下面的清单显示了使用exec()函数执行一个系统命令的例子。
var childProcess = require('child_process');
var options = {
maxBuffer:100*1024,
encoding:'unicode',
timeout:5000
};
var child = childProcess.exec('dir /C',options,function(err,stdout,stderr){
if(err){
console.log(err.stack);
console.log('Error Code:'+error.code);
console.log('Error Signal: '+error.signal);
}console.log('Result: \n' + stdout);
if(stderr.length){
console.log('Error: '+stderr);
}
});
child.on('exit',function(code){
console.log('Completed with code: '+code);
});
2.3,使用execFile()在另一个进程上执行一个可执行文件
从另一个Node.js进程中吧工作添加到另一个进程的最简单方法是,使用execFile()函数在另一个进程上执行可执行文件。这与exec()是非常相似的,不同之处在于execFile()没有使用子shell。这使得execFile()更轻量,但是也意味着要执行的命令必须是一个二进制可执行文件。Linux的shell脚本和Windows的批处理文件不能使用execFile()函数来执行。
execFile()函数返回一个ChildProcess对象,它的调用语法如下所示:
child_process.execFile(file,args,options,callback)
file参数是一个字符串,它指定要执行的可执行文件的路径。args参数是一个数组,用于指定传递给可执行文件的命令行参数。options参数是一个对象,它指定执行命令时使用的设置,比如当前的工作目录。
var childProcess = require('child_process');
var options = {maxBuffer:100*1024,timeout:5000};
var child = childProcess.execFile('ping.exe',['-n','1','baidu.com'],options,function(err,stdout,stderr){
if(err){
console.log(err.stack);
console.log('Error Code:' + err.code);
console.log('Error Signal:' + err.signal);
}
console.log('Result: \n' + stdout);
if(stderr.length){
console.log('Errors: '+stderr);
}
});
child.on('exit',function(code){
console.log('Child completed with code: '+code);
});
2.4,使用spawn()在另一个Node.js实例中产生一个进程
从一个Node.js进程中吧工作加入到另一个进程中的一个相当复杂的方法是产生(spawn)另一个进程,连接它们之间stdio,stdout和stderr的管道,然后在新的进程中使用spawn()函数执行一个文件。这种方法比单纯使用exec()的负担稍重一些,但提供了一些很大的好处。
spawn()和exec()、execFile()的主要区别是产生的进程中的stdin可以进行配置,并且stdout和stderr都是父进程中的Readable流。这意味着exec()和execFile()必须先执行完成,然后才能读取缓冲区输出。但是,一旦一个spawn()进程的输出数据已被写入,你就可以读取它。
spawn()函数返回一个ChildProcess对象,它的语法如下所示:
child_process.spawn(command,[args],[options])
command参数是一个字符串,它指定被执行的命令。args参数是一个数组,用于指定传递给可执行命令的命令行参数。options参数是一个对象,它指定执行命令时使用的设置,比如当前的工作目录。下表列出了spawn()命令可以指定的选项。
选项 | 说明 |
cwd | 表示子进程的当前工作目录 |
env | 一个对象,它指定property:value作为环境的键/值对 |
detached | 一个布尔值,如果为true,则使子进程成为新进程组的组长,即使父进程退出,也让这个进程继续。你还应该使用child.unref() ,使得父进程退出之前不等待子进程 |
uid | 对于POSIX进程,指定进程的用户标识 |
gid | 对于POSIX进程,指定进程的组标志 |
stdio | 定义子进程的stdio配置([stdin,stdout,stderr])。默认情况下,Node.js为[stdin,stdout,stderr]打开文件描述符[0,1,2]。此字符串定义每个 输入和输出流的配置。例如: ['ipc','ipc','ipc'] 下列选项可用于此: ‘pipe’——创建子进程和父进程之间的管道。父进程可用使用ChildProcess.stdio[fd]访问改管道,其中fd是文件描述符,用[0,1,2]代表[stdin,stdout,stderr] ‘ipc’——父进程和子进程之间创建一个IPC通道,使用前面描述的send()方法来传递消息/文件描述符 ‘ignore’——在子进程中不设置一个文件描述符 Stream——指定使用在父进程中定义的Readable或Writable流对象。Stream的基本文件描述符被复制到子进程,因此数据可用在子进 程和父进程之间相互流式传输 文件描述符整数——指定使用一个文件描述符的整数值 null,undefined——使用[stdin,stdout,stderr]值的默认值[0,1,2] |
下面的清单是使用spawn()函数执行一个系统命令的例子。
var spawn = require('child_process').spawn;
var options = {
env:{user:'brad'},
detached:false,
stdio:['pipe','pipe','pipe']
};
var child = spawn('netstat',['-e']);
child.stdout.on('data',function(data){
console.log(data.toString());
});
child.stderr.on('data',function(data){
console.log(data.toString());
});
child.on('exit',function(code){
console.log('Child exited with code',code);
});
2.5 实现子派生
Node.js提供一种特殊形式的进程产生方式,被称为派生(fork),其目的是执行在一个单独的处理器上运行的另一个V8引擎示例中的Node.js模块代码。你可用用派生来并行运行多个服务。不过,这需要时间来运转V8的一个新示例,每个实例需要大约10mb的内存。因此,你应该把派生的进程设计为存活期更长的,而且你不应该需要大量的派生进程。请记住,你真的不能从创建比你系统中的CPU数目更多的进程中得到性能收益。
不同于spawn,在派生时,你不能为子进程配置stdio。相反,你使用在ChildProcess对象中的send()机制在父进程和子进程之间通信。
fork()函数返回一个ChildProcess对象,它的语法如下所示:
child_process.fork(modulePath,[args],[options])
modulePath参数是一个字符串,它指定会被新的Node.js实例启动的JavaScript文件的路径。args参数是一个数组,用于指定传递给node命令的命令行参数。options参数为一个对象,它指定执行命令时使用的设置,如当前工作目录。下表列出了可用在fork()命令中指定的选项。
callback参数是接受error,stdout和stderr这3个参数的函数。如果在执行命令时遇到错误,error参数就传递一个错误对象,stdout和stderr都是Readable流对象。
选项 | 说明 |
cwd | 指定子进程的当前工作目录 |
env | 一个对象,指定property:value作为环境的键/值对 |
encoding | 指定把数据写入输入流时和穿越send()IPC机制时使用的编码 |
execPath | 指定用于创建产生的Node.js进程的可执行文件。这可用让你对Node.js不同进程使用不同的版本。然而,这是不推荐的,以防进程的功能是不相同的 |
silent | 一个布尔值,如果为true,则将导致在派生的进程中的stdout和stderr不与父进程相关联。默认为false |
下面的两个程序清单显示派生到在一个单独的进程中运行的另一Node.js实例的例子。
child_fork.js:
var child_process = require('child_process');
var options = {
env :{user:'Brad'},
encoding:'utf8'
};
function makeChild(){
var child = child_process.fork('chef.js',[],options);
child.on('message',function(message){
console.log('Served:' + message);
});
return child;
}
function sendCommand(child,command){
console.log("Requesting: " + command);
child.send({cmd:command});
}
var child1 = makeChild();
var child2 = makeChild();
var child3 = makeChild();
sendCommand(child1,"makeBreakfast");
sendCommand(child2,"makeLunch");
sendCommand(child3,"makeDinner");
chef.js
process.on('message',function(message,parent){
var meal = {};
switch(message.cmd){
case 'makeBreakfast':
meal = ["ham","egg","toast"];
break;
case 'makeLunch':
meal = ["burger","fries","shake"];
break;
case 'makeDinner':
meal = ["soup","salad","steak"];
break;
}
process.send(meal);
});
3,实现进程集群
你可以用Node.js做的最酷的东西之一,就是在同一台机器的独立进程中创建并行运行的Node.js实例的集群。
3.1,使用cluster模块
cluster模块可以让你轻松实现运行在同一台机器不同进程生的TCP或HTTP服务器集群,但它们仍使用相同的底层套接字,从而在相同的IP地址和端口组合上处理请求。cluster模块是易于实现的,它提供了多个事件,方法和属性,你可以用它们来启动和监控Node.js服务器集群。
下表列出了一个cluster模块应用程序中发出的事件。
事件 | 说明 |
fork | 当新的工作进程已经被派生时发出。callback函数接收Worker对象作为唯一参数。例如:function(Worker) |
online | 当新工作进程发回一条信息,表明它已经启动时发出。callback函数接收一个Worker对象作为唯一参数。例如:function(Worker) |
listening | 当工作进程调用listen()开始监听共享端口的时候发出。callback处理程序接收Worker对象以及表示工作进程正在监听的端口的address对象。例如:function(Worker,address) |
disconnect | 当IPC通道已经被切断时发出,如当服务器调用worker.disconnect()的时候。callback函数接收一个Worker对象作为唯一参数。例如:function(Worker) |
exit | 在worker对象已断开的时候发出。callback处理程序接收Worker,code和使用的signal。 |
setup | 在setMaster()被首次调用时发出 |
下表列出了在cluster模块中的方法和属性,你可以用它们来获取诸如该节点是否是工作节点或主节点之类的信息,以及配置和实现派生进程。
属性 | 说明 |
settings | 包含exec,args和silent属性值,用于建立集群 |
isMaster | 如果当前进程是集群的主节点,返回true |
isWorker | 如果当前的节点是集群的工作节点,返回true |
setupMaster([settings]) | 接受一个可选的settings对象,它包含exec,args和silent属性。exec属性指向工作进程JavaScript文件。args属性 是要传递的参数的数组,而silent断开工作线程的IPC机制 |
disconnect([callback]) | 断开工作进程的IPC机制,并关闭句柄。当断开连接完成时回调函数被执行 |
worker | 引用在工作进程的当前Worker对象。这不在主进程中定义 |
workers | 包含Worker对象,你可以通过标识从主进程引用它们。例如:cluster.workers[workerId] |
3.2,了解Worker对象
当一个工作进程被派生时,一个新的Worker对象同时在主进程和工作进程中创建。在工作进程中,Worker对象用来表示当前的工作进程,并与正在发送的集群事件进行交互。在主进程中,Worker对象代表子工作进程,使你的主应用程序可以向它们发送信息,接受他们的状态变化的事件,甚至杀掉它们。
下表列出了Worker对象可以发出的事件
事件 | 说明 |
message | 在工作进程收到一个新消息时发出。回调函数吧message作为唯一参数传递 |
disconnect | 在IPC通道已对这个工作进程断开后发出 |
exit | 在这个Worker对象已断开时发出 |
error | 在这个工作进程发生错误时发出 |
下表列出了Worker对象的方法和属性,你可以用它们来获取诸如节点是否是工作进程或主节点的信息,以及配置和实现派生的过程。
id | 表示该工作进程的唯一ID |
process | 指定该工作进程运行的ChildProcess对象 |
suicide | 对这个工作进程调用kill()或disconnect()时被设置为true。你可以使用此标志来确定是否要跳出尝试的循环,并优雅的退出 |
send(message,[sendHandle]) | 将消息发送到主进程 |
kill([signal]) | 通过断开IPC通道杀死当前工作进程,然后退出。将suicide标志设置为true |
disconnect() | 在工作进程中调用它时,关闭所有服务器,等待关闭事件,并断开IPC通道。当从主节点中调用它时,发送一个内部消息给工作进程,使其断开本身。设置suicide标志 |
3.3,实现一个http集群
要说明cluster模块的价值,最好的方法是通过Node.js HTTP服务器的基本实现来展示。下面的程序清单实现了一个基本的HTTP服务集群。
cluster_server.js
var cluster = require('cluster');
var http = require('http');
if(cluster.isMaster){
cluster.on('fork',function(worker){
console.log("Worker " + worker.id + " created");
});
cluster.on('listening',function(worker,address){
console.log("Worker " + worker.id + " is listening on " + address.address + ":" + address.port);
});
cluster.on('exit',function(worker,code,signal){
console.log("Worker " + worker.id + " Exited");
});
cluster.setupMaster({exec:'cluster_worker.js'});
var numCPUs = require('os').cpus().length;
for(var i =0;i<numCPUs;i++){
if(i>=4) break;
cluster.fork();
}
Object.keys(cluster.workers).forEach(function(id){
cluster.workers[id].on('message',function(message){
console.log(message);
});
});
}
cluster_worker.js
var cluster = require('cluster');
var http = require('http');
if(cluster.isWorker){
http.Server(function(req,res){
res.writeHead(200);
res.end("Process " + process.pid + " handled request");
}).listen(8080,function(){
console.log("Child Server Runing on Process: " + process.pid);
});
};
cluster_client.js
var http = require('http');
var options = {port:'8080'};
function sendRequest(){
http.request(options,function(response){
var serverData = '';
response.on('data',function(chunk){
serverData += chunk;
});
response.on('end',function(){
console.log(serverData);
});
}).end();
}
for(var i =0;i<5;i++){
console.log("Sending Request");
sendRequest();
}