NodeJS簡易部落格系統(六)NodeJS入門學習(下)
一、網路程式設計
1、小試牛刀
NodeJS本來的用途是編寫高效能Web伺服器。首先在這裡重複一下官方文件裡的例子,使用NodeJS內建的http
模組簡單實現一個HTTP伺服器。
var http = require('http'); http.createServer(function (request, response) { |
以上程式建立了一個HTTP伺服器並監聽8080
埠,開啟瀏覽器訪問該埠http://127.0.0.1:8080/
就能夠看到效果。
2、常用
-
HTTP
'http'模組提供兩種使用方式: 作為服務端使用時,建立一個HTTP伺服器,監聽HTTP客戶端請求並返回響應。 作為客戶端使用時,發起一個HTTP客戶端請求,獲取服務端響應。 首先來看看服務端模式下如何工作。如小試牛刀中的例子所示,首先需要使用.createServer方法建立一個伺服器,然後呼叫.listen方法監聽埠。之後,每當來了一個客戶端請求,建立伺服器時傳入的回撥函式就被呼叫一次。可以看出,這是一種事件機制。 HTTP請求本質上是一個數據流,由請求頭(headers)和請求體(body)組成。例如以下是一個完整的HTTP請求資料內容。 POST / HTTP/1.1 Hello World http.createServer(function (request, response) { console.log(request.method); request.on('data', function (chunk) { request.on('end', function () { ------------------------------------ HTTP/1.1 200 OK Hello World http.createServer(function (request, response) { request.on('data', function (chunk) { request.on('end', function () { var options = { var request = http.request(options, function (response) {}); request.write('Hello World'); http.get('http://www.example.com/', function (response) {}); http.get('http://www.example.com/', function (response) { console.log(response.statusCode); response.on('data', function (chunk) { response.on('end', function () { ------------------------------------
|
-
HTTPS
https模組與http模組極為類似,區別在於https模組需要額外處理SSL證書。 在服務端模式下,建立一個HTTPS伺服器的示例如下。 var options = { var server = https.createServer(options, function (request, response) { 另外,NodeJS支援SNI技術,可以根據HTTPS客戶端請求使用的域名動態使用不同的證書,因此同一個HTTPS伺服器可以使用多個域名提供服務。接著上例,可以使用以下方法為HTTPS伺服器新增多組證書。 server.addContext('foo.com', { server.addContext('bar.com', { var options = { var request = https.request(options, function (response) {}); request.end(); |
-
URL
處理HTTP請求時url模組使用率超高,因為該模組允許解析URL、生成URL,以及拼接URL。首先我們來看看一個完整的URL的各組成部分。 href url.parse('http://user:[email protected]:8080/p/a/t/h?query=string#hash'); http.createServer(function (request, response) { 反過來,format方法允許將一個URL物件轉換為URL字串,示例如下。 url.format({ url.resolve('http://www.example.com/foo/bar', '../baz'); |
-
Query String
querystring模組用於實現URL引數字串與引數物件的互相轉換,示例如下。 querystring.parse('foo=bar&baz=qux&baz=quux&corge'); querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' }); |
-
Zlib
zlib模組提供了資料壓縮和解壓的功能。當我們處理HTTP請求和響應時,可能需要用到這個模組。 首先我們看一個使用zlib模組壓縮HTTP響應體資料的例子。這個例子中,判斷了客戶端是否支援gzip,並在支援的情況下使用zlib模組返回gzip之後的響應體資料。 http.createServer(function (request, response) { while (i--) { if ((request.headers['accept-encoding'] || '').indexOf('gzip') !== -1) { var options = { http.request(options, function (response) { response.on('data', function (chunk) { response.on('end', function () { if (response.headers['content-encoding'] === 'gzip') { |
-
Net
net模組可用於建立Socket伺服器或Socket客戶端。由於Socket在前端領域的使用範圍還不是很廣,這裡先不涉及到WebSocket的介紹,僅僅簡單演示一下如何從Socket層面來實現HTTP請求和響應。 首先我們來看一個使用Socket搭建一個很不嚴謹的HTTP伺服器的例子。這個HTTP伺服器不管收到啥請求,都固定返回相同的響應。 net.createServer(function (conn) { var options = { var client = net.connect(options, function () { client.on('data', function (data) { |
二、程序管理
NodeJS可以感知和控制自身程序的執行環境和狀態,也可以建立子程序並與其協同工作,這使得NodeJS可以把多個程式組合在一起共同完成某項工作,並在其中充當膠水和排程器的作用。
1、小試牛刀
nodejs利用終端命令搞定目錄拷貝,示例程式碼:
var child_process = require('child_process'); function copy(source, target, callback) { copy('a', 'b', function (err) { |
從以上程式碼中可以看到,子程序是非同步執行的,通過回撥函式返回執行結果。
2、常用
-
Process
任何一個程序都有啟動程序時使用的命令列引數,有標準輸入標準輸出,有執行許可權,有執行環境和執行狀態。在NodeJS中,可以通過process物件感知和控制NodeJS自身程序的方方面面。另外需要注意的是,process不是內建模組,而是一個全域性物件,因此在任何地方都可以直接使用。 |
-
Child Process
使用child_process模組可以建立和控制子程序。該模組提供的API中最核心的是.spawn,其餘API都是針對特定使用場景對它的進一步封裝,算是一種語法糖。 |
-
Cluster
cluster模組是對child_process模組的進一步封裝,專用於解決單程序NodeJS Web伺服器無法充分利用多核CPU的問題。使用該模組可以簡化多程序伺服器程式的開發,讓每個核上執行一個工作程序,並統一通過主程序監聽埠和分發請求。 |
-
如何獲取命令列引數
在NodeJS中可以通過process.argv獲取命令列引數。但是比較意外的是,node執行程式路徑和主模組檔案路徑固定佔據了argv[0]和argv[1]兩個位置,而第一個命令列引數從argv[2]開始。為了讓argv使用起來更加自然,可以按照以下方式處理。 function main(argv) { main(process.argv.slice(2)); |
-
如何退出程式
通常一個程式做完所有事情後就正常退出了,這時程式的退出狀態碼為0。或者一個程式執行時發生了異常後就掛了,這時程式的退出狀態碼不等於0。如果我們在程式碼中捕獲了某個異常,但是覺得程式不應該繼續執行下去,需要立即退出,並且需要把退出狀態碼設定為指定數字,比如1,就可以按照以下方式: try { |
-
如何控制輸入輸出
NodeJS程式的標準輸入流(stdin)、一個標準輸出流(stdout)、一個標準錯誤流(stderr)分別對應process.stdin、process.stdout和process.stderr,第一個是隻讀資料流,後邊兩個是隻寫資料流,對它們的操作按照對資料流的操作方式即可。例如,console.log可以按照以下方式實現。 function log() { |
-
如何降權
在Linux系統下,我們知道需要使用root許可權才能監聽1024以下埠。但是一旦完成埠監聽後,繼續讓程式執行在root許可權下存在安全隱患,因此最好能把許可權降下來。以下是這樣一個例子。 http.createServer(callback).listen(80, function () { process.setgid(gid); 如果是通過sudo獲取root許可權的,執行程式的使用者的UID和GID儲存在環境變數SUDO_UID和SUDO_GID裡邊。如果是通過chmod +s方式獲取root許可權的,執行程式的使用者的UID和GID可直接通過process.getuid和process.getgid方法獲取。 process.setuid和process.setgid方法只接受number型別的引數。 降權時必須先降GID再降UID,否則順序反過來的話就沒許可權更改程式的GID了。 |
-
如何建立子程序
以下是一個建立NodeJS子程序的例子。 var child = child_process.spawn('node', [ 'xxx.js' ]); child.stdout.on('data', function (data) { child.stderr.on('data', function (data) { child.on('close', function (code) { 另外,上例中雖然通過子程序物件的.stdout和.stderr訪問子程序的輸出,但通過options.stdio欄位的不同配置,可以將子程序的輸入輸出重定向到任何資料流上,或者讓子程序共享父程序的標準輸入輸出流,或者直接忽略子程序的輸入輸出。 |
-
程序間如何通訊
在Linux系統下,程序之間可以通過訊號互相通訊。以下是一個例子。 /* parent.js */ child.kill('SIGTERM'); /* child.js */ 另外,如果父子程序都是NodeJS程序,就可以通過IPC(程序間通訊)雙向傳遞資料。以下是一個例子。 /* parent.js */ child.on('message', function (msg) { child.send({ hello: 'hello' }); /* child.js */ |
-
如何守護子程序
守護程序一般用於監控工作程序的執行狀態,在工作程序不正常退出時重啟工作程序,保障工作程序不間斷執行。以下是一種實現方式。 /* daemon.js */ worker.on('exit', function (code) { spawn('worker.js'); |
三、非同步程式設計
NodeJS最大的賣點——事件機制和非同步IO,對開發者並不是透明的。開發者需要按非同步方式編寫程式碼才用得上這個賣點,而這一點也遭到了一些NodeJS反對者的抨擊。但不管怎樣,非同步程式設計確實是NodeJS最大的特點,沒有掌握非同步程式設計就不能說是真正學會了NodeJS。本章將介紹與非同步程式設計相關的各種知識。
1、回撥
在程式碼中,非同步程式設計的直接體現就是回撥。非同步程式設計依託於回撥來實現,但不能說使用了回撥後程序就非同步化了。我們首先可以看看以下程式碼。
function heavyCompute(n, callback) { for (i = n; i > 0; --i) { callback(count); heavyCompute(10000, function (count) { console.log('hello'); -- Console ------------------------------ |
可以看到,以上程式碼中的回撥函式仍然先於後續程式碼執行。JS本身是單執行緒執行的,不可能在一段程式碼還未結束執行時去執行別的程式碼,因此也就不存在非同步執行的概念。
但是,如果某個函式做的事情是建立一個別的執行緒或程序,並與JS主執行緒並行地做一些事情,並在事情做完後通知JS主執行緒,那情況又不一樣了。我們接著看看以下程式碼。
setTimeout(function () { console.log('hello'); -- Console ------------------------------ |
這次可以看到,回撥函式後於後續程式碼執行了。如同上邊所說,JS本身是單執行緒的,無法非同步執行,因此我們可以認為setTimeout這類JS規範之外的由執行環境提供的特殊函式做的事情是建立一個平行執行緒後立即返回,讓JS主程序可以接著執行後續程式碼,並在收到平行程序的通知後再執行回撥函式。除了setTimeout、setInterval這些常見的,這類函式還包括NodeJS提供的諸如fs.readFile之類的非同步API。
另外,我們仍然回到JS是單執行緒執行的這個事實上,這決定了JS在執行完一段程式碼之前無法執行包括回撥函式在內的別的程式碼。也就是說,即使平行執行緒完成工作了,通知JS主執行緒執行回撥函數了,回撥函式也要等到JS主執行緒空閒時才能開始執行。以下就是這麼一個例子。
function heavyCompute(n) { for (i = n; i > 0; --i) { var t = new Date(); setTimeout(function () { heavyCompute(50000); -- Console ------------------------------ |
可以看到,本來應該在1秒後被呼叫的回撥函式因為JS主執行緒忙於執行其它程式碼,實際執行時間被大幅延遲。
2、程式碼設計模式
-
函式返回值
使用一個函式的輸出作為另一個函式的輸入是很常見的需求,在同步方式下一般按以下方式編寫程式碼: var output = fn1(fn2('input')); fn2('input', function (output2) { |
-
遍歷陣列
在遍歷陣列時,使用某個函式依次對資料成員做一些處理也是常見的需求。如果函式是同步執行的,一般就會寫出以下程式碼: var len = arr.length, for (; i < len; ++i) { // All array items have processed. (function next(i, len, callback) { 如果陣列成員可以並行處理,但後續程式碼仍然需要所有陣列成員處理完畢後才能執行的話,則非同步程式碼會調整成以下形式: (function (i, len, count, callback) { |
-
異常處理
JS自身提供的異常捕獲和處理機制——try..catch..,只能用於同步執行的程式碼。以下是一個例子。 function sync(fn) { try { -- Console ------------------------------ function async(fn, callback) { try { -- Console ------------------------------ function async(fn, callback) { async(null, function (err, data) { -- Console ------------------------------ 有了異常處理方式後,我們接著可以想一想一般我們是怎麼寫程式碼的。基本上,我們的程式碼都是做一些事情,然後呼叫一個函式,然後再做一些事情,然後再呼叫一個函式,如此迴圈。如果我們寫的是同步程式碼,只需要在程式碼入口點寫一個try語句就能捕獲所有冒泡上來的異常,示例如下。 function main() { try { function main(callback) { main(function (err) { |
3、域(Domain)
NodeJS提供了domain模組,可以簡化非同步程式碼的異常處理。在介紹該模組之前,我們需要首先理解“域”的概念。簡單的講,一個域就是一個JS執行環境,在一個執行環境中,如果一個異常沒有被捕獲,將作為一個全域性異常被丟擲。NodeJS通過process物件提供了捕獲全域性異常的方法,示例程式碼如下
process.on('uncaughtException', function (err) { setTimeout(function (fn) { -- Console ------------------------------ |
雖然全域性異常有個地方可以捕獲了,但是對於大多數異常,我們希望儘早捕獲,並根據結果決定程式碼的執行路徑。我們用以下HTTP伺服器程式碼作為例子:
function async(request, callback) { http.createServer(function (request, response) { |
以上程式碼將請求物件交給非同步函式處理後,再根據處理結果返回響應。這裡採用了使用回撥函式傳遞異常的方案,因此async函式內部如果再多幾個非同步函式呼叫的話,程式碼就變成上邊這副鬼樣子了。為了讓程式碼好看點,我們可以在每處理一個請求時,使用domain模組建立一個子域(JS子執行環境)。在子域內執行的程式碼可以隨意丟擲異常,而這些異常可以通過子域物件的error事件統一捕獲。於是以上程式碼可以做如下改造:
function async(request, callback) { http.createServer(function (request, response) { d.on('error', function () { d.run(function () { |
可以看到,我們使用.create方法建立了一個子域物件,並通過.run方法進入需要在子域中執行的程式碼的入口點。而位於子域中的非同步函式回撥函式由於不再需要捕獲異常,程式碼一下子瘦身很多。
注意
無論是通過process物件的uncaughtException事件捕獲到全域性異常,還是通過子域物件的error事件捕獲到了子域異常,在NodeJS官方文件裡都強烈建議處理完異常後立即重啟程式,而不是讓程式繼續執行。按照官方文件的說法,發生異常後的程式處於一個不確定的執行狀態,如果不立即退出的話,程式可能會發生嚴重記憶體洩漏,也可能表現得很奇怪。
但這裡需要澄清一些事實。JS本身的throw..try..catch異常處理機制並不會導致記憶體洩漏,也不會讓程式的執行結果出乎意料,但NodeJS並不是存粹的JS。NodeJS裡大量的API內部是用C/C++實現的,因此NodeJS程式的執行過程中,程式碼執行路徑穿梭於JS引擎內部和外部,而JS的異常丟擲機制可能會打斷正常的程式碼執行流程,導致C/C++部分的程式碼表現異常,進而導致記憶體洩漏等問題。
因此,使用uncaughtException或domain捕獲異常,程式碼執行路徑裡涉及到了C/C++部分的程式碼時,如果不能確定是否會導致記憶體洩漏等問題,最好在處理完異常後重啟程式比較妥當。而使用try語句捕獲異常時一般捕獲到的都是JS本身的異常,不用擔心上訴問題。
參考原文:http://nqdeng.github.io/7-days-nodejs/#4.2.2,尊重原創,感謝原文作者