node系列4

进程管理

NodeJS能够感知和控制自身进程的运行环境和状态,也能够建立子进程并与其协同工做,这使得NodeJS能够把多个程序组合在一块儿共同完成某项工做,并在其中充当胶水和调度器的做用。本章除了介绍与之相关的NodeJS内置模块外,还会重点介绍典型的使用场景node

开门红

如何使用NodeJS调用终端命令来简化目录拷贝编程

var child_process = require('child_process');
var util = require('util'); function copy(source, target, callback) { child_process.exec( util.format('cp -r %s/* %s', source, target), callback); } copy('a', 'b', function (err) { // ... });

从以上代码中能够看到,子进程是异步运行的,经过回调函数返回执行结果设计模式

API蜻蜓点水

NodeJS提供了哪些和进程管理有关的API数组

Process

任何一个进程都有启动进程时使用的命令行参数,有标准输入标准输出,有运行权限,有运行环境和运行状态。在NodeJS中,能够经过process对象感知和控制NodeJS自身进程的方方面面。另外须要注意的是,process不是内置模块,而是一个全局对象,所以在任何地方均可以直接使用安全

Child Process

使用child_process模块能够建立和控制子进程。该模块提供的API中最核心的是.spawn,其他API都是针对特定使用场景对它的进一步封装,算是一种语法糖服务器

Cluster

cluster模块是对child_process模块的进一步封装,专用于解决单进程NodeJS Web服务器没法充分利用多核CPU的问题。使用该模块能够简化多进程服务器程序的开发,让每一个核上运行一个工做进程,并统一经过主进程监听端口和分发请求app

应用场景

和进程管理相关的API单独介绍起来比较枯燥,所以这里从一些典型的应用场景出发,分别介绍一些重要API的使用方法异步

如何获取命令行参数

在NodeJS中能够经过process.argv获取命令行参数。可是比较意外的是,node执行程序路径和主模块文件路径固定占据了argv[0]argv[1]两个位置,而第一个命令行参数从argv[2]开始。为了让argv使用起来更加天然,能够按照如下方式处理async

function main(argv) {
    // ...
} main(process.argv.slice(2));

如何退出程序

一般一个程序作完全部事情后就正常退出了,这时程序的退出状态码为0。或者一个程序运行时发生了异常后就挂了,这时程序的退出状态码不等于0。若是咱们在代码中捕获了某个异常,可是以为程序不该该继续运行下去,须要当即退出,而且须要把退出状态码设置为指定数字,好比1,就能够按照如下方式异步编程

try {
    // ...
} catch (err) { // ... process.exit(1); }

如何控制输入输出

NodeJS程序的标准输入流(stdin)、一个标准输出流(stdout)、一个标准错误流(stderr)分别对应process.stdinprocess.stdoutprocess.stderr,第一个是只读数据流,后边两个是只写数据流,对它们的操做按照对数据流的操做方式便可。例如,console.log能够按照如下方式实现

function log() {
    process.stdout.write(
        util.format.apply(util, arguments) + '\n'); }

如何降权

在Linux系统下,咱们知道须要使用root权限才能监听1024如下端口。可是一旦完成端口监听后,继续让程序运行在root权限下存在安全隐患,所以最好能把权限降下来。如下是这样一个例子

http.createServer(callback).listen(80, function () {
    var env = process.env, uid = parseInt(env['SUDO_UID'] || process.getuid(), 10), gid = parseInt(env['SUDO_GID'] || process.getgid(), 10); process.setgid(gid); process.setuid(uid); });

上例中有几点须要注意:

  • 若是是经过sudo获取root权限的,运行程序的用户的UID和GID保存在环境变量SUDO_UIDSUDO_GID里边。若是是经过chmod +s方式获取root权限的,运行程序的用户的UID和GID可直接经过process.getuidprocess.getgid方法获取
  • process.setuidprocess.setgid方法只接受number类型的参数
  • 降权时必须先降GID再降UID,不然顺序反过来的话就没权限更改程序的GID了

如何建立子进程

如下是一个建立NodeJS子进程的例子

var child = child_process.spawn('node', [ 'xxx.js' ]);

child.stdout.on('data', function (data) { console.log('stdout: ' + data); }); child.stderr.on('data', function (data) { console.log('stderr: ' + data); }); child.on('close', function (code) { console.log('child process exited with code ' + code); });

上例中使用了.spawn(exec, args, options)方法,该方法支持三个参数。第一个参数是执行文件路径,能够是执行文件的相对或绝对路径,也能够是根据PATH环境变量能找到的执行文件名。第二个参数中,数组中的每一个成员都按顺序对应一个命令行参数。第三个参数可选,用于配置子进程的执行环境与行为

另外,上例中虽然经过子进程对象的.stdout.stderr访问子进程的输出,但经过options.stdio字段的不一样配置,能够将子进程的输入输出重定向到任何数据流上,或者让子进程共享父进程的标准输入输出流,或者直接忽略子进程的输入输出

进程间如何通信

在Linux系统下,进程之间能够经过信号互相通讯。如下是一个例子

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ]);

child.kill('SIGTERM'); /* child.js */ process.on('SIGTERM', function () { cleanUp(); process.exit(0); });

在上例中,父进程经过.kill方法向子进程发送SIGTERM信号,子进程监听process对象的SIGTERM事件响应信号。不要被.kill方法的名称迷惑了,该方法本质上是用来给进程发送信号的,进程收到信号后具体要作啥,彻底取决于信号的种类和进程自身的代码

另外,若是父子进程都是NodeJS进程,就能够经过IPC(进程间通信)双向传递数据。如下是一个例子

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ], {
        stdio: [ 0, 1, 2, 'ipc' ] }); child.on('message', function (msg) { console.log(msg); }); child.send({ hello: 'hello' }); /* child.js */ process.on('message', function (msg) { msg.hello = msg.hello.toUpperCase(); process.send(msg); });

能够看到,父进程在建立子进程时,在options.stdio字段中经过ipc开启了一条IPC通道,以后就能够监听子进程对象的message事件接收来自子进程的消息,并经过.send方法给子进程发送消息。在子进程这边,能够在process对象上监听message事件接收来自父进程的消息,并经过.send方法向父进程发送消息。数据在传递过程当中,会先在发送端使用JSON.stringify方法序列化,再在接收端使用JSON.parse方法反序列化

如何守护子进程

守护进程通常用于监控工做进程的运行状态,在工做进程不正常退出时重启工做进程,保障工做进程不间断运行。如下是一种实现方式

/* daemon.js */
function spawn(mainModule) {
    var worker = child_process.spawn('node', [ mainModule ]); worker.on('exit', function (code) { if (code !== 0) { spawn(mainModule); } }); } spawn('worker.js');

异步编程

NodeJS最大的卖点——事件机制和异步IO,没有掌握异步编程就不能说是真正学会了NodeJS。

回调

在代码中,异步编程的直接体现就是回调。异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了

function heavyCompute(n, callback) {
    var count = 0, i, j; for (i = n; i > 0; --i) { for (j = n; j > 0; --j) { count += 1; } } callback(count); } heavyCompute(10000, function (count) { console.log(count); }); console.log('hello'); -- Console ------------------------------ 100000000 hello

以上代码中的回调函数仍然先于后续代码执行。JS自己是单线程运行的,不可能在一段代码还未结束运行时去运行别的代码,所以也就不存在异步执行的概念

可是,若是某个函数作的事情是建立一个别的线程或进程,并与JS主线程并行地作一些事情,并在事情作完后通知JS主线程,那状况又不同了。咱们接着看看如下代码

setTimeout(function () {
    console.log('world'); }, 1000); console.log('hello'); -- Console ------------------------------ hello world

 此次能够看到,回调函数后于后续代码执行了。如同上边所说,JS自己是单线程的,没法异步执行,所以咱们能够认为setTimeout这类JS规范以外的由运行环境提供的特殊函数作的事情是建立一个平行线程后当即返回,让JS主进程能够接着执行后续代码,并在收到平行进程的通知后再执行回调函数。除了setTimeoutsetInterval这些常见的,这类函数还包括NodeJS提供的诸如fs.readFile之类的异步API

另外,咱们仍然回到JS是单线程运行的这个事实上,这决定了JS在执行完一段代码以前没法执行包括回调函数在内的别的代码。也就是说,即便平行线程完成工做了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程空闲时才能开始执行。如下就是这么一个例子

function heavyCompute(n) {
    var count = 0,
        i, j;

    for (i = n; i > 0; --i) {
        for (j = n; j > 0; --j) {
            count += 1;
        }
    }
}

var t = new Date();

setTimeout(function () {
    console.log(new Date() - t);
}, 1000);

heavyCompute(50000);

-- Console ------------------------------
8520

原本应该在1秒后被调用的回调函数由于JS主线程忙于运行其它代码,实际执行时间被大幅延迟

代码设计模式

异步编程有不少特有的代码设计模式,为了实现一样的功能,使用同步方式和异步方式编写的代码会有很大差别

函数返回值

使用一个函数的输出做为另外一个函数的输入是很常见的需求,在同步方式下通常按如下方式编写代码

var output = fn1(fn2('input'));
// Do something.

在异步方式下,因为函数执行结果不是经过返回值,而是经过回调函数传递,所以通常按如下方式编写代码

fn2('input', function (output2) {
    fn1(output2, function (output1) {
        // Do something.
    });
});

这种方式就是一个回调函数套一个回调函多,套得太多了很容易写出>形状的代码

遍历数组

var len = arr.length,
    i = 0;

for (; i < len; ++i) {
    arr[i] = sync(arr[i]);
}

// All array items have processed.

若是函数是异步执行的,以上代码就没法保证循环结束后全部数组成员都处理完毕了。若是数组成员必须一个接一个串行处理,则通常按照如下方式编写异步代码

(function next(i, len, callback) {
    if (i < len) {
        async(arr[i], function (value) {
            arr[i] = value;
            next(i + 1, len, callback);
        });
    } else {
        callback();
    }
}(0, arr.length, function () {
    // All array items have processed.
}));

以上代码在异步函数执行一次并返回执行结果后才传入下一个数组成员并开始下一轮执行,直到全部数组成员处理完毕后,经过回调的方式触发后续代码的执行

若是数组成员能够并行处理,但后续代码仍然须要全部数组成员处理完毕后才能执行的话,则异步代码会调整成如下形式

(function (i, len, count, callback) {
    for (; i < len; ++i) {
        (function (i) {
            async(arr[i], function (value) {
                arr[i] = value;
                if (++count === len) {
                    callback();
                }
            });
        }(i));
    }
}(0, arr.length, 0, function () {
    // All array items have processed.
}));

与异步串行遍历的版本相比,以上代码并行处理全部数组成员,并经过计数器变量来判断何时全部数组成员都处理完毕了

异常处理

相关文章
相关标签/搜索