寫在前面 書籍介紹:本書由首章Node介紹為索引,涉及Node的各個方面,主要內容包含模塊機制的揭示、非同步I/O實現原理的展現、非同步編程的探討、記憶體控制的介紹、二進位數據Buffer的細節、Node中的網路編程基礎、Node中的Web開發、進程間的消息傳遞、Node測試以及通過Node構建產品需要的 ...
寫在前面
- 書籍介紹:本書由首章Node介紹為索引,涉及Node的各個方面,主要內容包含模塊機制的揭示、非同步I/O實現原理的展現、非同步編程的探討、記憶體控制的介紹、二進位數據Buffer的細節、Node中的網路編程基礎、Node中的Web開發、進程間的消息傳遞、Node測試以及通過Node構建產品需要的註意事項。
- 我的簡評:這是一本難得的好書,這本書理論和實踐結合的很好。如果你是一個純前端的開發者,這本書可以讀讀開拓些視野,如果你是一個全棧的開發者,這本書作為入門和深入後端也很不錯,推薦拜讀。
- !!文末有pdf書籍、筆記思維導圖、隨書代碼打包下載地址,需要請自取!閱讀[書籍精讀系列]所有文章,請移步:推薦收藏-JavaScript書籍精讀筆記系列導航
第一章 Node簡介
1.1.Node的誕生歷程
- 2009年3月, Ryan Dahl
1.2.Node的命名與起源
- 別名 Nodejs、 NodeJS、 Node.js
- 找到了設計高性能, Web伺服器的幾個要點: 事件驅動、非阻塞I/O
- JavaScript 高性能、符合事件驅動、沒有歷史包袱
- 構建網路應用的一個基礎框架
1.3.Node給JavaScript帶來的意義
- 瀏覽器中除了V8作為JavaScript引擎外,還有一個WebKit佈局引擎
- 瀏覽器通過事件驅動來服務界面上的交互, Node通過過事件驅動來服務I/O
1.4.Node的特點
- 非同步I/O、事件與回調函數、單線程、跨平臺
- 單線程:弱點1:無法利用多核CPU;弱點2:錯誤會引起整個應用退出,應用的健壯性值得考驗;弱點3:大量計算占用CPU導致無法繼續調用非同步I/O;Node採用與Web Workers相同的思路來解決單線程中大計算量的問題:child_process;
- 跨平臺:在操作系統與Node上層模塊系統之間構建了一層平臺層架構,即libuv;Node的第三方C++模塊也可以藉助libuv實現跨平臺;
1.5.Node的應用場景
- I/O密集型:面向網路且擅長並行I/O
- 是否不擅長CPU密集型業務:採用使用多線程的方式進行計算;通過編寫C++擴展的方式更高效利用CPU;
- 與遺留系統和平共處
- 分散式應用:數據平臺、資料庫集群
1.6.Node的使用者
- 前後端編程語言環境統一
- Node帶來的高性能I/O用於實時應用
- 並行I/O使得使用者可以更高效的利用分散式環境
- 並行I/O,有效利用穩定介面提升Web渲染能力
- 雲計算平臺提供Node支持
- 游戲開發領域
- 工具類應用
第二章 模塊機制
- 大致經歷了工具類庫、組件庫、前端框架、前端應用的變遷
2.1.CommonJS規範
- CommonJS的出發點:規範薄弱,以下缺陷(沒有模塊系統、標準庫較少、沒有標準介面、缺乏包管理系統)
- CommonJS的模塊規範:主要分為模塊引用、模塊定義和模塊標識3個部分
2.2.Node的模塊實現
- 優先從緩存載入:瀏覽器僅僅緩存文件,而Node緩存的是編譯和執行之後的對象
- 路徑分析和文件定位:Node 會按.js、.json、.node的次序補足擴展名
- 模塊編譯:每一個編譯成功的模塊都會將其文件路徑作為索引緩存在Module._cache對象上;在編譯的過程中,Node對獲取的JavaScript文件內容進行頭尾包裝;(function(exports, require, module, __filename, __dirname) {\n, 在尾部添加了\n});C/C++模塊,Node調用process.dlopen()方法進行載入和執行;
2.3.核心模塊
- JavaScript核心模塊的編譯過程:C/C++文件放在Node項目的src目錄下,JavaScript文件存放在lib目錄下;編譯程式需要將所有的JavaScript模塊文件編譯為C/C++代碼;
- C/C++核心模塊的編譯過程:Node在啟動時,會生成一個全局變數process,並提供Binding()方法來協助內建模塊
2.4.C/C++擴展模塊
- 說明:JavaScript的一個典型弱點就是位運算;*nix下通過g++/gcc等編譯器編譯為動態鏈接共用對象文件.so,Windows下則需要通過VisualC++的編譯器編譯為動態鏈接庫文件.dll;
- 前提條件:GYP項目生成工具、V8引擎C++庫、libuv庫、Node內部庫、等等
- C/C++擴展模塊的編寫:普通的擴展模塊與內建模塊的區別在於無需將源代碼編譯進Node,而是通過dlopen()方法動態載入
- C/C++擴展模塊的編譯:寫好.gyp項目文件是除編碼外的頭等大事;編譯過程會根據平臺不同,分別通過make或者vcbuild進行編譯;
- C/C++擴展模塊的載入:require()方法通過解析標識符、路徑分析、文件定位,然後載入執行即可;載入.node文件實際上經歷了兩個步驟,第一個步驟是掉用uv_dlopen方法去打開動態鏈接庫,第二個步驟調用uv_dlsym()方法找到動態鏈接庫中通過NODE_MODULE巨集定義的方法地址;
2.5.模塊調用棧
- JavaScript核心模塊主要扮演的職責有兩類:一類是作為C/C++內建模塊的封裝層和橋接層,供文件模塊調用;一類是純粹的功能模塊,不需要跟底層打交道
2.6.包和NPM
- 包描述文件與NPM:包規範的定義可以幫助Node解決依賴包安裝的問題;NPM實際需要的欄位主要有name、version、description、keywords、repositories、author、bin、main、scripts、engines、dependencies、devDependencies;
- NPM常用功能:NPM幫助完成了第三方模塊的發佈、安裝和依賴;查看幫助npm、分析包npm ls;
- 局域NPM:能夠享受到NPM上眾多的包,同時對自己的包進行保密和限制
- NPM潛在問題:開發人員水平不一,包質量也良莠不齊;NPM模塊首頁上的依賴榜可以說明模塊的質量和可靠性;GitHub上項目的觀察者數量和分支數量從側面反映模塊的可靠性和流行度;計劃引入CPAN社區中的Kwalitee風格來讓模塊進行自然排序;
2.7.前後端共用模塊
- 模塊的側重點:前端通過網路載入代碼,瓶頸在於帶寬,後端從磁碟載入,瓶頸在於CPU和記憶體等資源
- AMD規範:AMD模塊需要用define來明確定義一個模塊,而在Node實現中是隱式包裝的
- CMD規範:CMD與AMD規範的主要區別在於定義模塊和依賴引入的部分
- 相容多種模塊規範:包裝相容Node、AMD、CMD以及常見的瀏覽器環境中
第三章 非同步IO
- 伴隨著非同步I/O的還有事件驅動和單線程
3.1.為什麼要非同步I/O
- 用戶體驗:I/O是昂貴的,分散式I/O是更昂貴的
- 資源分配:利用單線程,遠離多線程死鎖、狀態同步等問題;利用非同步I/O,讓單線程遠離阻塞,以更好的使用CPU
3.2.非同步I/O與實現現狀
- 非同步I/O在Node中應用最為廣泛,但是它並非Node的原創
- 非同步I/O與非阻塞I/O:操作系統內核對於I/O只有兩種方式:阻塞與非阻塞;現存的輪詢技術主要有:read,select,poll,epoll,kequeue(read:重覆調用來檢查I/O的狀態來完成完整數據的讀取;select:通過對文件描述符上的事件狀態來進行判斷;poll:採用鏈表的方式避免數組長度的限制,其次能避免不需要的檢查;epoll:Linux下效率最高的I/O事件通知機制;kqueue:與epoll類似,不過僅在FreeBSD系統下存在)
- 理想的非阻塞非同步I/O
- 現實的非同步I/O:在Node中,無論是*nix還是Windows平臺,內部完成I/O任務的另有線程池
3.3.Node的非同步I/O
- 事件迴圈:Node自身的執行模型:事件迴圈
- 觀察者:在Node中,事件主要來源於網路請求、文件I/O等;在Windows下,這個迴圈基於IOCP創建,而在*nix下則基於多線程創建;
- 請求對象:從JavaScript發起調用到內核執行完I/O操作的過渡過程中存在的一種中間產物
- 執行回調:事件迴圈、觀察者、請求對象、I/O線程池這四者共同構成Node非同步I/O模型的基本要素;完成非同步I/O的過程(Windows下通過IOCP向系統內核發送I/O調用和從內核獲取已完成的I/O操作,配以事件迴圈;Linux下通過epoll實現;FreeBSD下通過kqueue實現;Solaris下通過Event ports實現)
3.4.非I/O的非同步API
- 與I/O無關的非同步API:setTimeout()、setInterval()、setImmedate()、process.nextTick()
- 定時器:實現原理與非同步I/O比較類似,只是不需要I/O線程池的參與;問題在於並非精確的;
- process.nextTick():採用定時器需要動用紅黑樹,創建定時器對象和迭代等操作;定時器中採用紅黑樹的操作時間複雜度為O(lg(n)),nextTick()的時間複雜度為O(1);
- setImmediate():process.nextTick()中的回調函數執行的優先順序要高於setImmediate();process.nextTick()屬於idle觀察者,setImmediate()屬於check觀察者;
3.5.事件驅動與高性能伺服器
- Node無需為每一個請求創建額外的對應線程
- 不受線程上下文切換開銷的影響
- 一些知名的基於事件驅動的實現:Ruby的Event Machine;Perl的AnyEvent;Python的Twisted;
第四章 非同步編程
- Node是首個將非同步大規模帶到應用層面的平臺
4.1.函數式編程
- 高階函數:高階函數是可以把函數作為參數,或是將函數作為返回值的函數
- 偏函數用法:偏函數用法是指創建一個調用另外一個部分參數或變數已經預置的函數的函數的用法;通過指定部分參數來產生一個新的定製函數的形式就是偏函數;
4.2.非同步編程的優勢與難點
- 優勢:Node的非同步模型和V8的高性能
- 難點:異常處理、函數嵌套、阻塞代碼、多線程編程、非同步轉同步
4.3.非同步編程解決方案
- 事件發佈/訂閱模式:如果一個事件添加了超過10個偵聽器將會得到一條警告,使用emitter.setMaxListeners(0)去掉限制
- Promise/Deferred模式:Deferred主要是用於內部,用於維護非同步模型的狀態;Promise則作用與外部,通過then()方法暴露給外部以添加自定義邏輯;Promise/Deferred模式將業務中不可變的部分封裝在了Deferred,將可變的部分交給Promise;
- 流程式控制制庫:尾觸發與Next,目前應用最多的地方是Connect的中間件;async,長期占據NPM依賴榜的前三名,series實現非同步的串列執行,parallel實現非同步的並行執行,waterfall實現非同步調用的依賴處理;step,更輕量;wind,思路完全不同的非同步編程方案;
4.4.非同步併發控制
- 同步I/O,每個I/O都是彼此阻塞的,不會出現耗用文件描述符太多的情況
- bagpip的解決方案:bagpipe模塊的解決思路(通過一個隊列來控制併發量;調用發起但未執行的非同步調用量小於限定值,從隊列中取出執行;如果活躍調用達到限定值,調用暫時存放在隊列中;每一個非同步調用結束時,從隊列中取出新的非同步調用執行;);拒絕訪問;超時控制;
- async的解決方案:async中parallelLimit()用於處理非同步調用的限制
第五章 記憶體控制
- 基於無阻塞、事件驅動建立的Node服務,具有記憶體消耗低的優點,非常適合處理海量的網路請求
5.1.V8的垃圾回收機制與記憶體限制
- Node與V8
- V8的記憶體限制:只能使用部分記憶體(64位系統下約為1.4G,32位系統下約為0.7G)
- V8的對象分配:V8依然提供選項讓我們使用更多的記憶體;--max-old-space-size設置老生代記憶體空間的最大值;--max-new-space-size設置新生代記憶體空間大小;
- V8的垃圾回收機制:垃圾回收策略主要基於分代式垃圾回收機制;在分代的基礎上,新生代的對象主要通過Scavenge演算法進行垃圾回收;V8在老生代中主要採用Mark-Sweep和Mark-Compact相結合的方式進行垃圾回收;
- 查看垃圾回收日誌:在啟動時添加--trace-gc參數;node啟動時使用--prof參數,可以得到V8執行時的性能分析數據;提供linux-tick-processor工具用於統計日誌信息;
5.2.高效使用記憶體
- 作用域:能形成作用域的有函數調用、with以及全局作用域
- 閉包:實現外部作用域訪問內部作用域中變數的方法
- 無法立即回收的記憶體有閉包和全局變數引用這兩種情況
5.3.記憶體指標
- 查看記憶體使用情況:process.memoryUsage()可以看到Node進程的記憶體占用情況;os模塊的totalmem()和freemem()查看系統的總記憶體和閑置記憶體;
- 堆外記憶體:不通過V8分配的記憶體稱為堆外記憶體;利用堆外記憶體可以突破記憶體限制的問題;
5.4.記憶體泄露
- 造成記憶體泄露的原因幾個:緩存、隊列消費不及時、作用域未釋放
- 慎將記憶體當作緩存:在Node中任何試圖拿記憶體當緩存的行為都應當被限制
- 關註隊列狀態:使任何非同步調用的回調都具備可控的響應時間
5.5.記憶體泄露排查
- 常見的用於定位Node應用記憶體泄露的工具:v8-profiler、node-headpdump、node-mtrace、dtrace、node-memwatch
- node-headdump
- node-memwatch
5.6.大記憶體應用
- Node提供了stream模塊用於處理大文件
- 要小心,即使V8不限制堆記憶體的大小,物理記憶體依然有限制
第六章 理解Buffer
6.1.Buffer結構
- 模塊結構:Buffer是一個像Array的對象,但它主要用於操作對象
- Buffer對象:
buf[10]
的元素值是一個0到255的隨機值 - Buffer記憶體分配:Buffer對象的記憶體分配不是在V8的堆記憶體中,而是在Node的C++層面實現記憶體的申請的;Node以8KB為界限來區分Buffer是大對象還是小對象;真正的記憶體是在Node的C++層面提供的,JavaScript層面只是使用它;
6.2.Buffer的轉換
- 目前支持的字元串編碼類型:ASCII、UTF-8、UTF-16LE/UCS-2、Base64、Binary、Hex
- 字元串轉Buffer
- Buffer轉字元串
- Buffer不支持的編程類型:Buffer提供一個isEncoding()函數來判斷編碼是否支持轉換;iconv和iconv-lite兩個模塊可以支持更多的編碼類型轉換;
6.3.Buffer的拼接
- 亂碼是如何產生的
- setEncode()與string_decoder()
- 正確拼接Buffer:調用Buffer.concat()方法生成一個合併的Buffer對象
6.4.Buffer與性能
- Buffer在文件I/O和網路I/O中運用廣泛
- Buffer是二進位數據,字元串與Buffer之間存在編碼關係
第七章 網路編程
- Node提供了net、dgram、http、https這4個模塊,分別用於處理TCP、UDP、HTTP、HTTPS
7.1.構建TCP服務
- TCP
- 創建TCP伺服器端
- TCP服務的事件:TCP套接字是可寫可讀的Stream對象,可以利用pipe()方法巧妙的實現管道操作
7.2.構建UDP服務
- 創建UDP套接字
- 創建UDP伺服器端
- 創建UDP客戶端
- UDP套接字事件:UDP套接字只是一個EventEmitter的實例,而非Stream的實例
7.3.構建HTTP服務
- HTTP
- http模塊:TCP服務以connection為單位進行服務,HTTP服務以request為單位進行服務
- HTTP客戶端
7.4.構建WebSocket服務
- WebSocket握手
- WebSocket的數據傳輸
7.5.網路服務與安全
- Node在網路安全上提供了3個模塊,分別為crypto、tls、https
- TLS/SSL
- TLS服務
- HTTPS服務
第八章 構建Web應用
8.1.基礎功能
- 請求方法
- 路徑解析
- 查詢字元串
- Cookie:Cookie處理的幾步(伺服器向客戶端發送cookie;瀏覽器將Cookie保存;之後每次瀏覽器都會將Cookie發向伺服器端)
- Session:常見的兩種實現方式(基於Cookie來實現用戶和數據的映射;通過查詢字元串來實現瀏覽器端和伺服器端數據的對應);Connect預設採用connect_uid,Tomcat會採用jsessionid等;
- 緩存:提高性能,YSlow中提到的幾條關於緩存的規則(添加Expires或Cache-Control到報文中;配置ETags;讓Ajax可緩存)
- Basic認證:通過Base64加密後在網路中傳送,有太多的缺點
8.2.數據上傳
- 表單數據:通過報頭的Transfer-Encoding或Content-Length即可判斷請求中是否帶有內容
- 附件上傳:瀏覽器在遇到multipart/form-data表單提交時,構造的請求報文與普通表單完全不同;formidable,基於流式處理解析報文,將接收到的文件寫入到系統的臨時文件夾中,並返回對應的路徑;
- 數據上傳與安全
8.3.路由解析
- 文件路徑型
- MVC
- RESTful
8.4.中間件
- 異常處理
- 中間件與性能
- 從凌亂的發散狀態收斂成很規整的組織方式
8.5.頁面渲染
- 內容響應:不同的文件類型具有不同的Mime值;Content-Disposition欄位影響的行為是客戶端會根據它的值判斷是應該將報文數據當作即時瀏覽的內容,還是可下載的附件;
- 視圖渲染
- 模板:實質就是將模板文件和數據通過模板引擎生成最終的HTML代碼;形成模板技術4個要素(模板語言;包含模板語言的模板文件;擁有動態數據的數據對象;模板引擎);mustache,弱邏輯的模板;最知名的有EJS、Jade等;
- Bigpipe:用於調用限流,解決重數據頁面的載入速度問題;解決思路是將頁面分割成多個部分,先向用戶輸出沒有數據的佈局,將每個部分逐步輸出到前端,再最渲染填充框架,完成整個網頁的渲染;
第九章 玩轉進程
9.1.服務模型的變遷
- 石器時代:同步 - 只在一些無併發要求的應用中存在
- 青銅時代:複製進程 - 要複製較多的數據,啟動是較為緩慢的
- 白銀時代:多進程 - 時間將會被耗用在上下文切換中
- 黃金時代:事件驅動 - 記憶體耗用的問題著名的C10k問題;單線程避免了不必要的記憶體開銷和上下文切換開銷;
9.2.多進程架構
- child_process.fork()複製的都是一個獨立的進程,獨立而全新的V8實例
- 啟動多個進程只是為了充分將CPU資源利用起來,而不是為瞭解決併發問題
- 創建子進程
- 進程間通信:JavaScript主線程與UI渲染共用同一個線程;實現進程間通信的技術有很多,如命名管道、匿名管道、socket、信號量、共用記憶體、消息隊列、Domain Socket等;操作系統的文件描述符是有限的;
- 句柄傳遞:句柄是一種可以用來標識資源的引用,它的內部包含了指向對象的文件描述符;文件描述符實際上是一個整數值;Node進程之間只有消息傳遞,不會真正的傳遞對象;
9.3.集群穩定之路
- 進程事件
- 自動重啟:創建新工作進程在前,退出異常進程在後
- 負載均衡:輪叫調度的工作方式是由主進程接受連接,將其依次分發給工作進程
- 狀態共用:解決數據共用最簡單、直接的方式就是通過第三方進行數據存儲;主動通知;
9.4.Cluster模塊
- Cluster工作原理:事實上是child_process和net模塊的組合應用
- Cluster事件
第十章 測試
10.1.單元測試
- 編寫可測試代碼的幾個原則:單一職責、介面抽象、層次分離;
- 單元測試主要包含斷言、測試框架、測試用例、測試覆蓋率、mock、持續繼承等,由於Node的特殊性,還會加入非同步代碼測試和私有方法的測試;
- JavaScript的斷言規範最早來自於CommonJS的單元測試規範;
- 單元測試風格主要有TDD(測試驅動開發)和BDD(行為驅動開發)兩種;
- BDD對測試用例的組織主要採用describe和it進行組織;
- TDD對測試用例的組織主要採用suite和test完成;
- 單元測試覆蓋率方便我們定位沒有測試到的代碼行;
- 私有方法的測試(Java一類的語言,私有方法訪問可以通過反射的方式實現;巧妙利用閉包的訣竅,在eval()執行時,實現對模塊內部局部變數的訪問,從而可以將局部變數導出給測試用例調用執行;)
10.2.性能測試
- 單元測試主要用於檢測代碼的行為是否符合預期,性能測試的範疇比較廣泛,包括負載測試、壓力測試和基準測試
- 基準測試
- 壓力測試:最常用的工具是ab、siege、http_load等
- 基準測試驅動開發
- 測試數據與業務數據的轉換
第十一章 產品化
11.1.項目工程化
- 目錄結構
- 構建工具:在Web應用中通常會在Makefile文件中編寫一些構建任務來幫助提升效率;合併編譯、應用打包、運行測試、清理目錄、掃描代碼等;
- 編碼規範:一種是文檔式的約定,一種是代碼提交時的強制檢查
- 代碼審查
11.2.部署流程
- 部署環境
- 部署操作
11.3.性能
- 幾個拆分原則:做專一的事;讓擅長的工具做擅長的事情;將模型簡化;將風險分離;
- 動靜分離
- 啟動緩存
- 多進程架構
- 讀寫分離:進行資料庫的讀寫分離,將資料庫進行主從設計
11.4.日誌
- 訪問日誌
- 異常日誌:log與info方法都將信息輸出給標準輸出process.stdout,warn與error方法則將信息輸出到標準錯誤process.stderr;console對象上有個Console屬性,它是console對象的構造函數;回調函數中產生的異常,交給全局的uncaughtException事件去捕獲;
- 日誌與資料庫
- 分割日誌
11.5.監控報警
- 監控:一種是業務邏輯型的監控,一種是硬體型的監控;主要指標:日誌監控、響應時間、進程監控、磁碟監控、記憶體監控、CPU占用監控、CPU load監控、I/O負載、網路監控、應用狀態監控、DNS監控;
- 報警的實現
- 監控系統的穩定性
11.6.穩定性
- 典型的水平擴展方式就是多進程、多機器、多機房
寫在後面
- pdf書籍、筆記思維導圖、隨書代碼打包下載地址:https://pan.baidu.com/s/1OhLjjtfffjX3hv2_Pw7AHQ(提取碼:9m33)
- 紙質書京東購買地址:https://u.jd.com/wHmeh4(推薦購買紙質書來學習)