首頁  >  文章  >  web前端  >  Node.js中建立和管理外部程序詳解_node.js

Node.js中建立和管理外部程序詳解_node.js

WBOY
WBOY原創
2016-05-16 16:39:421181瀏覽

Node被設計用來有效率的處理I/O操作,但你應該知道,有些類型的程式並不適合這種模式。例如,如果你打算用Node處理一個CPU密集的任務,你可能會堵塞事件循環,並因此降低了程式的回應。替代辦法是,把CPU密集的任務分配給一個單獨的程序來處理,從而釋放事件循環。 Node允許你產生進程,並把這個新進程做成它父進程的子進程。在Node裡,子進程可以和父進程進行雙向通信,而且在某種程度上,父進程還可以監控和管理子進程。

另一個需要使用子程序的情況是,當你想簡單地執行一個外部指令,並讓Node取得指令的回傳值時。例如,你可以執行一個UNIX指令、腳本或其他那些不能在Node裡直接執行的指令。

本章將向你展示如何執行外部命令,創建,並和子進程通信,以及終結子進程。重點是讓你了解如何在Node進程外完成一系列任務。

執行外部指令

當你需要執行一個外部shell指令或執行檔時,你可以使用child_process模組,像這樣導入它:

複製程式碼 程式碼如下:

var child_process = require(‘child_process')

然後可以用模組內的exec函數來執行外部指令:
複製程式碼 程式碼如下:

var exec = child_process.exec;

exec(command,callback);


exec的第一個參數是你準備要執行的shell指令字串,第二個參數是一個回呼函數。這個回呼函數將會在exec執行完外部指令或有錯誤發生時被呼叫。回呼函數有三個參數:error,stdout,stderr,看下面的範例:
複製程式碼 程式碼如下:

exec(‘ls',function(err,stdout,stderr){

         //譯者註:若使用windows,可改為windows指令,如dir,後面不再贅述

});

如果有錯誤發生,第一個參數將會是一個Error類別的實例,如果第一個參數不包含錯誤,那麼第二個參數stdout將會包含指令的標準輸出。最後一個參數包含命令相關的錯誤輸出。

清單8-1 展示了一個複雜些的執行外部指令的範例

LISTING 8-1:執行外部指令(原始碼:chapter8/01_external_command.js)

複製程式碼 程式碼如下:

