【編者按】:Node.js從2009年誕生至今,已經(jīng)發(fā)展了兩年有余,其成長(zhǎng)的速度有目共睹。從在github的訪問(wèn)量超過(guò)Rails,到去年底Node.jsS創(chuàng)始人Ryan Dalh加盟Joyent獲得企業(yè)資助,再到今年發(fā)布Windows移植版本,Node.js的前景獲得了技術(shù)社區(qū)的肯定。InfoQ一直在關(guān)注Node.js的發(fā)展,在今年的兩次Qcon大會(huì)(北京站和杭州站)都有專門(mén)的講座。為了更好地促進(jìn)Node.js在國(guó)內(nèi)的技術(shù)推廣,我們決定開(kāi)設(shè)“深入淺出Node.js”專欄,邀請(qǐng)來(lái)自Node.js領(lǐng)域的布道師、開(kāi)發(fā)人員、技術(shù)專家來(lái)講述Node.js的各方面內(nèi)容,讓讀者對(duì)Node.js有更深入的了解,并且能夠積極投入到新技術(shù)的討論和實(shí)踐中。
專欄的第一篇文章《什么是Node.js》嘗試從各個(gè)角度來(lái)闡述Node.js的基本概念、發(fā)展歷史、優(yōu)勢(shì)等,對(duì)該領(lǐng)域不熟悉的開(kāi)發(fā)人員可以通過(guò)本文了解Node.js的一些基礎(chǔ)知識(shí)。
有關(guān)Node.js的技術(shù)報(bào)道越來(lái)越多,Node.js的寫(xiě)法也是五花八門(mén),有寫(xiě)成NodeJS的,有寫(xiě)成Nodejs的,到底哪一種寫(xiě)法最標(biāo)準(zhǔn)呢,我們不妨遵循官方的說(shuō)法。在Node.js的官方網(wǎng)站上,一直將其項(xiàng)目稱之為”Node“或者”Node.js“,沒(méi)有發(fā)現(xiàn)其他的說(shuō)法,”Node“用的最多,考慮到Node這個(gè)單詞的意思和用途太廣泛,容易讓開(kāi)發(fā)人員誤解,我們采用了第二種稱呼——”Node.js“,js的后綴點(diǎn)出了Node項(xiàng)目的本意,其他的名稱五花八門(mén),沒(méi)有確切的出處,我們不推薦使用。
看到Node.js這個(gè)名字,初學(xué)者可能會(huì)誤以為這是一個(gè)Javascript應(yīng)用,事實(shí)上,Node.js采用C++語(yǔ)言編寫(xiě)而成,是一個(gè)Javascript的運(yùn)行環(huán)境。為什么采用C++語(yǔ)言呢?據(jù)Node.js創(chuàng)始人Ryan Dahl回憶,他最初希望采用Ruby來(lái)寫(xiě)Node.js,但是后來(lái)發(fā)現(xiàn)Ruby虛擬機(jī)的性能不能滿足他的要求,后來(lái)他嘗試采用V8引擎,所以選擇了C++語(yǔ)言。既然不是Javascript應(yīng)用,為何叫.js呢?因?yàn)镹ode.js是一個(gè)Javascript的運(yùn)行環(huán)境。提到Javascript,大家首先想到的是日常使用的瀏覽器,現(xiàn)代瀏覽器包含了各種組件,包括渲染引擎、Javascript引擎等,其中Javascript引擎負(fù)責(zé)解釋執(zhí)行網(wǎng)頁(yè)中的Javascript代碼。作為Web前端最重要的語(yǔ)言之一,Javascript一直是前端工程師的專利。不過(guò),Node.js是一個(gè)后端的Javascript運(yùn)行環(huán)境(支持的系統(tǒng)包括*nux、Windows),這意味著你可以編寫(xiě)系統(tǒng)級(jí)或者服務(wù)器端的Javascript代碼,交給Node.js來(lái)解釋執(zhí)行,簡(jiǎn)單的命令類(lèi)似于:
#node helloworld.js
Node.js采用了Google Chrome瀏覽器的V8引擎,性能很好,同時(shí)還提供了很多系統(tǒng)級(jí)的API,如文件操作、網(wǎng)絡(luò)編程等。瀏覽器端的Javascript代碼在運(yùn)行時(shí)會(huì)受到各種安全性的限制,對(duì)客戶系統(tǒng)的操作有限。相比之下,Node.js則是一個(gè)全面的后臺(tái)運(yùn)行時(shí),為Javascript提供了其他語(yǔ)言能夠?qū)崿F(xiàn)的許多功能。
事件驅(qū)動(dòng)這個(gè)詞并不陌生,在某些傳統(tǒng)語(yǔ)言的網(wǎng)絡(luò)編程中,我們會(huì)用到回調(diào)函數(shù),比如當(dāng)socket資源達(dá)到某種狀態(tài)時(shí),注冊(cè)的回調(diào)函數(shù)就會(huì)執(zhí)行。Node.js的設(shè)計(jì)思想中以事件驅(qū)動(dòng)為核心,它提供的絕大多數(shù)API都是基于事件的、異步的風(fēng)格。以Net模塊為例,其中的net.Socket對(duì)象就有以下事件:connect、data、end、timeout、drain、error、close等,使用Node.js的開(kāi)發(fā)人員需要根據(jù)自己的業(yè)務(wù)邏輯注冊(cè)相應(yīng)的回調(diào)函數(shù)。這些回調(diào)函數(shù)都是異步執(zhí)行的,這意味著雖然在代碼結(jié)構(gòu)中,這些函數(shù)看似是依次注冊(cè)的,但是它們并不依賴于自身出現(xiàn)的順序,而是等待相應(yīng)的事件觸發(fā)。事件驅(qū)動(dòng)、異步編程的設(shè)計(jì)(感興趣的讀者可以查閱筆者的另一篇文章《Node.js的異步編程風(fēng)格》),重要的優(yōu)勢(shì)在于,充分利用了系統(tǒng)資源,執(zhí)行代碼無(wú)須阻塞等待某種操作完成,有限的資源可以用于其他的任務(wù)。此類(lèi)設(shè)計(jì)非常適合于后端的網(wǎng)絡(luò)服務(wù)編程,Node.js的目標(biāo)也在于此。在服務(wù)器開(kāi)發(fā)中,并發(fā)的請(qǐng)求處理是個(gè)大問(wèn)題,阻塞式的函數(shù)會(huì)導(dǎo)致資源浪費(fèi)和時(shí)間延遲。通過(guò)事件注冊(cè)、異步函數(shù),開(kāi)發(fā)人員可以提高資源的利用率,性能也會(huì)改善。
從Node.js提供的支持模塊中,我們可以看到包括文件操作在內(nèi)的許多函數(shù)都是異步執(zhí)行的,這和傳統(tǒng)語(yǔ)言存在區(qū)別,而且為了方便服務(wù)器開(kāi)發(fā),Node.js的網(wǎng)絡(luò)模塊特別多,包括HTTP、DNS、NET、UDP、HTTPS、TLS等,開(kāi)發(fā)人員可以在此基礎(chǔ)上快速構(gòu)建Web服務(wù)器。以簡(jiǎn)單的helloworld.js為例:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(80, "127.0.0.1");
上面的代碼搭建了一個(gè)簡(jiǎn)單的http服務(wù)器(運(yùn)行示例部署在http://helloworld.cnodejs.net/中,讀者可以訪問(wèn)),在本地監(jiān)聽(tīng)80端口,對(duì)于任意的http請(qǐng)求,服務(wù)器都返回一個(gè)頭部狀態(tài)碼為200、Content-Type'值為text/plain'的”Hello World“文字響應(yīng)。從這個(gè)小例子中,我們可以看出幾點(diǎn):
下面我們來(lái)說(shuō)說(shuō)Node.js的特點(diǎn)。事件驅(qū)動(dòng)、異步編程的特點(diǎn)剛才已經(jīng)詳細(xì)說(shuō)過(guò)了,這里不再重復(fù)。
Node.js的性能不錯(cuò)。按照創(chuàng)始人Ryan Dahl的說(shuō)法,性能是Node.js考慮的重要因素,選擇C++和V8而不是Ruby或者其他的虛擬機(jī)也是基于性能的目的。Node.js在設(shè)計(jì)上也是比較大膽,它以單進(jìn)程、單線程模式運(yùn)行(很吃驚,對(duì)吧?這和Javascript的運(yùn)行方式一致),事件驅(qū)動(dòng)機(jī)制是Node.js通過(guò)內(nèi)部單線程高效率地維護(hù)事件循環(huán)隊(duì)列來(lái)實(shí)現(xiàn)的,沒(méi)有多線程的資源占用和上下文切換,這意味著面對(duì)大規(guī)模的http請(qǐng)求,Node.js憑借事件驅(qū)動(dòng)搞定一切,習(xí)慣了傳統(tǒng)語(yǔ)言的網(wǎng)絡(luò)服務(wù)開(kāi)發(fā)人員可能對(duì)多線程并發(fā)和協(xié)作非常熟悉,但是面對(duì)Node.js,我們需要接受和理解它的特點(diǎn)。由此我們是否可以推測(cè)出這樣的設(shè)計(jì)會(huì)導(dǎo)致負(fù)載的壓力集中在CPU(事件循環(huán)處理?)而不是內(nèi)存(還記得Java虛擬機(jī)拋出OutOfMemory異常的日子嗎?),眼見(jiàn)為實(shí),不如來(lái)看看淘寶共享數(shù)據(jù)平臺(tái)團(tuán)隊(duì)對(duì)Node.js的性能測(cè)試:
從上面的結(jié)果,我們可以看到在這樣的測(cè)試場(chǎng)景下,qps能夠達(dá)到16700次,內(nèi)存僅占用30M(其中V8堆占用22M),CPU則達(dá)到95%,可能成為瓶頸。此外,還有不少實(shí)踐者對(duì)Node.js做了性能分析,總的來(lái)說(shuō),它的性能讓人信服,也是受歡迎的重要原因。既然Node.js采用單進(jìn)程、單線程模式,那么在如今多核硬件流行的環(huán)境中,單核性能出色的Node.js如何利用多核CPU呢?創(chuàng)始人Ryan Dahl建議,運(yùn)行多個(gè)Node.js進(jìn)程,利用某些通信機(jī)制來(lái)協(xié)調(diào)各項(xiàng)任務(wù)。目前,已經(jīng)有不少第三方的Node.js多進(jìn)程支持模塊發(fā)布,專欄后面的文章會(huì)詳細(xì)講述Node.js在多核CPU下的編程。
Node.js的另一個(gè)特點(diǎn)是它支持的編程語(yǔ)言是Javascript。關(guān)于動(dòng)態(tài)語(yǔ)言和靜態(tài)語(yǔ)言的優(yōu)缺點(diǎn)比較在這里不再展開(kāi)討論。只說(shuō)三點(diǎn):
var hostRequest = http.request(requestOptions,function(response) {
var responseHTML ='';
response.on('data', function (chunk) {
responseHTML = responseHTML + chunk;
});
response.on('end',function(){
console.log(responseHTML);
// do something useful
});
});
在上面的代碼中,我們需要在end事件中處理responseHTML變量,由于Javascript的閉包特性,我們可以在兩個(gè)回調(diào)函數(shù)之外定義responseHTML變量,然后在data事件對(duì)應(yīng)的回調(diào)函數(shù)中不斷修改其值,并最終在end事件中訪問(wèn)處理。
2009年2月,Ryan Dahl在博客上宣布準(zhǔn)備基于V8創(chuàng)建一個(gè)輕量級(jí)的Web服務(wù)器并提供一套庫(kù)。
2009年5月,Ryan Dahl在GitHub上發(fā)布了最初版本的部分Node.js包,隨后幾個(gè)月里,有人開(kāi)始使用Node.js開(kāi)發(fā)應(yīng)用。
2009年11月和2010年4月,兩屆JSConf大會(huì)都安排了Node.js的講座。
2010年年底,Node.js獲得云計(jì)算服務(wù)商Joyent資助,創(chuàng)始人Ryan Dahl加入Joyent全職負(fù)責(zé)Node.js的發(fā)展。
2011年7月,Node.js在微軟的支持下發(fā)布Windows版本。
雖然Node.js誕生剛剛兩年多,但是其發(fā)展勢(shì)頭逐漸趕超Ruby/Rails,我們?cè)谶@里列舉了部分企業(yè)應(yīng)用Node.js的案例,聽(tīng)聽(tīng)來(lái)自客戶的聲音。
在社交網(wǎng)站LinkedIn最新發(fā)布的移動(dòng)應(yīng)用中,NodeJS是該移動(dòng)應(yīng)用的后臺(tái)基礎(chǔ)。LinkedIn移動(dòng)開(kāi)發(fā)主管Kiran Prasad對(duì)媒體表示,其整個(gè)移動(dòng)軟件平臺(tái)都由NodeJS構(gòu)建而成:
LinkedIn內(nèi)部使用了大量的技術(shù),但是在移動(dòng)服務(wù)器這一塊,我們完全基于Node。
(使用它的原因)第一,是因?yàn)槠潇`活性。第二,如果你了解Node,就會(huì)發(fā)現(xiàn)它最擅長(zhǎng)的事情是與其他服務(wù)通信。移動(dòng)應(yīng)用必須與我們的平臺(tái)API和數(shù)據(jù)庫(kù)交互。我們沒(méi)有做太多數(shù)據(jù)分析。相比之前采用的Ruby on Rails技術(shù),開(kāi)發(fā)團(tuán)隊(duì)發(fā)現(xiàn)Node在性能方面提高很多。他們?cè)诿颗_(tái)物理機(jī)上跑了15個(gè)虛擬服務(wù)器(15個(gè)實(shí)例),其中4個(gè)實(shí)例即可處理雙倍流量。容量評(píng)估基于負(fù)載測(cè)試的結(jié)果。
企業(yè)社會(huì)化服務(wù)網(wǎng)站Yammer則利用Node創(chuàng)建了針對(duì)其自身平臺(tái)的跨域代理服務(wù)器,第三方的開(kāi)發(fā)人員可以通過(guò)該服務(wù)器實(shí)現(xiàn)從自身域托管的Javascript代碼與Yammer平臺(tái)API的AJAX通信。Yammer平臺(tái)技術(shù)主管Jim Patterson對(duì)Node的優(yōu)點(diǎn)和缺點(diǎn)提出了自己的看法:
(優(yōu)點(diǎn))因?yàn)镹ode是基于事件驅(qū)動(dòng)和無(wú)阻塞的,所以非常適合處理并發(fā)請(qǐng)求,因此構(gòu)建在Node上的代理服務(wù)器相比其他技術(shù)實(shí)現(xiàn)(如Ruby)的服務(wù)器表現(xiàn)要好得多。此外,與Node代理服務(wù)器交互的客戶端代碼是由javascript語(yǔ)言編寫(xiě)的,因此客戶端和服務(wù)器端都用同一種語(yǔ)言編寫(xiě),這是非常美妙的事情。
(缺點(diǎn))Node是一個(gè)相對(duì)新的開(kāi)源項(xiàng)目,所以不太穩(wěn)定,它總是一直在變,而且缺少足夠多的第三方庫(kù)支持。看起來(lái),就像是Ruby/Rails當(dāng)年的樣子。
知名項(xiàng)目托管網(wǎng)站GitHub也嘗試了Node應(yīng)用。該Node應(yīng)用稱為NodeLoad,是一個(gè)存檔下載服務(wù)器(每當(dāng)你下載某個(gè)存儲(chǔ)分支的tarball或者zip文件時(shí)就會(huì)用到它)。GitHub之前的存檔下載服務(wù)器采用Ruby編寫(xiě)。在舊系統(tǒng)中,下載存檔的請(qǐng)求會(huì)創(chuàng)建一個(gè)Resque任務(wù)。該任務(wù)實(shí)際上在存檔服務(wù)器上運(yùn)行一個(gè)git archive命令,從某個(gè)文件服務(wù)器中取出數(shù)據(jù)。然后,初始的請(qǐng)求分配給你一個(gè)小型Ruby Sinatra應(yīng)用等待該任務(wù)。它其實(shí)只是在檢查memcache flag是否存在,然后再重定向到最終的下載地址上。舊系統(tǒng)運(yùn)行大約3個(gè)Sinatra實(shí)例和3個(gè)Resque worker。GitHub的開(kāi)發(fā)人員覺(jué)得這是Node應(yīng)用的好機(jī)會(huì)。Node基于事件驅(qū)動(dòng),相比Ruby的阻塞模型,Node能夠更好地處理git存檔。在編寫(xiě)新下載服務(wù)器過(guò)程中,開(kāi)發(fā)人員覺(jué)得Node非常適合該功能,此外,他們還里利用了Node庫(kù)socket.io來(lái)監(jiān)控下載狀態(tài)。
不僅在國(guó)外,Node的優(yōu)點(diǎn)也同樣吸引了國(guó)內(nèi)開(kāi)發(fā)人員的注意,淘寶就實(shí)際應(yīng)用了Node技術(shù):
MyFOX 是一個(gè)數(shù)據(jù)處理中間件,負(fù)責(zé)從一個(gè)MySQL集群中提取數(shù)據(jù)、計(jì)算并輸出統(tǒng)計(jì)結(jié)果。用戶提交一段SQL語(yǔ)句,MyFOX根據(jù)該SQL命令的語(yǔ)義,生成各個(gè)數(shù)據(jù)庫(kù)分片所需要執(zhí)行的查詢語(yǔ)句,并發(fā)送至各個(gè)分片,再將結(jié)果進(jìn)行匯總和計(jì)算。 MyFOX的特點(diǎn)是CPU密集,無(wú)文件IO,并只處理只讀數(shù)據(jù)。起初MyFOX使用PHP編寫(xiě),但遇到許多問(wèn)題。例如PHP是單線程的,MySQL又需要阻塞查詢,因此很難并發(fā)請(qǐng)求數(shù)據(jù),后來(lái)的解決方案是使用nginx和dirzzle,并基于HTTP協(xié)議實(shí)現(xiàn)接口,并通過(guò)curl_multi_get命 令進(jìn)行請(qǐng)求。不過(guò)MyFOX項(xiàng)目組最終還是決定使用Node.js來(lái)實(shí)現(xiàn)MyFOX。
選擇Node.js有許多方面的原因,比如考慮了興趣及社區(qū)發(fā)展,同時(shí)也希望可以提高并發(fā)能力,榨干CPU。例如,頻繁地打開(kāi)和關(guān)閉連接會(huì)讓大量端口處于等待狀態(tài),當(dāng)并發(fā)數(shù)量上去之后,時(shí)常會(huì)因?yàn)槎丝诓粔蛴茫ㄌ幱赥IME_WAIT狀態(tài))而導(dǎo)致連接失敗。之前往往是通過(guò)修改系統(tǒng)設(shè)置來(lái)減少等待時(shí)間以繞開(kāi)這個(gè)錯(cuò)誤,然而使用連接池便可以很好地解決這個(gè)問(wèn)題。此外,以前MyFOX會(huì)在某些緩存失效的情況下出現(xiàn)十分密集的訪問(wèn)壓力,使用 Node.js便可以共享查詢狀態(tài),讓某些請(qǐng)求“等待片刻”,以便系統(tǒng)重新填充緩存內(nèi)容。
本文簡(jiǎn)要介紹了Node.js的基本知識(shí),包括概念、特點(diǎn)、歷史、案例等等。作為一個(gè)僅僅2歲的平臺(tái),Node.js的發(fā)展勢(shì)頭有目共睹,越來(lái)越多的企業(yè)開(kāi)始關(guān)注并嘗試Node.js,前后端開(kāi)發(fā)人員應(yīng)該了解相關(guān)的內(nèi)容。
[2]?http://beakkon.com/geek/node.js/why-node.js-single-thread-event-loop-javascript
[3]?http://www.tbdata.org/archives/1285
[4]?http://www.infoq.com/interviews/node-ryan-dahl
[5]?http://www.infoq.com/cn/news/2011/08/enterprise-nodejs
[6]?http://www.infoq.com/cn/news/2010/11/nodejs-joyent
[7]?http://www.infoq.com/cn/news/2011/06/node-exe
[8]?http://nodenode.com/post/1176414531/node-js-a-short-history
[9]?http://www.infoq.com/cn/news/2011/05/nodeparty-hangzhou
【編者按】:本專欄歡迎有志于宣傳和推廣Node.js的布道師、開(kāi)發(fā)人員和技術(shù)專家投稿,有意者請(qǐng)通過(guò)郵件與本專欄主持人崔康(cuikang[at]infoq.com)聯(lián)系。
更多建議: