從開始學習node到現在已經有半年多了,中間沒有做過什麼實際工作中的項目,所以感覺自己的知識有些匱乏,但是我還是要寫這些文章,因為工作中的需要用node來開發後臺環境,再加上我對這些知識記得不多,都是來看以前寫的源碼抄過來,自己根本記不住一些繁瑣的代碼,想藉此機會來鞏固一下我所學到的東西,等以後慢慢 ...
從開始學習node到現在已經有半年多了,中間沒有做過什麼實際工作中的項目,所以感覺自己的知識有些匱乏,但是我還是要寫這些文章,因為工作中的需要用node來開發後臺環境,再加上我對這些知識記得不多,都是來看以前寫的源碼抄過來,自己根本記不住一些繁瑣的代碼,想藉此機會來鞏固一下我所學到的東西,等以後慢慢來補充我現在所寫的文章。
可以去百度node.js中文網,上面所寫的第一句話就是這麼一句概括了node.js所有的話吧。
node.js是一個基於Chorme V8引擎的javascript運行環境。node.js使用了一個事件驅動,非阻塞式I/O模型,使其輕量又高效。
什麼是Chorme V8引擎,它是怎麼來的?此處直達廖雪峰大佬的文章
Node.js是目前非常火熱的技術,但是它的誕生經歷卻很奇特。
眾所周知,在Netscape設計出JavaScript後的短短幾個月,JavaScript事實上已經是前端開發的唯一標準。
後來,微軟通過IE擊敗了Netscape後一統桌面,結果幾年時間,瀏覽器毫無進步。(2001年推出的古老的IE 6到今天仍然有人在使用!)
沒有競爭就沒有發展。微軟認為IE6瀏覽器已經非常完善,幾乎沒有可改進之處,然後解散了IE6開發團隊!而Google卻認為支持現代Web應用的新一代瀏覽器才剛剛起步,尤其是瀏覽器負責運行JavaScript的引擎性能還可提升10倍。
先是Mozilla藉助已壯烈犧牲的Netscape遺產在2002年推出了Firefox瀏覽器,緊接著Apple於2003年在開源的KHTML瀏覽器的基礎上推出了WebKit內核的Safari瀏覽器,不過僅限於Mac平臺。
隨後,Google也開始創建自家的瀏覽器。他們也看中了WebKit內核,於是基於WebKit內核推出了Chrome瀏覽器。
Chrome瀏覽器是跨Windows和Mac平臺的,並且,Google認為要運行現代Web應用,瀏覽器必須有一個性能非常強勁的JavaScript引擎,於是Google自己開發了一個高性能JavaScript引擎,名字叫V8,以BSD許可證開源。
現代瀏覽器大戰讓微軟的IE瀏覽器遠遠地落後了,因為他們解散了最有經驗、戰鬥力最強的瀏覽器團隊!回過頭再追趕卻發現,支持HTML5的WebKit已經成為手機端的標準了,IE瀏覽器從此與主流移動端設備絕緣。
瀏覽器大戰和Node有何關係?
話說有個叫Ryan Dahl的歪果仁,他的工作是用C/C++寫高性能Web服務。對於高性能,非同步IO、事件驅動是基本原則,但是用C/C++寫就太痛苦了。於是這位仁兄開始設想用高級語言開發Web服務。他評估了很多種高級語言,發現很多語言雖然同時提供了同步IO和非同步IO,但是開發人員一旦用了同步IO,他們就再也懶得寫非同步IO了,所以,最終,Ryan瞄向了JavaScript。
因為JavaScript是單線程執行,根本不能進行同步IO操作,所以,JavaScript的這一“缺陷”導致了它只能使用非同步IO。
選定了開發語言,還要有運行時引擎。這位仁兄曾考慮過自己寫一個,不過明智地放棄了,因為V8就是開源的JavaScript引擎。讓Google投資去優化V8,咱只負責改造一下拿來用,還不用付錢,這個買賣很划算。
於是在2009年,Ryan正式推出了基於JavaScript語言和V8引擎的開源Web伺服器項目,命名為Node.js。雖然名字很土,但是,Node第一次把JavaScript帶入到後端伺服器開發,加上世界上已經有無數的JavaScript開發人員,所以Node一下子就火了起來。
在Node上運行的JavaScript相比其他後端開發語言有何優勢?
最大的優勢是藉助JavaScript天生的事件驅動機制加V8高性能引擎,使編寫高性能Web服務輕而易舉。
其次,JavaScript語言本身是完善的函數式語言,在前端開發時,開發人員往往寫得比較隨意,讓人感覺JavaScript就是個“玩具語言”。但是,在Node環境下,通過模塊化的JavaScript代碼,加上函數式編程,並且無需考慮瀏覽器相容性問題,直接使用最新的ECMAScript 6標準,可以完全滿足工程上的需求。
node是啥?
傳統意義上的javascript運行在瀏覽器上,這是因為瀏覽器內核實際上分為兩個部分,渲染引擎和javaScript引擎。前者主要負責渲染HTML+CSS,後者主要負責運行javaScript。Chorme使用的javascript引擎是V8,它的運行速度非常快。
node 跟 chorme 有什麼區別?
架構一樣,都是基於事件驅動的非同步架構!
瀏覽器主要是通過事件驅動來服務頁面交互。
node 主要是通過事件驅動來服務 I/O
node 沒有HTML,WebKit和顯卡等等的UI技術支持
為什麼要用node.js?
總的來說,Node.js 適合以下場景:
- 實時性應用,比如線上多人協作工具,網頁聊天應用等。
- 以 I/O 為主的高併發應用,比如為客戶端提供 API,讀取資料庫。
- 流式應用,比如客戶端經常上傳文件。
- 前後端分離。
實際上前兩者可以歸結為一種,即客戶端廣泛使用長連接,雖然併發數較高,但其中大部分是空閑連接。
Node.js 也有它的局限性,它並不適合 CPU 密集型的任務,比如人工智慧方面的計算,視頻、圖片的處理等。
當然,以上缺點不是信口開河,或者死記硬背,更不是人云亦云,需要我們對 Node.js 的原理有一定的瞭解,才能做出正確的判斷。
基礎概念
併發 下麵的這些是參考的他人的文章
與客戶端不同,服務端開發者非常關心的一項數據是併發數,也就是這台伺服器最多能支持多少個客戶端的併發請求。早年的 C10K 問題就是討論如何利用單台伺服器支持 10K 併發數。當然隨著軟硬體性能的提高,目前 C10K 已經不再是問題,我們開始嘗試解決 C10M 問題,即單台伺服器如何處理百萬級的併發。
在 C10K 提出時,我們還在使用 Apache 伺服器,它的工作原理是每當有一個網路請求到達,就 fork 出一個子進程併在子進程中運行 PHP 腳本。執行完腳本後再把結果發回客戶端。
這樣可以確保不同進程之間互不幹擾,即使一個進程出問題也不影響整個伺服器,但是缺點也很明顯:進程是一個比較重的概念,擁有自己的堆和棧,占用記憶體較多,一臺伺服器能運行的進程數量有上限,大約也就在幾千左右。
雖然 Apache 後來使用了 FastCGI,但本質上只是一個進程池,它減少了創建進程的開銷,但無法有效提高併發數。
Java 的 Servlet 使用了線程池,即每個 Servlet 運行在一個線程上。線程雖然比進程輕量,但也是相對的。 有人測試過 ,每個線程獨享的棧的大小是 1M,依然不夠高效。除此以外,多線程編程會帶來各種麻煩,這一點想必程式員們都深有體會。
如果不使用線程,還有兩種解決方案,分別是使用協程(coroutine)和非阻塞 I/O。協程比線程更加輕量,多個協程可以運行在同一個線程中,並由程式員自己負責調度,這種技術在 Go 語言中被廣泛使用。而非阻塞 I/O 則被 Node.js 用來處理高併發的場景。
非阻塞 I/O
這裡所說的 I/O 可以分為兩種: 網路 I/O 和文件 I/O,實際上兩者高度類似。 I/O 可以分為兩個步驟,首先把文件(網路)中的內容拷貝到緩衝區,這個緩衝區位於操作系統獨占的記憶體區域中。隨後再把緩衝區中的內容拷貝到用戶程式的記憶體區域中。
對於阻塞 I/O 來說,從發起讀請求,到緩衝區就緒,再到用戶進程獲取數據,這兩個步驟都是阻塞的。
非阻塞 I/O 實際上是向內核輪詢,緩衝區是否就緒,如果沒有則繼續執行其他操作。當緩衝區就緒時,講緩衝區內容拷貝到用戶進程,這一步實際上還是阻塞的。
I/O 多路復用技術是指利用單個線程處理多個網路 I/O,我們常說的 select 、 epoll 就是用來輪詢所有 socket 的函數。比如 Apache 採用了前者,而 Nginx 和 Node.js 使用了後者,區別在於後者效率更高。由於 I/O 多路復用實際上還是單線程的輪詢,因此它也是一種非阻塞 I/O 的方案。
非同步 I/O 是最理想的 I/O 模型,然而可惜的是真正的非同步 I/O 並不存在。 Linux 上的 AIO 通過信號和回調來傳遞數據,但是存在缺陷。現有的 libeio 以及 Windows 上的 IOCP,本質上都是利用線程池與阻塞 I/O 來模擬非同步 I/O。
Node.js 線程模型
很多文章都提到 Node.js 是單線程的,然而這樣的說法並不嚴謹,甚至可以說很不負責,因為我們至少會想到以下幾個問題:
- Node.js 在一個線程中如何處理併發請求?
- Node.js 在一個線程中如何進行文件的非同步 I/O?
- Node.js 如何重覆利用伺服器上的多個 CPU 的處理能力?
網路 I/O
Node.js 確實可以在單線程中處理大量的併發請求,但這需要一定的編程技巧。總之,在利用 Node.js 編程時,任何耗時操作一定要使用非同步來完成,避免阻塞當前函數。因為你在為客戶端提供服務,而所有代碼總是單線程、順序執行。
文件 I/O
我在之前的文章中也強調過,非同步是為了優化體驗,避免卡頓。而真正節省處理時間,利用 CPU 多核性能,還是要靠多線程並行處理。
實際上 Node.js 在底層維護了一個線程池。之前在基礎概念部分也提到過,不存在真正的非同步文件 I/O,通常是通過線程池來模擬。線程池中預設有四個線程,用來進行文件 I/O。
需要註意的是,我們無法直接操作底層的線程池,實際上也不需要關心它們的存在。線程池的作用僅僅是完成 I/O 操作,而非用來執行 CPU 密集型的操作,比如圖像、視頻處理,大規模計算等。
如果有少量 CPU 密集型的任務需要處理,我們可以啟動多個 Node.js 進程並利用 IPC 機制進行進程間通訊,或者調用外部的 C++/Java 程式。如果有大量 CPU 密集型任務,那隻能說明選擇 Node.js 是一個錯誤的決定。
事件迴圈
在 Node.js 中存在一個事件迴圈(Event Loop)。
一次完整的 Event Loop 也可以分為多個階段(phase),依次是 poll、check、close callbacks、timers、I/O callbacks 、Idle。
由於 Node.js 是事件驅動的,每個事件的回調函數會被註冊到 Event Loop 的不同階段。比如 fs.readFile 的回調函數被添加到 I/O callbacks, setImmediate 的回調被添加到下一次 Loop 的 poll 階段結束後, process.nextTick() 的回調被添加到當前 phase 結束後,下一個 phase 開始前。
不同非同步方法的回調會在不同的 phase 被執行,掌握這一點很重要,否則就會因為調用順序問題產生邏輯錯誤。
Event Loop 不斷的迴圈,每一個階段內都會同步執行所有在該階段註冊的回調函數。這也正是為什麼我在網路 I/O 部分提到,不要在回調函數中調用阻塞方法,總是用非同步的思想來進行耗時操作。一個耗時太久的回調函數可能會讓 Event Loop 卡在某個階段很久,新來的網路請求就無法被及時響應。
由於本文的目的是對 Node.js 有一個初步的,全面的認識。就不詳細介紹 Event Loop 的每個階段了。
數據流
使用數據流的好處很明顯,生活中也有真實寫照。舉個例子,老師佈置了暑假作業,如果學生每天都做一點(作業流),就可以比較輕鬆的完成任務。如果積壓在一起,到了最後一天,面對堆成小山的作業本,就會感到力不從心。
Server 開發也是這樣,假設用戶上傳 1G 文件,或者讀取本地 1G 的文件。如果沒有數據流的概念,我們需要開闢 1G 大小的緩衝區,然後在緩衝區滿後一次性集中處理。
如果是採用數據流的方式,我們可以定義很小的一塊緩衝區,比如大小是 1Mb。當緩衝區滿後就執行回調函數,對這一小塊數據進行處理,從而避免出現積壓。
實際上 request 和 fs 模塊的文件讀取都是一個可讀數據流:
總結
對於高併發的長連接,事件驅動模型比線程輕量得多,多個 Node.js 進程配合負載均衡可以方便的進行拓展。因此 Node.js 非常適合為 I/O 密集型應用提供服務。但這種方式的缺陷就是不擅長處理 CPU 密集型任務。
本來這次是想自己寫一篇文章看看的,但是看到他人寫的文章太好了~~~
對不起,這次的文章後半部分都是參考的他人的文章
參考文章: 為什麼要用node.js