//導入child_process模組的exec函數
var exec = require(‘child_process').exec;
//呼叫“cat *.js | wc -l”指令
exec(‘cat *.js | wc –l ‘, function(err, stdout, stderr ){  //第四行
         //指令退出或呼叫失敗
         if( err ){
              //啟動外部程序失敗
              console.log(‘child_process 退出,錯誤碼是:',err.code);
              return;
         }
}

第四行,我們把「cat *.js | wc -l」當作第一個參數傳遞給exec,你也可以嘗試任何其它指令,只要你在shell裡使用過的指令都可以。

然後將一個回呼函數作為第二個參數,它將會在錯誤發生或子程序終結的時候被呼叫。

還可以在回呼函數之前傳遞第三個可選參數,它包含一些配置選項,例如:

複製程式碼 程式碼如下:

var exec = require(‘child_process').exec;       

var options ={
         timeout: 1000,
         killSignal: ‘SIGKILL'
     };

exec(‘cat *.js | wc –l ‘, options, function(err,stdout,stderr){
         //…
});

可以使用的參數有:

1.cwd —— 目前目錄,可以指定目前工作目錄。
2.encoding —— 子進程輸出內容的編碼格式,預設值是”utf8”,也就是UTF-8編碼。如果子進程的輸出不是utf8,你可以用這個參數來設置,支援的編碼格式有:

複製程式碼 程式碼如下:

ascii
utf8
ucs2
base64

如果你想了解Node支援的這些編碼格式的更多信息,請參考第4章「使用Buffer處理,編碼,解碼二進位資料」。

1.timeout —— 以毫秒為單位的指令執行逾時時間,預設為0,即無限制,一直等到子程序結束。
2.maxBuffer —— 指定stdout流和stderr流允許輸出的最大位元組數,如果達到最大值,子程序會被殺死。預設值是200*1024。
3.killSignal —— 當逾時或輸出快取達到最大值時發送給子程序的終結訊號。預設值是“SIGTERM”,它將給子程序發送一個終結訊號。通常都會使用這種有序的方式來結束流程。當用SIGTERM訊號時,進程接收到以後還可以進行處理或重寫訊號處理器的預設行為。如果目標進程需要,你可以同時向他傳遞其它的訊號(例如SIGUSR1)。你也可以選擇發送SIGKILL訊號,它會被作業系統處理並強制立刻結束子進程,這樣的話,子進程的任何清理操作都不會被執行。

如果你想更進一步的控制進程的結束,可以使用child_process.spawn指令,後面會介紹。

1.evn —— 指定傳遞給子進程的環境變量,預設是null,也就是說子進程會繼承在它被建立之前的所有父進程的環境變數。

注意:使用killSignal選項,你可以以字串的形式向目標程序發送訊號。在Node裡訊號以字串的形式存在,以下是UNIX訊號和對應預設操作的列表:

你可能想要為子行程提供一組可擴充的父級環境變數。如果直接去修改process.env對象,你會改變Node進程內所有模組的環境變量,這樣會惹很多麻煩。替代方案是,建立一個新對象,複製process.env裡的所有參數,請參閱範例8-2:

LISTING 8-2:使用參數化的環境變數來執行指令(原始碼:chapter8/02_env_vars_augment.js)

複製程式碼 程式碼如下:

var env = process.env,
    varName,
    envCopy = {},
    exec = require(‘child_prcess').exec;
//將process.env複製到envCopy
for( vaName in ev){
    envCopy[varName] = env[varName];
}

//設定一些自訂變數
envCopy[‘CUSTOM ENV VAR1'] = ‘some value';
envCopy[‘CUSTOM ENV VAR2'] = ‘some other value';

//使用process.env和自訂變數來執行指令
exec(‘ls –la',{env: envCopy}, function(err,stdout,stderr){
    if(err){ throw err; }
    console.log(‘stdout:', stdout);
    console.log(‘stderr:',stderr);
}

上面例子,創建了一個用來保存環境變量的envCopy變量,它首先從process.env那裡複製Node進程的環境變量,然後又添加或替換了一些需要修改的環境變量,最後把envCopy作為環境變數參數傳遞給exec函數並執行外部命令。

記住,環境變數是透過作業系統在進程之間傳遞的,所有類型的環境變數值都是以字串的形式到達子進程的。例如,如果父進程將數字123作為一個環境變量,子進程將會以字串的形式接收「123」。

下面的例子,將在同一個目錄裡建立2個Node腳本:parent.js和child.js,第一個腳本將會呼叫第二個,下面我們來建立這兩個檔案:

LISTING 8-3: 父行程設定環境變數(chapter8/03_environment_number_parent.js)

複製程式碼 程式碼如下:

var exec = require('child_process').exec;

exec('node child.js', {env: {number: 123}}, function(err, stdout, stderr) {

if (err) { throw err; }

    console.log('stdout:n', stdout);

    console.log('stderr:n', stderr);

});

把這段程式碼存到parent.js,下面是子進程的源碼,把它們存到child.js(見例8-4)

例 8-4: 子程序解析環境變數(chapter8/04_environment_number_child.js)

複製程式碼 程式碼如下:

var   number = process.env.number;

console.log(typeof(number)); // → "string"

number = parseInt(number, 10);

console.log(typeof(number)); // → "number"

當你把這個檔案儲存為child.js後,就可以在這個目錄下執行下面的指令:

複製程式碼 程式碼如下:

$ node parent.js

將會看到以下的輸出:

複製程式碼 程式碼如下:

sdtou:

string

number

stderr:

可以看到,儘管父進程傳遞了一個數字型的環境變量,但是子進程卻以字符串形式接收它(參見輸出的第二行),在第三行你把這個字符串解析成了一個數字。

生成子程序

如你所見,可以使用child_process.exec()函數來啟動外部進程,並在進程結束的時候呼叫你的回呼函數,這樣用起來很簡單,不過也有一些缺點:

1.除了使用命令列參數和環境變量,使用exec()無法和子進程通訊
2.子程序的輸出是被快取的,因此你無法流化它,它可能會耗盡記憶體

幸運的是,Node的child_process模組允許更細粒度的控制子程序的啟動,停止,及其它常規操作。你可以在應用程式裡啟動一個新的子進程,Node提供一個雙向的通訊通道,可以讓父進程和子進程相互收發字串資料。父進程還可以有些針對子進程的管理操作,給子進程發送訊號,以及強制關閉子進程。

建立子程序

你可以使用child_process.spawn函式來建立一個新的子進程,見例8-5:

例 8-5: 產生子程序。 (chapter8/05_spawning_child.js)

複製程式碼 程式碼如下:

// 導入child_process模組的spawn函式

var spawn = require('child_process').spawn;

// 產生用來執行 "tail -f /var/log/system.log"指令的子程序

var child = spawn('tail', ['-f', '/var/log/system.log']);

上面程式碼產生了一個用來執行tail指令的子程序,並將「-f」和「/bar/log/system.log」當作參數。 tail指令將會監控/var/log/system.og檔案(如果有的話),然後將所有追加的新資料輸出到stdout標準輸出流。 spawn函數傳回一個ChildProcess對象,它是一個指針對象,封裝了真實進程的存取介面。這個例子裡我們把這個新的描述子賦值給一個叫做child的變數。

監聽來自子程序的資料

任何包含stdout屬性的子程序句柄,都會將子程序的標準輸出stdout作為一個流對象,你可以在這個流對像上綁定data事件,這樣每當有資料塊可用時,就會調用對應的回調函數,請看下面的例子:

複製程式碼 程式碼如下:

 //將子程序的輸出印到控制台

child.stdout.on(‘data',function(data){

         console.log(‘tail output: ‘ data);

});

每當子程序將資料輸出到標準輸出stdout時,父行程就會被通知並把資料印到控制台。

除了標準輸出,進程還有另一個預設輸出流:標準錯誤流,通常用這個流來輸出錯誤訊息。

在這個例子裡,如果/var/log/system.log檔案不存在,tail進程將會輸出類似下面的訊息:“/var/log/system.log:No such file or directory”,透過監聽stderr流,父進程會在這種錯誤發生時被通知。

父行程可以這樣監聽標準錯誤流:

複製程式碼 程式碼如下:

child.stderr.on('data', function(data) {

    console.log('tail error output:', data);

});

 stderr屬性和stdout一樣,也是唯讀流,每當子程序往標準錯誤流裡輸出資料時,父程序就會被通知,並輸出資料。

傳送資料到子程序

除了從子進程的輸出流裡接收數據,父進程還可以透過childPoces.stdin屬性往子進程的標準輸入裡寫入數據,以此來往子進程發送數據。

子程序可以透過process.stdin只讀流來監聽標準輸入的數據,但是注意你首先必須得恢復(resume)標準輸入流,因為它默認處於暫停(paused)狀態。

例8-6將會建立一個包含下列功能的程式:

1. 1 應用:一個簡單的應用程序,可以從標準輸入接收整型,然後相加,再把相加以後的結果輸出到標準輸出流。這個應用程式作為一個簡單的計算服務, 把Node進程模擬成一個可以執行特定工作的外部服務。

2.測試 1應用的客戶端,傳送隨機整型,然後輸出結果。用來示範Node進程如何產生一個子進程然後讓它執行特定的任務。

用下面例8-6的程式碼建立一個名為plus_one.js的檔案:

例 8-6: 1 應用程式(chapter8/06_plus_one.js)

複製程式碼 程式碼如下:

// 恢復預設是暫停狀態的標準輸入流
process.stdin.resume();
process.stdin.on('data', function(data) {
    var number;
    try {
        // 將輸入資料解析為整數
        number = parseInt(data.toString(), 10);
        // 1
        number = 1;
        // 輸出結果
        process.stdout.write(number "n");
    } catch(err) {
        process.stderr.write(err.message "n");
    }
});

上面程式碼裡,我們等待來自stdin標準輸入流的數據,每當有數據可用,就假設它是個整數並把它解析到一個整型變數裡,然後加1,並把結果輸出到標準輸出流。

可以透過下面指令來執行這個程式:

複製程式碼 程式碼如下:

    $ node plus_one.js

執行後程式就開始等待輸入,如果你輸入一個整數然後按回車,就會看到一個被加1以後的數字被顯示到螢幕上。

可以按Ctrl-C來退出程式。

一個測試客戶端

現在你要建立一個Node進程來使用前面的「 1應用程式」提供的計算服務。

先建立一個名為plus_one_test.js的文件,內容見例8-7:

例 8-7: 測試 1應用(chapter8/07_plus_one_test.js)

複製程式碼 程式碼如下:

var spawn = require('child_process').spawn;
// 產生一個子程序來執行 1應用程式
var child = spawn('node', ['plus_one.js']);
// 每一秒呼叫一次函數
setInterval(function() {
    // Create a random number smaller than 10.000
    var number = Math.floor(Math.random() * 10000);
    // Send that number to the child process:
    child.stdin.write(number "n");
    // Get the response from the child process and print it:
    child.stdout.once('data', function(data) {
        console.log('child replied to ' number ' with: ' data);
    });
}, 1000);
child.stderr.on('data', function(data) {
    process.stdout.write(data);
});

 從第一行到第四行啟動了一個用來運行「 1應用」的子進程,然後使用setInterval函數每秒鐘執行一次下列操作:

1..新建一個小於10000的隨機數
2.將這個數字當作字串傳遞給子程序
3.等待子程序回復一個字串
4.因為你想每次只接收1個數字的計算結果,所以需要使用child.stdout.once而不是child.stdout.on。如果使用了後者,會每隔1秒註冊一個data事件的回調函數,每個被註冊的回呼函數都會在子進程的stdout接收到資料時被執行,這樣你會發現同一個計算結果會被輸出多次,這種行為顯然是錯的。

在子行程退出時接收通知

當子程序退出時,exit事件會被觸發。例8-8展示如何監聽它:

例 8-8: 監聽子程序的退出事件 (chapter8/09_listen_child_exit.js)

複製程式碼 程式碼如下:

var spawn = require('child_process').spawn;
// 產生子程序來執行 "ls -la"指令
var child = spawn('ls', ['-la']);
child.stdout.on('data', function(data) {
    console.log('data from child: ' data);
});

// 當子程序退出:
child.on('exit', function(code) {
    console.log('child process terminated with code ' code);
});

最後幾行加黑的程式碼,父行程使用子行程的exit事件來監聽它的退出事件,當事件發生時,控制台顯示對應的輸出。子行程的退出碼會被當作第一個參數傳遞給回呼函數。有些程式使用一個非0的退出碼來代表某種失敗狀態。例如,如果你嘗試執行指令“ls –al click filename.txt”,但目前目錄沒有這個文件,你就會得到一個值為1的退出碼,見例8-9:

例8-9:取得子程序的退出碼 (chapter8/10_child_exit_code.js)

複製程式碼 程式碼如下:

var spawn = require('child_process').spawn;
// 產生子進程,執行"ls does_not_exist.txt" 指令
var child = spawn('ls', ['does_not_exist.txt']);
// 當子行程退出
child.on('exit', function(code) {
    console.log('child process terminated with code ' code);
});

這個例子裡,exit事件觸發了回呼函數,並把子行程的退出碼當作第一個參數傳遞給它。如果子進程是被訊號殺死而導致的非正常退出,那麼對應的訊號碼會被當作第二個參數傳遞給回呼函數,如例8-10:

LISTING 8-10: 取得子程序的退出訊號(chapter8/11_child_exit_signal.js)

複製程式碼 程式碼如下:

var spawn = require('child_process').spawn;
// 產生子進程,執行"sleep 10"指令
var child = spawn('sleep', ['10']);
setTimeout(function() {
    child.kill();
}, 1000);
child.on('exit', function(code, signal) {
    if (code) {
        console.log('child process terminated with code ' code);
    } else if (signal) {
        console.log('child process terminated because of signal ' signal);
    }
});

這個例子裡,啟動一個子進程來執行sleep 10秒的操作,但是還沒到10秒就發送了一個SIGKILL訊號給子進程,這將會導致如下的輸出:

複製程式碼 程式碼如下:

child process terminated because of signal SIGTERM

發送訊號並殺死進程

在這部分,你將學習如何使用訊號來管理子進程。訊號是父進程用來跟子進程通信,甚至殺死子進程的簡單方式。

不同的訊號代碼代表不同的意義,有很多訊號,其中最常見的一些是用來殺死進程的。如果一個行程接收到一個它不知道如何處理的訊號,程式就會被異常中斷。有些訊號會被子進程處理,有些則只能由作業系統處理。

一般情況下,你可以使用child.kill方法來傳送一個訊號,預設發送SIGTERM訊號:

複製程式碼 程式碼如下:

var spawn = require('child_process').spawn;
var child = spawn('sleep', ['10']);
setTimeout(function() {
    child.kill();
}, 1000);

也可以透過傳入一個識別訊號的字串作為kill方法的唯一參數,來傳送某個特定的訊號:

複製程式碼 程式碼如下:

child.kill(‘SIGUSR2');

要注意的是,雖然這個方法的名字叫做kill,但是發送的訊號不一定會殺死子程序。如果子程序處理了訊號,預設的訊號行為就會被覆蓋。用Node寫的子程序可以像下面這樣重寫訊號處理器的定義:

複製程式碼 程式碼如下:

process.on('SIGUSR2', function() {
    console.log('Got   a SIGUSR2 signal');
});

現在,你定義了SIGUSR2的訊號處理器,當你的行程再收到SIGUSR2訊號的時候就不會被殺死,而是輸出「Got a SIGUSR2 signal」這句話。使用這個機制,你可以設計一個簡單的方式來跟子行程溝通甚至命令它。雖然不像使用標準輸入功能那麼豐富,但這方式要簡單很多。

小結

這一章,學習了使用child_process.exec方法來執行外部命令,這種方式可以不使用命令列參數,而是透過定義環境變數的方式把參數傳遞給子進程。

也學習了透過呼叫child_process.spawn方法產生子進程的方式來呼叫外部命令,這種方式你可以使用輸入流,輸出流來跟子進程通信,或者使用信號來跟子進程通信以及殺死進程。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn