1. 加密
1.1 Hashing
Node的加密算法是以OpenSSL庫爲基礎的,所以需要在編譯Node的時候指定添加OpenSSL支持,才能使用加密算法。
要在Node裏使用哈希,需要調用工廠方法crypto.createHash()來創建一個Hash對象。它會返回指定哈希算法的Hash新實例,幾個常見的算法有:md5、sha1、sha256、sha512、ripemd160。
在哈希中使用數據時,可以調用hash.update()來生成數據摘要。可以用更多的數據不停地更新哈希,直到需要把它輸出爲止。要把哈希輸出,只需調用hash.digest()方法,之後就不可以再添加任何輸入了,例如:
var crypto = require('crypto'); var md5 = crypto.createHash('md5'); md5.update('test'); md5.digest();
上面代碼中的輸出是以二進制格式呈現的,可以爲hash.digest()提供進制選項,例如:
var crypto = require('crypto'); var md5 = crypto.createHash('md5'); md5.update('test'); md5.digest(); md5.update('test2'); md5.digest('hex');
1.2 HMAC
HMAC結合了哈希算法和加密密鑰,是爲了阻止對簽名完整性的一些惡意***。這意味着HMAC同時使用了哈希算法以及一個加密密鑰。Node提供的HMAC API和Hash API是一樣的,只是在創建hmac對象時需要再傳入一個密鑰,例如:
var crypto = require('crypto'); var fs = require('fs'); var pem = fs.readFileSync('key.pem'); var key = pem.toString('ascii'); var hamc = crypto.createHmac('sha1', key); hmac.update('test'); hmac.digest('hex');
創建Hmac對象的密鑰必須是一個PEM編碼的密鑰,以字符串的格式傳入。在命令行用OpenSSL可以輕鬆創建一個密鑰。
1.3 公鑰加密
公鑰加密功能分佈在4個類中:Cipher、Decipher、Sign和Verify。和加密模塊一樣,它們也有工廠方法。Cipher把數據加密,Decipher解密數據,Sign爲數據創建加密簽名,Verify驗證加密簽名。
公鑰加密算法需要一組配對的密鑰:一個是私鑰,由物主保存,用來解密和數據簽名。另一個是公鑰,提供給第三方,可以用來加密數據,或者用來驗證數據是否被對應的私鑰所簽名。
Cipher類提供了用私鑰加密數據的功能。該工廠方法輸入一個算法和私鑰,然後創建cipher對象。Cipher API也採用update()方法來輸入數據,但是不太一樣。首先,如果條件允許,cipher.update()會返回一塊加密的數據,如果cipher中的數據加上傳給cipher.update()的數據足夠用來創建一個或多個加密塊,那這些加密塊就會被返回,否則輸入會被保存在cipher對象內。Cipher還有一個新的方法cipher.final()用以代替degest()方法,當被調用時,cipher對象中剩餘的所有數據都會被加密並返回,但會添加足夠填充使其滿足塊大小的要求,例如:
var crypto = require('crypto'); var fs = require('fs'); var pem = fs.readFileSync('key.pem'); var key = pem.toString('ascii'); var cipher = crypto.createCipher('test', key); cipher.update(new Buffer(4), 'binary', 'hex'); cipher.update(new Buffer(4), 'binary', 'hex'); cipher.final('hex');
第一次調用cipher.update()時傳入了4個字符的數據,得到的會是一個空字符串,第二次因爲有足夠的數據來生成加密塊,可以得到十六進制格式的加密數據。如果發送的數據超過一個塊所需要的大小,cipher.final()會先返回儘可能多的加密塊,然後纔會採用補全的辦法。
Decipher類是Cipher類的反面,可以把加密的數據通過decipher.update()傳給一個Decipher對象,它會把數據以流的形式保存成塊,並在數據足夠的時候輸出解密數據,例如:
var crypto = require('crypto'); var fs = require('fs'); var pem = fs.readFileSync('key.pem'); var key = pem.toString('ascii'); var plaintext = new Buffer('test'); var encrypted = ''; var cipher = crypto.createCipher('test', $key); encrypted += cipher.update(plaintext, 'binary', 'hex'); encrypted += cipher.final('hex'); var decrypted = ''; var decipher = crypto.createDecipher('test', $key); decrypted += decipher.update(encrypted, 'hex', 'binary'); decrypted += decipher.final('binary'); var output = new Buffer(decrypted);
Signatures驗證的是簽名者是否用其私鑰對數據進行授權,Sign類的API與HMAC的幾乎一樣,crypto.createSign()用來創建sign對象,createSign()只需要傳入簽名算法,sign.update()可給sign對象添加數據,例如:
var crypto = require('crypto'); var fs = require('fs'); var pem = fs.readFileSync('key.pem'); var key = pem.toString('ascii'); var sign = crypto.createSign('RSA-SHA256'); sign.update('test'); var sig = sign.sign(key, 'hex');
Verify API使用的方法類似,它用verify.update()來添加數據,之後就可以調用verify.verify()對簽名進行驗證,例如:
var crypto = require('crypto'); var fs = require('fs'); var privatePem = fs.readFileSync('key.pem'); var publicPem = fs.readFileSync('cert.pem'); var key = privatePem.toString(); var pubkey = publicPem.toString(); var sign = crypto.createSign('RSA-SHA256'); sign.update('test'); var sig = sign.sign(key, 'hex'); var verify = crypto.createVerify('RSA-SHA256'); verify.update('test'); verify.verify(pubkey, sig, 'hex');
2. 進程
2.1 process
可以使用process模塊從當前的Node進程中獲取信息,並可以修改配置。和其他大部分模塊不同,process模塊是全局的,並且可以一直通過變量process獲得。
process提供了基於對Node進程的系統調用的事件。exit事件提供了在Node進程退出前的最終響應時機,例如:
process.on('exit', function() { setTimeout(function() { console.log('This will not run'); }, 100); console.log('Bye.'); });
process提供的一個非常有用的事件是uncaughtException,它會提供一個暴力的方法來捕獲進程退出的異常,例如:
process.on('uncaughtException', function(err) { console.log('Caught exception: ' + err); }); setTimeout(function() { console.log('This will still run'); }, 500);
我們還能利用process來訪問一些系統事件。當進程得到一個信號時,它會通過process觸發的事件通知Node程序。比如當用戶在終端的程序按下CTRL+C的時候,SIGINT就會發生,除非通過process來處理信號事件,否則Node會採取默認方法進行處理,例如:
process.on('SIGINT', function() { console.log('Got SIGINI. Press Control-D to exit.'); });
process包含了有關Node進程的許多元信息:
1) process.version: 包含了正在運行的Node的版本號。
2) process.installPrefix: 包含了安裝時指定的安裝目錄。
3) process.platform: 列出正在運行的平臺名稱。
4) process.uptime(): 列出當前進程運行了多少秒。
此外,還可以從Node進程得到或設置一些屬性。
可以調用process.getgid()、process.setgid()、process.getuid()和process.setuid()來獲得或修改進程用戶及用戶組的屬性,set方法除了可以接受用戶名/用戶組所對應的數字ID外,還可以直接使用用戶組/用戶名本身。
正在運行的Node實例的進程ID,或稱爲PID,可以通過process.pid屬性得到。此外還能修改process.title屬性來設置Node顯示在系統的標題名稱。
其他可用的信息包括process.execPath,它顯示當前執行的node程序所在的路徑。當前的工作目錄可以用process.cwd()獲取,工作目錄是Node啓動的目錄,可以調用process.chdir()來修改。還可以使用process.memoryUsage()來得到當前進程的內存使用情況。
通過process,還有若干方法可以與操作系統交互。其中一個主要功能就是可以訪問操作系統的標準I/O流,stdin是進程的默認輸入流,stdout是進程的默認輸出流,stderr是錯誤輸出流。它們對應的接口是process.stdin、process.stdout和process.stderr。
因爲任何時候都能使用process,所以process.stdin也會爲所有的Node進程初始化。但它一開始處於暫停狀態,這是Node可以對它進行寫入,但不能讀取,在嘗試從stdin讀數據前,需要先調用它的resume()方法,Node會爲此數據流填入供讀取的緩存,並等待處理,例如:
process.stdin.resume(); process.stdin.setEncoding('utf8'); process.stdin.on('data', function(data) { process.stdout.write('data: ' + data); }); process.stdin.on('end', function() { process.stdout.write('end'); });
因爲stdin和stdout都是真正的數據流,我們也可以採用更簡便的方法,使用流據流的pipe()方法,例如:
process.stdin.resume(); process.stdin.pipe(process.stdout);
stderr用來輸出異常和程序運行過程中遇到的問題。當寫入stderr時,Node將保證該次寫入的會被完成,但是,這會以堵塞的方式執行。通常,調用Stream.wirte()會返回一個布爾值,用來表示Node是否能夠寫到內核緩存中,對於process.stderr來說這個返回值永遠是真,但它需要等待一會兒。
process.stderr永遠是UTF-8編碼的數據流,不需要設置編碼格式,而且,編碼格式不能被更改。
另外,Node程序員要從操作系統讀取的內容還包括程序啓動時的參數。argv是包含命令行參數的數組,以node命令爲第一個參數,例如:
console.log(process.argv);
在Node裏,我們可以訪問事件循環,並且可以推延工作。process.nextTick()創建了一個回調函數,它會在下一個tick或者事件循環下一次迭代時被調用。因爲實現是使用隊列的,所以它會取代其他事件,例如:
var http = require('http'); var s = http.createServer(function(req, res) { res.writeHead(200, {}); res.end('test'); console.log('http response'); process.nextTick(function(){console.log('tick')}); }); s.listen(9000);
2.2 child_process
可以使用child_process模塊來爲Node主進程創建子進程。因爲Node的單進程只有一個事件循環,所以有時可能需要用子程序來更好地利用CPU的多核,或者可以用child_process來啓動其他程序,然後與其交互。
child_process有兩個主要的方法。spawn()會創建一個子進程,並且有獨立的stdin、stdout和stderr文件描述符。exec()會創建子進程,並會在進程結束時以回調函數的方式返回結果。
所有的子進程都有一些公共的屬性,它們每個都包含了stdin、stdout和stderr的牧場生,此外它們還有一個pid屬性,它包含了該子進程的OS進程ID。子進程在退出時會觸發exit事件,其他data事件可通過child_process.stdin、child_process.stdout和child_process.stderr的流方法獲得。
使用exec(),可以創建一個子進程來運行其他程序,然後在回調函數中返回執行的結果,例如:
var cp = require('child_process'); cp.exec('ls -l', function(e, stdout, stderr) { if (!e) { console.log(stdout); console.log(stderr); } });
回調函數接收3個參數:一個error對象、stdout的結果和stderr的結果。如果子進程返回了錯誤的狀態碼或有其他異常發生,error對象就不會是null。當子進程退出時,它會把狀態碼傳回給父進程。error對象會包含錯誤代碼和stderr,但是,若一個子進程運行是成功的,stderr中依然可以有數據。
exec()的第二個參數可以是一個可選的配置對象,這個對象包含了如下屬性:
1) encoding: I/O流輸入字符的編碼格式。
2) timeout: 進程運行的時間,以毫秒爲單位。
3) killSignal: 當時間或Buffer大小超過限制時,用來終止進程的信號。
4) maxBuffer: stdout或stderr允許最大的大小。
5) setsid: 是否創建Node子進程的新會話。
6) cwd: 爲子進程初始化工作目錄。
7) env: 進程的環境變量,所有的環境變量都可以從父進程繼承。
spawn()和exec()很像,但它是一個更加通用的方法,它要求你自己處理流和它們的回調函數。所以spawn()最常見的用途是用來在服務器開發中創建服務器程序的子模塊。
spawn()的API與exec()有些差異。第一個參數依然是讓進程運行的命令,但它不再是一個命令字符串,而只是可執行程序。進程的參數以數組的形式作爲第二個參數(可選)傳給spawn()。最後spawn()還可以接受一個選項數組作爲最後一個參數,配置的部分屬性與exec()相同,例如:
var cp = require('child_process'); var cat = cp.spawn('cat'); cat.stdout.on('data', function(data) { console.log(data.toString()); }); cat.on('exit', function() { console.log('bye.'); }); cat.stdin.write('meow'); cat.stdin.end();
傳給spawn()的配置內容並非和exec()完全一樣,這是因爲需要對spawn()進行更多的手工操作。evn、setsid和cwd屬性都是spawn()的可選項,還有uid和gid,分別用來設置用戶ID和組ID,這會引起短暫堵塞。spawn()還比exec()多一個配置項,可以設置自定義的文件描述符來傳給新建立的子進程。
3. 其他API
3.1 DNS
DNS模塊提供了用域名來替代IP地址的查找功能,也爲那些使用域名的模塊提供支持,如HTTP客戶端。該模塊包含了兩個主要方法:resolve()和reverse(),前者把域名轉換成DNS記錄,後者將IP地址轉換成域名。DNS模塊的其他方法都是這兩種方法的特殊形式。
dns.resolve()接受3個參數:待解析的域名、請求的記錄類型和回調函數,例如:
dns.resolve('test.com', 'A', function(e, r) { if (e) { console.log(e); } console.log(r); });
因爲resolve()通常會返回一個包含許多IP地址的列表,所以需要有dns模板提供的dns.lookup()方法,可以從一個A記錄查詢中只返回一個IP地址。該方法參數是域名、IP類型(4或6)和回調函數,例如:
var dns = require('dns'); dns.lookup('test.com', 4, function(e, a) { console.log(a); });
此外,API還提供了resolve4()和resolve6()方法,分別用來解析IPv4和IPv6地址。
3.2 assert
assert是爲測試代碼提供基礎功能的核心庫。Node的斷言功能與其他開發語言類似,允許爲對象或耿函數調用提出要求,並在破壞斷言時發出信息。Node自己的測試也是用assert編寫的。
assert的許多方法都是成對出現的,一個方法提供正面測試,另一個就提供反面功能,例如:
var assert = require('assert'); assert.equal(1, true, 'Truthy'); assert.notEqual(1, true, 'Truthy');
當assert方法不通過時,會拋出異常。
只有幾個斷言函數,如equal()和notEqual(),會檢查相等(==)和不相等(!=)操作,其他測試只會弱化地檢查真值和假值。當測試作爲一個布爾值時,假值包含了false、0、空字符串、null、undefined和NaN,所有其他值都爲真值。
strictEqual()和notstrictEqual()方法檢測兩個數值是否相等時會採用"==="和"!==",這樣可以確保測試時的true和false可分別被作爲真和假來對待。
deepEqual()和notDeepEqual()方法提供了深入比較兩個對象值的方法。這些方法會進行若干測試,而無需太多細節。如果任何一個檢查失敗了,測試就會拋出異常。它們是很有用的,但是代價可能很大,所以應該只在需要的時候才使用它們。
throws()和doesNotThrow()會檢查指定的代碼塊是否會拋出異常,可以檢測指定的異常,或者是任意的異常是否拋出。要把代碼塊傳給throws()和doesNotThrow(),需要把它們包含在一個沒有參數的函數裏。待測試的異常是可選的,如果沒有傳入,throws()會檢查是否有異常發生,而doesNotThrow()會確保不拋出異常。
3.3 虛擬機
虛擬機模塊可以運行任意一塊代碼,並得到運行結果。它提供了一些功能,可以修改指定代碼的上下文。vm和eval()類似,但提供了更多功能和更好的API來管理代碼,然而它不像eval()那樣能提供與本地作用域互動的功能。
用vm運行代碼有兩種方法,第一種與eval()類似,把代碼內嵌運行,第二種是先把代碼預編譯成vm.Script對象,例如:
var vm = require('vm'); vm.runInThisContext('1+1');
vm實際上會在每一個實例的內部,維護一套獨立的本地上下文,並且能夠保持狀態。所以如果在vm的作用域內創建了變量v,該變量就能夠在同一個vm的後續操作中有效,並且保持上一次調用時的狀態。此外,也可以傳給vm一個已經存在的上下文內容,該上下言語會作爲默認的上下文使用,例如:
var vm = require('vm'); var context = {alphabet: ''}; vm.runInNewContext{"alphabet+='a'", context}; vm.runInNewContext{"alphabet+='b'", context};
或者也可以把代碼編譯成vm.Script對象,這樣就可以重複運行同一段代碼,在運行的時候,可以選擇用哪個上下文來執行,例如:
var vm = require('vm'); var fs = require('fs'); var code = fs.readFileSync('test.js'); var script = vm.createScript(code); script.runInNewContext({'console':console,'output': 'Hello'});