04月23, 2018

node深入笔记(6)

来聊聊子进程及集群的相关内容。

其实在14年底的时候,我就开始接触子进程了,当时是老大写的一段node代码:

alt

当时有些概念,但其实这个体系对我来说,并不是太完整。

spawn

语法

child_process.spawn(command,[args],[options]);
  • command 必须指定的参数,指定需要执行的命令
  • args 数组,存放了所有运行该命令需要的参数
  • options 参数为一个对象,用于指定开启子进程时使用的选项
    • cwd 子进程的工作目录
    • detached 用来控制子进程的独立性
    • stdio

stdio说明

alt

示例

pipe

const {spawn} = require('child_process');
const path = require('path');
const child = spawn('node',['1.test.js','a','b','c'],{
    cwd:path.join(__dirname,'test')
});

//如果不写stdio 默认是管道类型
child.stdout.on('data',function(data){
    console.log(data.toString());
});

child.on('exit',function(){
    console.log('exit')
});
child.on('close',function(){
    console.log('close')
})
child.on('error',function(err){
    console.log('发生错误')
});
// test/1.test.js
process.argv.slice(2).forEach(function(arg){
    process.stdout.write(arg);
})

将第一个进程中的参数 传入第二个进程中去,再第二个进程中写入到文件里

const {spawn } = require('child_process');
const path = require('path');
const child1 = spawn('node',['1.test.js','a','b','c'],{
    cwd:path.join(__dirname,'test'),
    stdio:'pipe'
});
const child2 = spawn('node',['2.test.js','a','b','c'],{
    cwd:path.join(__dirname,'test'),
    stdio:'pipe'
});
child1.stdout.on('data',function(data){
  child2.stdout.write(data);
});
// test/1.test.js
process.argv.slice(2).forEach(function(arg){
    process.stdout.write(arg);
})
// test/2.test.js
let fs = require('fs');
let ws = fs.createWriteStream('./1.txt')
process.stdout.on('data',function(data){
    ws.write(data);
})

ipc

简单写法,可以通过send on message来发送和接收数据。

const {spawn} = require('child_process');
const path = require('path');
const child = spawn('node',['3.test.js'],{
    cwd:path.join(__dirname,'test'),
    stdio:['pipe','pipe','pipe','ipc']
})
child.send({name:'小翼'});
child.on('message',function(data){
    console.log(data);
    child.kill(); // 杀死进程
});
// test/3.test.js
process.on('message',function(msg){
    process.send(`${msg.name},你好`);
})

detach

