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\n');
// 通過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
。