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.
}));

與異步串行遍歷的版本相比,以上代碼並行處理全部數組成員,並經過計數器變量來判斷何時全部數組成員都處理完畢了

異常處理

相關文章
相關標籤/搜索