// detach 将主进程关掉,子进程可以自己运行
const {spawn} = require('child_process');
const path  = require('path');
const fd = require('fs').openSync('./test.txt','w')
const child = spawn('node',['4.test.js'],{
    cwd:path.join(__dirname,'pro'),
    stdio:['ignore',fd,'ignore'],
    detached:true
});
child.unref(); // 这一步也比较关键,不要忘记
`
// test/4.test.js
// 每隔1秒往test.txt文件中写hello
setInterval(function(){
    process.stdout.write('hello');
},1000);

fork

child_process.fork() 方法是 child_process.spawn() 的一个特殊情况,专门用于衍生新的 Node.js 进程。

我们可以简单通过spawn来实现fork方法:

const { spawn } = require('child_process');
const path = require('path');
function fork(modulePath, args, options = {}) {
    if (options.silent) {
        options.stdio = ['ignore', 'ignore', 'ignore', 'ipc']
    } else {
        options.stdio = [0, 1, 2, 'ipc']
    }
    return spawn('node', [modulePath, ...args],options)
}
let child = fork('fork.js', ['a', 'b', 'c'], {
    cwd: path.join(__dirname, 'test'),
    silent: false // 这句话的意思就是 ['ignore','ignore','ignore','ipc']
});
child.send('hello');
// test/fork.js
process.on('message',function(msg){
    console.log(msg)
})

http多进程

const {fork} = require('child_process');
const path = require('path');
const child = fork('http.js',{
    cwd:path.join(__dirname,'test')
});
const http = require('http');
http.createServer(function(req,res){
    res.setHeader("Content-Type", "text/html; charset=utf-8");
    res.end('父进程接收处理请求')
}).listen(3000);
child.send('server',server);
// test/http.js 子进程文件
const http = require('http');
process.on('message',function(msg,server){
    http.createServer(function(req,res){
        res.setHeader("Content-Type", "text/html; charset=utf-8");
        res.end('子进程接收处理请求')
    }).listen(server);
})

alt

alt

我们可以写一个http客户端请求来测试一下,是否结果会是父、子进程交替着输出结果:

// 写一个client用来跑结果
const http = require('http');
for(var i = 0;i<10;i++){
    http.get({
        hostname:'localhost',
        port:3000,
        path:'/',
        method:'GET'
    },function(res){
        res.on('data',function(data){
            console.log(data.toString())
        })
    })
}

测试结果如下:

alt

exec/execFile

child_process.exec() 和 child_process.execFile() 之间的区别会因平台而不同。 在类 Unix 操作系统(Unix、 Linux、 macOS)上,child_process.execFile() 效率更高,因为它不需要衍生 shell。 但在 Windows 上,.bat 和 .cmd 文件在没有终端的情况下是不可执行的,因此不能使用 child_process.execFile() 启动。 可以使用设置了 shell 选项的 child_process.spawn()、或使用 child_process.exec()、或衍生 cmd.exe 并将 .bat 或 .cmd 文件作为参数传入(也就是 shell 选项和 child_process.exec() 所做的工作)。 如果脚本文件名包含空格,则需要加上引号。

child_process.exec() 和 child_process.execFile() 函数可以额外指定一个可选的 callback 函数,当子进程结束时会被调用

通常如果是执行一些shell脚本,我们可以考虑使用三方的npm包:shelljs

使用

// child_process.exec(command[, options][, callback])
const { exec } = require('child_process');
exec('ls -l',function(err,stdout,stderr){
    console.log(stdout)
});
// child_process.execFile(file[, args][, options][, callback])
const { execFile } = require('child_process');
execFile('ls',['-l'],function(err,stdout,stderr){
    // 它的结果会被缓存 等待结束后一起输出 200
    console.log(stdout)
})

其中exec方法调用了execFile:

alt

cluster

为了利用多核CPU的优势,Node.js提供了一个cluster模块允许在多个子进程中运行不同的Node.js应用程序。

fork方法创建work对象

  • 可以使用fork方法开启多个子进程,在每个子进程中创建一个Node.js应用程序的实例,并且在该应用程序中运行一个模块文件。
  • fork方法返回一个隐式创建的work对象
  • 在cluster模块中,分别提供了一个isMaster属性与一个isWork属性,都是布尔值
const cluster = require('cluster');
const http = require('http')
const cpus  = require('os').cpus().length;
// 根据cpu的核数 创建对用的进程
// 可以通过ipc的方式进行进程之间的通信,默认不支持管道的方式
if(cluster.isMaster){
    cluster.setupMaster({
        stdio:'pipe'
    })
    // 在主分支中可以 创建子进程
    for(let i = 0;i<cpus;i++){
        cluster.fork();
    }
}else{
    http.createServer(function(req,res){
        res.end('ok'+process.pid);
    }).listen(3000);
}
cluster.on('online',function(worker){
    console.log('已经收到子进程#'+worker.id+"的消息");
});
// cluster.on('listening', function (worker, address) {
//     console.log('子进程中的服务器开始监听,地址为:' + address.address + ":" + address.port);
// });
cluster.on('disconnect',function(){
    console.log('断开连接')
})
cluster.on('fork',function(worker){

});
cluster.on('exit',function(){
    console.log('exit')
})

alt

我们可以通过跑客户端的方式来请求http:

alt

通过活动监视器,找到pid,关掉其中一个后,就会触发 disconnect方法。

父子进程分离

const cluster = require('cluster');
const http = require('http');
const path = require('path');
const cpus  = require('os').cpus().length;
cluster.setupMaster({
    exec:path.join(__dirname,'subprocess.js')
})
// 在主分支中可以 创建子进程
for(let i = 0;i<cpus;i++){
    cluster.fork();
}
// subprocess.js
let http = require('http');
http.createServer(function(req,res){
    res.end('ok'+process.pid);
}).listen(3000,function(){
    console.log(process.pid);
});

父子进程通信

worker.send(message,[sendHandle]);//在主进程中向子进程发送消息
process.send(message,[sendHandle]);//在子进程中向主进程发送消息
process.on('message',function(m,setHandle){});

kill服务

let cluster = require('cluster');
let http = require('http');
if (cluster.isMaster) {
    cluster.setupMaster({
        silent: true
    });
    let worker = cluster.fork();
    worker.process.stdout.on('data', function (data) {
        console.log('接收到来自客户端的请求,目标地址为:' + data);
        worker.kill(); //让子进程退出
    });
    worker.on('exit', function (code, signal) {
        console.log(arguments)
        //suicide可以判断是自动退出还是异常退出,退出之前时undefined.自动退出为true,异常退出为false
        if (worker.exitedAfterDisconnect  == true) {
            console.log(`子进程%{worker.id}自动退出`);
        } else if (worker.exitedAfterDisconnect  == false) {
            console.log(`子进程${worker.id}异常退出,退出代码为${code}`);
        }
        if (signal) {
            console.log('退出信号为 %s', signal);
        }
    });
} else {
    let server = http.createServer(function (req, res) {
        if (req.url != '/favicon.ico') {
            res.end('hello');
            process.stdout.write(req.url);
        }
    }).listen(8080);
}

总结

集群这一块,主要是考虑多利用cpu核数,来分担压力,在企业级开发中还是相当重要的。

在eggjs中,有两个链接关于集群的,大家可以看看:

文章我看的半懂,大概清楚为什么要这么做,但毕竟我node还没有怎么应用于企业级(之前写的一个DPL平台,只是简单地放在了内网中,只用了pm2,没用集群),所以等未来研究地更深了,我会再继续总结的。

本文链接:www.my-fe.pub/post/node-deep-note-6.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。