利用NODE整合外部應(yīng)用程序
執(zhí)行外部應(yīng)用程序
- execFile:執(zhí)行外部程序寂纪,并且需要提供一組參數(shù),以及一個在進(jìn)程退出后的緩沖輸出的回調(diào)统翩。
- spawn:執(zhí)行外部程序锣杂,并且需要提供一組參數(shù),以及一個在進(jìn)程退出后的輸入輸出和時間的數(shù)據(jù)流接口畅厢。
- exec:在一個命令行窗口中執(zhí)行一個或多個命令冯痢,以及一個在進(jìn)程退出后緩沖輸出的回調(diào)。
- fork:在一個獨立的進(jìn)程中執(zhí)行一個Node模塊框杜,并且需要提供一組參數(shù)浦楣,以及一個類似spawn方法里的數(shù)據(jù)流和事件式的接口,同時設(shè)置好父進(jìn)程和子進(jìn)程之間的進(jìn)程間通信霸琴。
execFile
這是一個非常通用的方法椒振,運行一個外部程序并且得到相應(yīng)的輸出結(jié)果。該方法是一個異步方法梧乘,在外部應(yīng)用的輸出內(nèi)部使用buffer存放起來直到外部應(yīng)用退出時,回調(diào)被調(diào)用傳入對應(yīng)的輸出數(shù)據(jù)庐杨。
var cp = require('child_process')
cp.execFile('echo', ['hello', 'world'],
function(err, stdout, stderr) {
if (err) console.error(err);
console.log('stdout', stdout);
console.log('stderr', stderr);
});
Windows/UNIX操作系統(tǒng)中都有一個PATH的環(huán)境變量选调,PATH包含了一組可執(zhí)行程序的執(zhí)行目錄列表。在Node中灵份,當(dāng)后臺運行execvp時仁堪,當(dāng)沒有提供絕對或者相對路徑時,它會基于PATH里定義的路徑搜索所有相關(guān)的程序填渠。
如果想要快速檢查PATH路徑包含哪一些目錄弦聂,可以在Node的交互式命令解析器里輸入:
$ node
> console.log(process.env.PATH.split(':').join('\n'))
/usr/local/bin
/usr/bin/bin
...
當(dāng)然你可以繼續(xù)向PATH路徑當(dāng)中添加你的路徑,但是必須保證這個設(shè)置是在你執(zhí)行execFile之前氛什。
process.env.PATH += ':/a/new/path/to/executable';
執(zhí)行外部程序時出現(xiàn)的異常
主要的異常分為兩種莺葫,一種是提供的路徑或文件名稱不存在,一種是提供的應(yīng)用路徑被鎖定(執(zhí)行應(yīng)用的權(quán)限不足)枪眉。
- 提供的路徑或文件名不存在時:通常會報錯ENOENT
- 執(zhí)行應(yīng)用的權(quán)限不足:通常會報錯EACCESS或者EPERM
- 該程序不能在當(dāng)前的平臺下執(zhí)行時:外部程序退出返回的狀態(tài)碼非零(即err.code!=0)
spawn
對于調(diào)用一個你可能預(yù)期有大數(shù)據(jù)量輸出的外部應(yīng)用捺檬,流確實是一個很好的選擇。當(dāng)外部程序輸出的數(shù)據(jù)可用時贸铜,此時你可以選擇馬上將數(shù)據(jù)輸出到內(nèi)部應(yīng)用堡纬,通過流。相反蒿秦,而不是等到將所有數(shù)據(jù)緩存好之后再將其輸出烤镐。
var cp = require('child_process');
var child = cp.spawn('echo', ['hello', 'world']);
child.on('error', console.error);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
實例化一個spawn后,會返回一個ChildProccess對象棍鳖,該對象中包含了stdin炮叶、stdout和stderr流對象,并且因為其流的性質(zhì),可以很好的進(jìn)行無縫的處理悴灵。
var cp = require('child_process');
var cat = cp.spawn('cat', ['messy.txt']);
var sort = cp.spawn('sort');
var uniq = cp.spawn('uniq');
cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout);
exec
如果需要在命令解析器里執(zhí)行命令扛芽,你可以選擇使用exec。exec方法實際上也是調(diào)用/bin/sh(在UNIX/Linux下)或者cmd.exe(在Windows下)來執(zhí)行命令的积瞒。當(dāng)然川尖,這種方法可行的前提是,你必須擁有其他需要被執(zhí)行的命令的權(quán)限(如管道茫孔、重定向和后臺命令)叮喳。
cp.exec('cat messy.txt | sort | uniq',
function(err, stdout, stderr) {
console.log(stdout);
})
分離子進(jìn)程
-
形式上的分離
當(dāng)在進(jìn)程里開啟該進(jìn)程的子進(jìn)程以后,子進(jìn)程會依賴于父進(jìn)程缰贝。當(dāng)父進(jìn)程關(guān)閉的時候馍悟,子進(jìn)程也會隨著關(guān)閉,并且子進(jìn)程是沒有自己的獨立I/O剩晴。那么如果想讓子進(jìn)程脫離父進(jìn)程锣咒,就需要使用spawn方法,從而使得子進(jìn)程擁有和父進(jìn)程一樣的級別赞弥,即成為一個進(jìn)程組的頭毅整。
var child = cp.spawn('./', [], {detached: true});
但是此時,子進(jìn)程和父進(jìn)程之間還是通過I/O互相連接的绽左,所以如果不強(qiáng)制性終結(jié)正在運行的Node程序悼嫉,就會發(fā)現(xiàn)父進(jìn)程會一直保持活躍狀態(tài),直到子進(jìn)程結(jié)束拼窥。但是強(qiáng)制性終結(jié)Node程序以后戏蔑,longrun會繼續(xù)執(zhí)行,直到它自己終結(jié)鲁纠。
-
I/O分離
而stdio選項就是來控制子進(jìn)程的I/O連接到一個具體的地方总棵,
stdio:['pipe', 'pipe', 'pipe']
三個流分別對應(yīng)child.stdin、child.stdout和child.stderr房交。默認(rèn)這些流都是開放的彻舰,所以父進(jìn)程能夠與子進(jìn)程之間進(jìn)行通信。當(dāng)然你可以使用很暴力的方式關(guān)閉掉這些流候味,阻止父進(jìn)程與子進(jìn)程進(jìn)行通信刃唤。child.stdin.destroy(); child.stdout.destroy(); child.stderr.destroy();
但是既然我們根本不需要這些流,那就應(yīng)該在源頭上去放棄掉這些流或者重新賦值將I/O指向別的地方白群。
var fs = require('fs'); var cp = require('child_process'); var outFd = fs.openSync('./longrun.out', 'a'); var errFd = fs.openSync('./longrun.err', 'a'); var child = cp.spawn('./longrun', [], { detached: true, stdio: ['ignore', outFd, errFd] });
ignore關(guān)鍵詞就是用來放棄相對應(yīng)的流尚胞。
-
引用分離
盡管子進(jìn)程被分離了并且它和父進(jìn)程的I/O也被中斷了,但是父進(jìn)程仍然會有一個堆子進(jìn)程的內(nèi)部引用帜慢,并且只要子進(jìn)程沒有終結(jié)且這個引用沒有被移除笼裳,父進(jìn)程都不會終結(jié)唯卖。所以可以通過child.unref()方法告訴Node不要將子進(jìn)程的引用進(jìn)行計數(shù)。下面的代碼就會再子進(jìn)程執(zhí)行spawn方法之后退出躬柬。
var fs = require('fs'); var cp = require('child_process'); var outFd = fs.openSync('./longrun.out', 'a'); var errFd = fs.openSync('./longrun.err', 'a'); var child = cp.spawn('./longrun', [], { detached: true, stdio: ['ignore', outFd, errFd] }); child.unref();
fork(操作一個獨立的Node進(jìn)程)
var cp = require('child_process');
var child = cp.fork('./myChild');
默認(rèn)情況下拜轨,通過fork創(chuàng)建的子進(jìn)程所有的輸入輸出都是繼承自父進(jìn)程的,并不會有child.stdin允青、child.stdout或child.stderr橄碾。
如果想提供像spawn一樣的默認(rèn)的I/O配置,那么可以使用slient選項颠锉。
var cp = require('child_process');
var child = cp.fork('./myChild', { silent: true });
使用fork方法會開放一個IPC通道法牲,使得不同的Node進(jìn)程之間進(jìn)行消息傳送。而Node進(jìn)程之間主要是使用Event進(jìn)行通信琼掠,在子進(jìn)程這邊會暴露process.on('message')和process.send()來接收和發(fā)送消息拒垃,在父進(jìn)程這邊使用child.on('message')和child.send()。
var cp = require('child_process');
var child = cp.fork('./child');
child.on('message', function(msg) {
console.log('got a message from child', msg);
});
child.send('sending a string');
因為我們打開了一個父進(jìn)程和子進(jìn)程間的一個IPC通道瓷蛙,只要子進(jìn)程不中斷悼瓮,父進(jìn)程也就會保持活動狀態(tài)。如果需要中斷IPC通信連接速挑,可以在父進(jìn)程中顯式的實現(xiàn):child.disconnect();
一個較好的父進(jìn)程例子谤牡,考慮了多次調(diào)用以及子進(jìn)程出現(xiàn)問題的情況:
function doWork(job, cb) {
var child = cp.fork('./worker');
var cbTriggered = false;
child
.once('error', function(err) {
if (!cbTriggered) {
cb(err);
cbTriggered = true;
}
// 子進(jìn)程出現(xiàn)了異常則殺死子進(jìn)程
child.kill();
})
.once('exit', function(code, signal) {
if (!cbTriggered)
cb(new Error('Child exited with code: ' + code));
})
.once('message', function(result) {
cb(null, result);
cbTriggered = true;
})
.send(job);
}
工作池
在Node的官方文檔中有述:
這些子節(jié)點仍然是一個V8的新實例。預(yù)計每一個節(jié)點需要耗時30毫秒的啟動時間和10MB的內(nèi)存姥宝。
也就是說,你不能創(chuàng)建太多了恐疲,因為這些并不是沒有代價開銷的腊满。
所以說在實現(xiàn)的過程當(dāng)中,與其使用多個短時間的子進(jìn)程培己,還不如維護(hù)一個工作池碳蛋,池中存放了一些可以長時間運行的進(jìn)程。
那么我們就在上面doWork的基礎(chǔ)之上省咨,做一些優(yōu)化肃弟,完成我們在工作池上的一個作業(yè)分配以及發(fā)送。
var cp = require('child_process');
var cpus = require('os').cpus().length;
module.exports = function(workModule) {
// 等待的作業(yè)
var awaiting = [];
// 空閑的子進(jìn)程
var readyPool = [];
// 總子進(jìn)程的個數(shù)
var poolSize = 0;
return function doWork(job, cb) {
// 如果現(xiàn)在沒有準(zhǔn)備好的子進(jìn)程零蓉,并且總子進(jìn)程數(shù)已經(jīng)超過cpu的個數(shù)了笤受,就讓作業(yè)先排隊等待
if (!readyPool.length && poolSize > cpus)
return awaiting.push([ doWork, job, cb ]);
// 如果有空閑的子進(jìn)程則取出第一個使用,沒有的話就新建一個子進(jìn)程
var child = readyPool.length
? readyPool.shift()
: (poolSize++, cp.fork(workModule));
var cbTriggered = false;
child
// 先刪除原來的監(jiān)聽
.removeAllListeners()
.once('error', function(err) {
if (!cbTriggered) {
cb(err);
cbTriggered = true;
}
child.kill();
})
.once('exit', function() {
if (!cbTriggered)
cb(new Error('Child exited with code: ' + code));
// 進(jìn)程關(guān)閉的時候?qū)⑺鼜年犃兄刑叱? poolSize--;
var childIdx = readyPool.indexOf(child);
if (childIdx > -1) readyPool.splice(childIdx, 1);
})
.once('message', function(msg) {
cb(null, msg);
cbTriggered = true;
// 子進(jìn)程再次就緒敌蜂,將其加回readPool
readyPool.push(child);
// 如果現(xiàn)在有等待的任務(wù)箩兽,運行之
if (awaiting.length) setImmediate.apply(null, awaiting.shift());
})
}
}
在這段代碼的最后我是有一些疑問的,因為當(dāng)子進(jìn)程收到message的時候章喉,進(jìn)行了異步回調(diào)汗贫。因為當(dāng)時沒太能分清異步與多線程的區(qū)別身坐。所以我當(dāng)時就不太懂回調(diào)都還沒有結(jié)束,為什么就能回收這個進(jìn)程了落包?
那么這邊也區(qū)分一下異步與多線程部蛇。
- 異步:首先說一下DMA,DMA就是直接內(nèi)存訪問的意思咐蝇,也就是說涯鲁,擁有DMA功能的硬件在和內(nèi)存進(jìn)行數(shù)據(jù)交換的時候可以不消耗CPU資源。只要CPU在發(fā)起數(shù)據(jù)傳輸時發(fā)送一個指令嘹害,硬件就開始自己和內(nèi)存交換數(shù)據(jù)撮竿,在傳輸完成之后硬件會觸發(fā)一個中斷來通知操作完成。這些無須消耗CPU時間的I/O操作正是異步操作的硬件基礎(chǔ)笔呀。所以即使在DOS這樣的單進(jìn)程(而且無線程概念)系統(tǒng)中也同樣可以發(fā)起異步的DMA操作幢踏。因為異步操作無須額外的線程負(fù)擔(dān),并且使用回調(diào)的方式進(jìn)行處理许师,在設(shè)計良好的情況下房蝉,處理函數(shù)可以不必使用共享變量(即使無法完全不用,最起碼可以減少 共享變量的數(shù)量)微渠,減少了死鎖的可能搭幻。當(dāng)然異步操作也并非完美無暇。編寫異步操作的復(fù)雜程度較高逞盆,程序主要使用回調(diào)方式進(jìn)行處理檀蹋,與普通人的思維方式有些出入,而且難以調(diào)試云芦。
- 多線程:線程不是一個計算機(jī)硬件的功能俯逾,而是操作系統(tǒng)提供的一種邏輯功能,線程本質(zhì)上是進(jìn)程中一段并發(fā)運行的代碼舅逸,所以線程需要操作系統(tǒng)投入CPU資源來運行和調(diào)度桌肴。多線程的優(yōu)點很明顯,線程中的處理程序依然是順序執(zhí)行琉历,符合普通人的思維習(xí)慣坠七,所以編程簡單。但是多線程的缺點也同樣明顯旗笔,線程的使用(濫用)會給系統(tǒng)帶來上下文切換的額外負(fù)擔(dān)彪置。并且線程間的共享變量可能造成死鎖的出現(xiàn)。
詳細(xì)可見:淺談多線程和異步
同步運行
其實在異步的API介紹完以后换团,同步的API就顯得很簡單了悉稠。因為它和異步的API基本上是一樣的,只不過在實現(xiàn)的過程當(dāng)中會阻塞掉主線程艘包,直到子進(jìn)程模塊完成的猛。
-
如果只想同步執(zhí)行一個單獨的命令耀盗,并且得到輸出,那么可以使用execFileSync
var ex = require('child_process').execFileSync; var stdout = ex('echo', ['hello']).toString(); console.log(stdout);
-
如果想程序式同步執(zhí)行多個命令卦尊,并且命令之間的結(jié)果存在相互依賴的關(guān)系叛拷,可以使用spawnSync
var sp = require('child_process').spawnSync; var ps = sp('ps', ['aux']); var grep = sp('grep', ['node']) { input: ps.stdout; encoding: 'utf-8' }); console.log(grep);
同步子進(jìn)程得到的結(jié)果包含了很多的細(xì)節(jié),這也是使用spawnSync的另外一個好處岂却。
當(dāng)然execSync也是同樣的使用方法忿薇,這里不再贅述
同步子進(jìn)程中的異常處理
如果在execSync或execFileSync執(zhí)行的結(jié)果中返回的是一個非零狀態(tài),這種情況下躏哩,將會有異常拋出署浩。這個拋出的異常對象將會包含我們在使用spawnExec返回的結(jié)果里的所有東西。我們可以訪問狀態(tài)編碼里的重要信息和stderr流扫尺。
var ex = require('child-process').execFileSync;
try {
ex('cd', ['non-existent-dir'], {
encoding: 'utf-8'
});
} catch(err) {
console.error('exit status was', err.status);
console.error('stderr', err.stderr);
}