什么是NodeJS
Node.js采用模塊化結構,按照CommonJS規范定義和使用模塊。模塊與文件是一一對應關系,即加載一個模塊,實際上就是加載對應的一個模塊文件。
JS是腳本語言,腳本語言都需要一個解析器才能運行。對於寫在HTML頁面里的JS,瀏覽器充當了解析器的角色。而對於需要獨立運行的JS,NodeJS就是一個解析器。
每一種解析器都是一個運行環境,不但允許JS定義各種數據結構,進行各種計算,還允許JS使用運行環境提供的內置對象和方法做一些事情。例如運行在瀏覽器中的JS的用途是操作DOM,瀏覽器就提供了document
之類的內置對象。而運行在NodeJS中的JS的用途是操作磁盤文件或搭建HTTP服務器,NodeJS就相應提供了fs
、http
等內置對象。
Node.js 被設計用來開發大規模高並發的網絡應用,這種網絡應用的瓶頸之一是在 I/O 的處理效率上。由於硬件及網絡的限制,I/O 的速度往往是固定的,如何在此前提下盡可能處理更多的客戶請求,提高 CPU 使用效率,便成了開發人員面臨的最大問題。得益於基於事件驅動的編程模型,Node.js 使用單一的 Event loop 線程處理客戶請求,將 I/O 操作分派至各異步處理模塊(這里一般人不理解,node.js包含很多模塊,這些模塊可以使用js直接調用系統的api),既解決了單線程模式下 I/O 阻塞的問題,又避免了多線程模式下資源分配及搶占的問題。
單線程模式:
客戶端發起一個 I/O 請求(數據庫查詢),然后等待服務器端返回 I/O 結果,結果返回后再對其進行操作,但這種請求常常需要很長時間(對於服務器的 CPU 處理能力來說)。這一過程中,服務器無法接受新的請求,即阻塞式 I/O。這種處理方式雖然簡單,卻不實用,尤其是面對大量請求的時候,簡直就不可用。這種情景類似在火車站售票窗口排隊買票,如果您在春節期間去北京火車站排隊買過票,絕不會認為這是一種好的處理方式。慶幸的是,現在很少有服務器采取這種處理方式。
多線程模式:
該方式下,服務器為每個請求分配一個線程,所有任務均在自己的線程內執行,就像火車站多開了幾個賣票窗口,處理效率高了許多。但就如讀者看到的那樣,在春節期間各個售票窗口前還是人滿為患,為什么火車站不再多開一些售票窗口呢?當然是因為成本。線程也一樣,服務器每創建一個線程,每個線程大概會占用 2M 的系統內存,而且線程之間的切換也會降低服務器的處理效率,基於成本的考慮,這種處理方式也有一定的局限性。然而,這卻不是最主要的,主要的是開發多線程程序非常困難,容易出錯。程序員需考慮死鎖,數據不一致等問題,多線程的程序極難調試和測試。基本上在程序運行出錯的時候,程序員才知道自己的程序有錯誤。而這種錯誤的代價往往又是巨大的,那些訪問量巨大的電子商務網站時常會曝出價格錯誤等導致公司損失的新聞。
事件驅動:
客戶發起 I/O 請求的同時傳入一個函數,該函數會在 I/O 結果返回后被自動調用,而且該請求不會阻塞后續操作。就像電話訂票,設想你一大早來到辦公室,給火車站打個電話,將自己的票務信息,地址告訴對方,然后放下電話,泡杯茶,瀏覽一下網頁,回復一下今天的電子郵件,你完全不用管火車票的事了,如果訂到票,火車站會派快遞公司按你電話中提到的聯系方式送票給你。無疑,這是一種極其理想的處理方式。所有請求以及同時傳入的回調函數均發送至同一線程,該線程通常叫做 Event loop 線程,該線程負責在 I/O 執行完畢后,將結果返回給回調函數。這里要注意的是 I/O 操作本身並不在該線程內執行,所以不會阻塞后續請求。比如:請求a要訪問數據庫,請求b要訪問文件系統,假設Event loop先接受到a請求,這時Event loop會把a的回調方法交給處理訪問數據庫的異步處理模塊。然后Event loop就可以去接受請求b,並把b的回調方法交給處理文件系統的一部處理模塊。然后Event loop繼續等待請求。當訪問數據的異步處理模塊處理完成后,會主動調用a的回調方法。在a的回調方法中,就會給客戶a發送查詢到的數據(當然這里需要短暫的使用Event loop來操作)。
為什么選用 JavaScript
事實上,在實現 Node.js 之初,作者 Ryan Dahl 並沒有選擇 JavaScript,他嘗試過 C、Lua,皆因其欠缺一些高級語言的特性,如閉包、函數式編程,致使程序復雜,難以維護。而 JavaScript 則是支持函數式編程范型的語言,很好地契合了 Node.js 基於事件驅動的編程模型。加之 Google 提供的 V8 引擎,使 JavaScript 語言的執行速度大大提高。最終呈現在我們面前的就成了 Node.js,而不是 Node.c,Node.lua 或其他語言的實現。Javascript的匿名函數和閉包特性非常適合事件驅動、異步編程。Javascript在動態語言中性能較好,有開發人員對Javacript、Python、Ruby等動態語言做了性能分析,發現Javascript的性能要好於其他語言,再加上V8引擎也是同類的佼佼者,所以Node.js的性能也受益其中。
Node.js采用C++語言編寫而成,是一個Javascript的運行環境。為什么采用C++語言呢?據Node.js創始人Ryan Dahl回憶,他最初希望采用Ruby來寫Node.js,但是后來發現Ruby虛擬機的性能不能滿足他的要求,后來他嘗試采用V8引擎,所以選擇了C++語言。
Node.js采用了Google Chrome瀏覽器的V8引擎,性能很好,同時還提供了很多系統級的API,如文件操作、網絡編程等。
Node.js是一個后端的Javascript運行環境(支持的系統包括*nux、Windows),這意味着你可以編寫系統級或者服務器端的Javascript代碼,交給Node.js來解釋執行
多核處理器情況下
NodeJS中的JavaScript確實是在單線程上執行,但是作為宿主的NodeJS,它本身並非是單線程的,NodeJS在I/O方面又動用到一小部分額外的線程協助實現異步。程序員沒有機會直接創建線程,這也是有的同學想當然的認為NodeJS的單線程無法很好的利用多核CPU的原因,他們甚至會說,難以想象由多人一起協作開發一個單線程的程序。
NodeJS封裝了內部的異步實現后,導致程序員無法直接操作線程,也就造成所有的業務邏輯運算都會丟到JavaScript的執行線程上,這也就意味着,在高並發請求的時候,I/O的問題是很好的解決了,但是所有的業務邏輯運算積少成多地都運行在JavaScript線程上,形成了一條擁擠的JavaScript運算線程。NodeJS的弱點在這個時候會暴露出來,單線程執行運算形成的瓶頸,拖慢了I/O的效率。這大概可以算得上是密集運算情況下無法很好利用多核CPU的缺點。這條擁擠的JavaScript線程,給I/O形成了性能上限。
但是,事情又並非絕對的。回到前端瀏覽器中,為了解決線程擁擠的情況,Web Worker應運而生。而同樣,Node也提供了child_process.fork來創建Node的子進程。在一個Node進程就能很好的解決密集I/O的情況下,fork出來的其余Node子進程可以當作常駐服務來解決運算阻塞的問題(將運算分發到多個Node子進程中上去,與Apache創建多個子進程類似)。當然child_process/Web Worker的機制永遠只能解決單台機器的問題,大的Web應用是不可能一台服務器就能完成所有的請求服務的。拜NodeJS在I/O上的優勢,跨OS的多Node之間通信的是不算什么問題的。解決NodeJS的運算密集問題的答案其實也是非常簡單的,就是將運算分發到多個CPU上。
在文章的寫作中,Node最新發布的0.5.10版本新增了cluster啟動參數。參數的使用方式如下:
node cluster server.js
啟動Node的時候,在附加了該參數的情況下,Node會檢測機器上的CPU數量來決定啟動多少進程實例,這些實例會自動共享相同的偵聽端口。
模塊
編寫稍大一點的程序時一般都會將代碼模塊化。在NodeJS中,一般將代碼合理拆分到不同的JS文件中,每一個文件就是一個模塊,而文件路徑就是模塊名。
在編寫每個模塊時,都有require
、exports
、module
三個預先定義好的變量可供使用。
require
require
函數用於在當前模塊中加載和使用別的模塊,傳入一個模塊名,返回一個模塊導出對象。模塊名可使用相對路徑(以./
開頭),或者是絕對路徑(以/
或C:
之類的盤符開頭)。
exports
exports
對象是當前模塊的導出對象,用於導出模塊公有方法和屬性。別的模塊通過require
函數使用當前模塊時得到的就是當前模塊的exports
對象。
module
通過module
對象可以訪問到當前模塊的一些相關信息,但最多的用途是替換當前模塊的導出對象。例如模塊導出對象默認是一個普通對象,如果想改成一個函數的話,可以使用以下方式。
|
module.exports = function () {
console.log( 'Hello World!' );
};
|
以上代碼中,模塊默認導出對象被替換為一個函數。
模塊初始化
一個模塊中的JS代碼僅在模塊第一次被使用時執行一次,並在執行過程中初始化模塊的導出對象。之后,緩存起來的導出對象被重復利用。
主模塊
通過命令行參數傳遞給NodeJS以啟動程序的模塊被稱為主模塊。主模塊負責調度組成整個程序的其它模塊完成工作。例如通過以下命令啟動程序時,main.js
就是主模塊。
|
$ node main.js
|
二進制模塊
雖然一般我們使用JS編寫模塊,但NodeJS也支持使用C/C++編寫二進制模塊。編譯好的二進制模塊除了文件擴展名是.node
外,和JS模塊的使用方式相同。雖然二進制模塊能使用操作系統提供的所有功能,擁有無限的潛能,但對於前端同學而言編寫過於困難,並且難以跨平台使用。
面試官一般會問:node.js是什么? node.js相對於傳統服務器的好處?為什么會有這些好處? 為什么選js為它的開發代碼?
根據上面的內容,在自己寫一個小例子,就基本上可以應付了。比如:我在服務器上安裝了node.js,然后寫一個js代碼,保存,這個js代碼,需要你require http模塊,然后新建http服務,然后監聽端口,當然后模擬http請求。同時,了解下node.js的常用模塊,比如:child_process,url等模塊的作用。
加油!