1 從名字說起
有關Node.js的技術報導越來越多,Node.js的寫法也是五花八門,有寫成 NodeJS的,有寫成Nodejs的,到底哪一種寫法最標準呢,我們不妨遵循官方的說法。在Node.js的官方網站上,一直將其專案稱之為”Node“或者”Node.js“
, 沒有發現其他的說法,”Node“用的最多,考慮到Node這個單詞的意思和用途太廣泛,容易讓開發人員誤解,我們採用了第二種稱呼——”Node.js“,js的字尾點出了Node專案的本意
,其他的名稱五花八門,沒有確切的出處,我們不推薦使用。
2 Node旨在解決什麼問題
Node公開宣稱的目標是 “旨在提供一種簡單的構建可伸縮網路程式的方法”
。當前的伺服器程式有什麼問題?我們來做個數學題。在 Java™ 和 PHP 這類語言中,每個連線都會生成一個新執行緒,每個新執行緒可能需要 2 MB 的配套記憶體。在一個擁有 8 GB RAM 的系統上,理論上最大的併發連線數量是 4,000 個使用者。隨著您的客戶群的增長,如果希望您的 Web 應用程式支援更多使用者,那麼,您必須新增更多伺服器。當然,這會增加伺服器成本、流量成本和人工成本等成本。除這些成本上升外,還有一個潛在技術問題,即使用者可能針對每個請求使用不同的伺服器,因此,任何共享資源都必須在所有伺服器之間共享。 鑑於上述所有原因,整個 Web 應用程式架構(包括流量、處理器速度和記憶體速度)中的瓶頸是:伺服器能夠處理的併發連線的最大數量
。
Node 解決這個問題的方法是:更改連線到伺服器的方式
。每個連線發射一個在 Node 引擎的程式中執行的事件,而不是為每個連線生成一個新的 OS 執行緒(併為其分配一些配套記憶體)。Node 聲稱它絕不會死鎖,因為它根本不允許使用鎖,它不會直接阻塞 I/O 呼叫
。Node 還宣稱,執行它的伺服器能支援數萬個併發連線。
3 Node.js不是JS應用、而是JS執行平臺
看到Node.js這個名字,初學者可能會誤以為這是一個Javascript應用, 事實上,Node.js採用C++語言編寫而成,是一個Javascript的執行環境
。為什麼採用C++語言呢?據Node.js創始人Ryan Dahl回憶,他最初希望採用Ruby來寫 Node.js,但是後來發現Ruby虛擬機器的效能不能滿足他的要求,後來他嘗試 採用V8引擎,所以選擇了C++語言
。既然不是Javascript應用,為何叫.js呢? 因為 Node.js是一個Javascript的執行環境
。提到Javascript,大家首先想到的是日常使用的瀏覽器,現代瀏覽器包含了各種元件,包括渲染引擎、Javascript引擎 等,其中Javascript引擎負責解釋執行網頁中的Javascript程式碼。作為Web前端最重要的語言之一,Javascript一直是前端工程師的專利。不過, Node.js是一個後端的Javascript執行環境(支援的系統包括Linux、Windows)
,這意味著你可以 編寫系統級或者伺服器端的Javascript程式碼,交給Node.js來解釋執行
,簡單的命令類似於:
#node helloworld.js
複製程式碼
Node.js採用了Google Chrome瀏覽器的V8引擎,效能很好, 同時還提供了很多系統級的API
,如檔案操作、網路程式設計等。瀏覽器端的Javascript程式碼在執行時會受到各種安全性的限制,對客戶系統的操作有限
。相比之下, Node.js則是一個全面的後臺執行時,為Javascript提供了其他語言能夠實現的許多功能
。
4 Node.js採用事件驅動、非同步程式設計, 為網路服務而設計
事件驅動這個詞並不陌生,在某些傳統語言的網路程式設計中,我們會用到回撥函式,比如當socket資源達到某種狀態時,註冊的回撥函式就會執行。Node.js的設計思想中以事件驅動為核心,它提供的絕大多數API都是基於事件的、非同步的風格
。以Net模組為例,其中的net.Socket物件就有以下事件:connect、data、 end、timeout、drain、error、close等,使用Node.js的開發人員需要根據自己的業務邏輯註冊相應的回撥函式。這些回撥函式都是非同步執行的
,這意味著雖然在程式碼結構中,這些函式看似是依次註冊的,但是 它們並不依賴於自身出現的順序,而是等待相應的事件觸發
。事件驅動、非同步程式設計的設計,重要的優勢在於,充分利用了系統資源,執行程式碼無須阻塞等待某種操作完成,有限的資源可以用於其他的任務
。此類設計非常適合於 後端的網路服務程式設計,Node.js的目標也在於此
。在伺服器開發中,併發的請求處理是個大問題,阻塞式的函式會導致資源浪費和時間延遲。通過事件註冊、非同步函式,開發人員可以提高資源的利用率,效能也會改善。
從Node.js提供的支援模組中,我們可以 看到包括檔案操作在內的許多函式都是非同步執行的
,這和傳統語言存在區別,而且為了方便伺服器開發,Node.js的網路模組特別多,包括HTTP、DNS、NET、UDP、HTTPS、TLS等,開發人員可以在此基礎上快速構建Web伺服器。以簡單的helloworld.js為例:
// 全域性方法require()是用來匯入模組的,一般直接把require()方法的返回值賦值給一個變數,在JavaScript程式碼中直接使用此變數即可。require("http")就是載入系統預置的http模組。
var http = require(`http`);
// http.createServer是模組的方法,目的就是建立並返回一個新的web server物件,並且給服務繫結一個回撥,用以處理請求。
http.createServer(function (req, res) {
// 使用response.writeHead()函式傳送一個HTTP狀態200和HTTP頭的內容型別(content-type)
// 使用response.write()函式在HTTP相應主體中傳送文字“Hello World"
res.writeHead(200, {`Content-Type`: `text/plain`});
// 完成響應
res.end(`Hello World
`);
// 通過http.listen()方法就可以讓該HTTP伺服器在特定埠監聽。
}).listen(80, "127.0.0.1");
// console.log就不用多說了,瞭解firebug的都應該知道,Node實現了這個方法。
console.log(`Server running at http://127.0.0.1:80/`);
複製程式碼
上面的程式碼搭建了一個簡單的http伺服器(執行示例部署 在http://127.0.0.1中可以訪問),在本地監聽80埠,對於任意的http請求,伺服器都返回一個頭部狀態碼為200、Content-Type值為`text/plain`的”Hello World”文字響應。從這個小例子中,我們可以看出幾點:
- Node.js的網路程式設計比較便利,提供的模組(在這裡是http)開放了容易上手的API介面,短短几行程式碼就可以構建伺服器。
- 體現了事件驅動、非同步程式設計,在createServer函式的引數中指定了一個回撥函式(採用Javascript的匿名函式實現),當有http請求傳送過來時,Node.js就會呼叫該回撥函式來處理請求並響應。當然,這個例子相對簡單,沒有太多的事件註冊,在以後的文章中讀者會看到更多的實際例子。
當我們使用 http.createServer 方法的時候,我們當然不只是想要一個偵聽某個埠的伺服器,我們還想要它在伺服器收到一個HTTP請求的時候做點什麼。問題是,這是非同步的:請求任何時候都可能到達,但是我們的伺服器卻跑在一個單程式中
。我們建立了伺服器,並且向建立它的方法傳遞了一個函式。無論何時我們的伺服器收到一個請求,這個函式就會被呼叫
。
為什麼這種事件驅動對 Node 很理想?JavaScript 是一種很棒的事件驅動程式語言
,因為它允許使用匿名函式和閉包,更重要的是,任何寫過程式碼的人都熟悉它的語法。事件發生時呼叫的回撥函式可以在捕獲事件處進行編寫。這樣可以使程式碼容易編寫和維護,沒有複雜的物件導向框架,沒有介面,沒有過度設計的可能性。只需監聽事件,編寫一個回撥函式,其他事情都可以交給系統處理!
5 Node.js的特點
下面我們來說說Node.js的特點。事件驅動、非同步程式設計的特點剛才已經詳細說過了,這裡不再重複。
Node.js的效能不錯
。按照創始人Ryan Dahl的說法,效能是Node.js考慮的重要因素, 選擇C++和V8而不是Ruby或者其他的虛擬機器也是基於效能的目的
。Node.js在設計上也是比較大膽, 它以單程式、單執行緒模式執行(很吃驚,對吧?這和Javascript的執行方式一致),事件驅動機制是Node.js通過內部單執行緒高效率地維護事件迴圈佇列來實現的,沒有多執行緒的資源佔用和上下文切換,這意味著面對大規模的http請求,Node.js憑藉事件驅動搞定一切
,習慣了傳統語言的網路服務開發人員可能對多執行緒併發和協作非常熟悉,但是面對 Node.js,我們需要接受和理解它的特點。由此我們是否可以推測出這樣的設計會 導致負載的壓力集中在CPU(事件迴圈處理?)而不是記憶體(還記得Java虛擬機器丟擲OutOfMemory異常的日子嗎?)
, 眼見為實,不如來看看淘寶共享資料平臺團隊對Node.js的效能測試:
- 物理機配置:RHEL 5.2、CPU 2.2GHz、記憶體4G
- Node.js應用場景:MemCache代理,每次取100位元組資料
- 連線池大小:50
- 併發使用者數:100
- 測試結果(socket模式):記憶體(30M)、QPS(16700)、 CPU(95%)
從上面的結果,我們可以看到在這樣的測試場景下,qps能夠達到16700次,記憶體僅佔用30M(其中V8堆佔用22M),CPU則達到95%,可能成為瓶頸。此外,還有不少實踐者對Node.js做了效能分析,總的來說,它的效能讓人信服, 也是受歡迎的重要原因。既然Node.js採用單程式、單執行緒模式,那麼在如今多核硬體流行的環境中,單核效能出色的Node.js如何利用多核CPU呢?創始人Ryan Dahl建議,執行多個Node.js程式,利用某些通訊機制來協調各項任務
。目前,已經有不少第三方的Node.js多程式支援模組釋出,後面的文章會詳細講述Node.js在多核CPU下的程式設計。
Node.js的另一個特點是它支援的程式語言是Javascript。關於動態語言和靜態語言的優缺點比較在這裡不再展開討論。只說三點:
- Javascript作為前端工程師的主力語言,在技術社群中有相當的號召力。而且,隨著Web技術的不斷髮展,特別是前端的重要性增加,不少前端工程師開始試水”後臺應用“,在許多采用Node.js的企業中,工程師都表示因為習慣了Javascript,所以選擇Node.js。
- Javascript的匿名函式和閉包特性非常適合事件驅動、非同步程式設計, 從helloworld例子中我們可以看到回撥函式採用了匿名函式的形式來實現,很方便。閉包的作用則更大,看下面的程式碼示例:
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的閉包特性,我們可以在兩個回撥函式之外定義responseHTML變數
,然後在data事件對應的回撥函式中不斷修改其值,並最終在end事件中訪問處理。
- Javascript在動態語言中效能較好, 有開發人員對Javacript、Python、Ruby等動態語言做了效能分析,發現Javascript的效能要好於其他語言, 再加上V8引擎也是同類的佼佼者,所以Node.js的效能也受益其中。選擇Node.js有許多方面的原因,比如考慮了興趣及社群發展,
同時也希望可以提高併發能力,榨乾CPU
。