[TOC]
> 大部分內容摘取自《深入淺出 node.js》- 樸靈
> 有些地方真的比較 “深入” 鑒于個人水平只記錄一些要點
# 第 1 章 node.js 簡介
## Node 的特點
1.異步 I/O:Node 在底層構建了很多異步 I/O 的 API,從文件讀取到網絡請求,極大地提升了程序性能。
2.事件與回調函數:配合異步 I/O,將事件點暴露給業務邏輯。這種事件的編程方式具有輕量級、松耦合、只關注事務點等優勢。
3.單線程:單線程的最大好處是不用像多線程編程那樣處處在意狀態的同步問題,沒有死鎖的存在,也沒有線程上下文交換所帶來的性能上的開銷。但是它也有以下弱點(child process 模塊出現后都得到了解決或緩解):
- 無法利用多核 CPU
- 錯誤會引起整個應用退出,應用的健壯性值得考驗
- 大量計算占用 CPU 導致無法繼續調用異步 I/O
在瀏覽器端,Web Workers 能夠創建工作線程來進行計算,以解決 JavaScript 大量計算阻塞 UI 渲染的問題,工作線程為了不阻塞主線程,通過消息傳遞的方式來傳遞運行結果,這也使得工作線程不能訪問到主線程的 UI。
Node 采用了與 Web Worker 相同的思路來解決單線程中大計算量的問題:child process。子進程的出現,意味著 Node 可以從容地應對單線程健壯性和無法利用多核 CPU 方面的問題。通過將計算分發到各個子進程,可以將大量計算分解掉,然后再通過進程之間的事件消息來傳遞結果。
4.跨平臺:借助 libuv 實現跨平臺運行。
## Node 的應用場景
探討的比較多的主要有 I/O 密級型和 CPU 密級型。
Node 面向網絡并且擅長并行 I/O,能夠有效地組織起更多的硬件資源,在 I/O 密集型場景表現還是不錯的。
CPU 密集型應用給 Node 帶來的挑戰主要是:由于 JavaScript 單線程的原因,如果有長時間運行的計算(比如大循環),將會導致 CPU 時間片不能釋放,使得后續 I/O 無法發起。Node 雖然沒有提供多線程用于計算支持,但是有以下兩個方式來充分利用 CPU:
- 編寫 C/C++ 擴展,將一些 V8 不能做到性能極致的地方通過 C/C++ 來實現
- 通過子進程的方式,將一部分 Node 進程當作常駐服務進程用于計算,然后利用進程間的消息來傳遞結果,將計算與 I/O 分離
## Node 使用者的倚重點
- 前后端語言環境統一
- 高性能 I/O 用于實時應用(應用在長連接中,通過 socket.io 實現實時通知的功能)
- 并行 I/O 使得使用者可以更高效地利用分布式環境
- 云計算平臺提供 Node 支持
## 經典的服務器模型的比較
- 同步式:一次只處理一個請求,并且其余請求都處于等待狀態
- 每進程 / 每請求:為每個請求啟動一個進程,這樣可以處理多個請求,但是它不具備擴展性,因為系統資源只有那么多
- 每線程 / 每請求:為每個請求啟動一個線程來處理。盡管線程比進程要輕量,但是由于每個線程要占用一定的內存,當大并發請求到來時,內存將會很快用光,導致服務器緩慢。
Node 通過事件驅動的方式處理請求,無須為每個請求創建額外的線程,可以省掉創建線程和銷毀線程的開銷,同時操作系統在調度任務時因為線程較少,上下文切換的代價很低,這便是 Node 高性能的一個原因。
# 第 2 章 Node 的模塊實現
## CommonJS 的模塊規范
CommonJS 對模塊的定義主要分為模塊引用、模塊定義和模塊標識 3 個部分
1.模塊引用
模塊引用的示例代碼如下:
```js
const math = require('math')
```
require() 方法接受模塊表示,以此引入一個模塊的 API 到當前上下文中
2.模塊定義
在模塊中,存在一個 module 對象,它代表模塊自身,而 exports 是 module 的屬性,用于導出當前模塊的方法或變量。在 Node 中,**一個文件就是一個模塊**。
3.模塊標識
模塊標識就是傳遞給 require() 方法的參數,它必須是符合小駝峰命名的字符串,或者以 .、.. 開頭的相對路徑,或者絕對路徑,可以沒有文件后綴名 .js。
定義模塊的意義在于:將類聚的方法和變量限定在私有的作用域中,每個模塊具有獨立的空間,它們互不干擾,在引用時也顯得干凈利落。這套導出和引入機制使得用戶完全不必考慮變量污染。

## Node 的模塊實現
規范中 require 和 exports 使用起來十分方便,但是 Node 在實現它們的過程中經歷了什么呢?
在 Node 引入模塊,需要經歷如下 3 個步驟:
1. 路徑分析
2. 文件定位
3. 編譯執行
在 Node 中,模塊分為兩類:一類是由 Node 提供的模塊,稱為核心模塊;另一類是用戶編寫的模塊,稱為文件模塊。
- 核心模塊部分在 Node 源代碼的編譯過程中,編譯進了二進制執行文件,在 Node 進程啟動時,部分核心模塊就被直接加載進內存中,所以這部分核心模塊引入時,文件定位和編譯執行這兩個步驟可以省略掉,并且在路徑分析中優先判斷,所以它的加載速度是最快的。
- 文件模塊則是在運行時動態加載,需要完整的路徑分析、文件定位、編譯執行過程,速度比核心模塊慢。
## 模塊加載過程(淺析)
### 優先從緩存加載
與前端瀏覽器會緩存靜態腳本文件以提高性能一樣,Node 對引入過的模塊都會進行緩存,以減少二次引入時的開銷。不同之處在于,瀏覽器僅僅緩存文件,而 Node 緩存的是編譯和執行之后的對象。
不論是核心模塊還是文件模塊,require() 方法對相同模塊的二次加載都一律采用緩存優先的方式,不同之處在于核心模塊的檢查先于文件模塊的緩存檢查。
### 路徑分析和文件定位
1、模塊標識符分析
require() 方法接受一個標識符作為參數,Node 正是基于這樣一個標識符進行模塊查找的。模塊標識符在 Node 中主要分為以下幾類:
- 核心模塊,如 http、fs、path 等
- `.`或`..`開始的相對路徑文件模塊
- 以`/`開始的絕對路徑文件模塊
- 非路徑形式的文件模塊,如自定義的 connect 模塊
**核心模塊**
核心模塊的優先級僅次于緩存加載,它在 Node 的源代碼編譯過程中已經編譯為二進制代碼,其加載過程最快。如果視圖加載一個與核心模塊標識符相同的自定義模塊,那是不會成功的。如果自己編寫了一個 http 用戶模塊,想要加載成功,必須選擇一個不同的標識符或者換用路徑的方式。
**路徑形式的文件模塊**
以`.`、`..`、`/`開頭的標識符,這里都被當作文件模塊來處理。在分析路徑模塊時,require() 方法會將路徑轉為真實路徑,并以真實路徑作為索引,將編譯執行后的結果存放到緩存中,以使二次加載時更快。
**自定義模塊**
自定義模塊指的是非核心模塊,也不是路徑形式的標識符(比如 npm install 的庫)。這類模塊的查找是最費時的,也是所有方式中最慢的一種。
在介紹自定義模塊的查找方式之前,首先需要了解 <span style="font-family: 楷體">模塊路徑</span> 這個概念。
模塊路徑是 Node 在定位文件模塊的具體文件時定制的查找策略,具體表現為一個路徑組成的數組,關于這個路徑的生成規則,我們可以手動嘗試一番:
(1)創建 module_path.js 文件,其內容為 console.log(module.paths)
(2)將其放到任意一個目錄中然后執行 node module_path.js
在 Windows 下,可以得到這樣的一個輸出:
```txt
[ 'D:\\Proj\\JS代碼片段\\node_modules',
'D:\\Proj\\node_modules',
'D:\\node_modules' ]
```
可以看出,模塊路徑的生成規則如下所示:
- 當前文件目錄下的 node_modules 目錄
- 父目錄下的 node_modules 目錄
- 父目錄的父目錄下的 node_modules 目錄
- 沿路徑向上逐級遞歸,直到根目錄下的 node_modules 目錄
它的查找方式與 JavaScript 的原型鏈或作用域鏈的查找方式十分類似:在加載的過程中,Node 會逐個嘗試模塊中的路徑,直到找到目標文件為止。可以看出,當前文件的路徑越深。模塊查找耗時會越多,這便是自定義模塊的加載速度最慢的原因。
2、文件定位
從緩存加載的優化策使得二次引入時不需要路徑分析、文件定位和編譯執行的過程,大大提高了再次加載模塊時的效率。
在文件定位的過程中,有一些細節需要注意,這主要包括文件擴展名的分析、目錄和包的處理。
? 文件擴展名的分析
require() 在分析標識符的過程中,會出現標識符中不包含文件擴展名的情況。CommonJS 模塊規范也允許在標識符中不包含文件擴展名,這種情況下,Node 會按 .js、.node、.json 的次序補足擴展名,依次嘗試。
在嘗試的過程中,需要調用 fs 模塊同步阻塞式地判斷文件是否存在。因為 Node 是單線程的,所以這里是一個會引起性能問題的地方。小訣竅是:如果是 .node 和 .json 文件,在傳遞給 require() 的標識符中帶上擴展名,會加快一點速度。另一個訣竅是:同步配合緩存,可以大幅緩解 Node 單線程中阻塞式調用的缺陷(怎么做?)
? 目錄分析和包
在分析標識符的過程中,require() 通過分析文件擴展名之后,可能沒有查找到對應文件,但卻得到一個目錄,這在引入自定義模塊和逐個模塊路徑進行查找時經常會出現,此時 Node 會將目錄當做一個包來處理。
在這個過程中,Node 對 CommonJS 包規范進行了一定程度的支持。首先,Node 在當前目錄下查找 package.json(CommonJS 包規范定義的包描述文件),通過 JSON.parse() 解析出包描述對象,從中取出 main 屬性指定的文件名進行定位。如果文件名缺少擴展名,將會進入擴展名分析的步驟。而如果 main 屬性指定的文件名錯誤,或者壓根沒有 package.json 文件,Node 會將 index 當作默認文件名,然后依次查找 index.js、index.node、index.json
如果在目錄分析的過程中沒有定位成功任何文件,則自定義模塊進入下一個模塊路徑進行查找,如果模塊路徑數組都被遍歷完畢,依然沒有查找到目標文件,則會拋出查找失敗的異常。
### 模塊編譯
在 Node 中,每個文件模塊都是一個對象,它的定義如下:
```js
function Module (id, parent) {
this.id = id
this.exports = {}
this.parent = parent
if (parent && parent.children) {
parent.children.push(this)
}
this.filename = null
this.loaded = false
this.children = []
}
```
編譯和執行是引入文件模塊的最后一個階段。定位到具體的文件后,Node 會新建一個模塊對象,然后根據路徑載入并編譯。對于不同的文件擴展名,其載入方法也有所不同,具體如下:
- .js 文件。通過 fs 模塊同步讀取文件后編譯執行
- .node 文件。這是用 C/C++ 編寫的擴展文件,通過 dlopen() 方法加載最后編譯生成的文件
- .json 文件。通過 fs 模塊同步讀取文件后,用 JSON.parse() 解析返回結果
- 其余擴展名文件,它們都被當做 .js 文件載入
每一個編譯成功的模塊都會將其文件路徑作為索引緩存在 Module._cache 對象上,以提高二次引入的性能。
編譯的過程較為復雜,有興趣的請自行閱讀。這里只記錄下 JavaScript 模塊的編譯過程。
<br />
**JavaScript 模塊的編譯**
我們知道每個模塊文件中都存在著 require、exports、module 這 3 個變量,但是它們在模塊文件中并沒有定義,那么它們從何而來呢?在 Node 的 API 文檔中,我們知道每個模塊還有`__filename`、`__dirname`這兩個變量的存在,它們又是從何而來的呢?
事實上,在編譯的過程中,Node 對獲取的 JavaScript 文件內容進行了頭尾包裝。一個正常的 JavaScript 文件會被包裝成如下的樣子:
```js
(function (exports, require, module, __filename, __dirname) {
var math = require('math')
exports.data = function (radius) {
return Math.PI * radius * radius
}
})
```
這樣每個模塊文件之間都進行了作用域隔離。包裝之后的代碼會通過 vm 原生模塊的 runInThisConText() 方法執行(類似 eval,只是具有明確上下文,不會污染全局),返回一個 function 對象。最后,將當前模塊對象的 exports 屬性、require() 方法、module(模塊對象自身),以及在文件定位中的得到的完整文件路徑和文件目錄作為參數傳遞給這個 function() 執行。這就是這些變量并沒有定義在每個模塊文件中卻存在的原因。
# 第 3 章 異步 I/O
Node 實現異步 I/O 的過程可以提取為幾個關鍵詞:單線程、事件循環、觀察者、I/O 線程池。
這里的單線程和 I/O 線程池之間看起來有些悖論的樣子,事實上,在 Node 中,除了 JavaScript 是單線程外,Node 自身其實是多線程的,只是 I/O 線程使用的 CPU 較少。另一個需要重視的觀點則是,除了用戶代碼無法并行執行外,所有的 I/O (磁盤 I/O 和網絡 I/O 等)都是可以并行執行的。
## 事件循環
這里將 Node 的事件循環抽象一下便于理解:

在進程啟動時,Node 便會創建一個類似于 white(true) 的循環,每執行一次循環體的過程我們稱為 Tick。每個 Tick 的過程就是查看是否有事件待處理,如果有,就取出事件及其相關的回調函數。如果存在關聯的回調函數,就執行它們。然后進入下個循環,如果不再有事件處理,就退出流程。
## 觀察者
在每個 Tick 的過程中,如何判斷是否有事件需要處理呢?這里必須要引入的概念是觀察者。每個事件循環中有一個或者多個觀察者,而判斷是否有事件要處理的過程就是向這些觀察者詢問是否有需要處理的事件。
在 Node 中,事件主要來源于網絡請求、文件 I/O 等,這些事件對應的觀察者有文件 I/O 觀察者、網絡 I/O 觀察者等。
事件循環是一個典型的 *生產者/消費者模型*。異步 I/O、網絡請求等是事件的生產者,源源不斷地為 Node 提供不同類型的事件,這些事件被傳遞到對應的觀察者那里,事件循環則從觀察者那里取出事件并處理。
在 Windows 下,這個循環基于 IOCP 創建,而在 *nix 下則基于多線程創建。
## 請求對象
對于 Node 中的異步 I/O 調用而言,回調函數不由開發者來調用,那么我們發出調用后,到回調函數被執行,中間發生了什么呢?事實上,從 JavaScript 發起調用到內核執行完 I/O 操作的過渡過程中,存在一種中間產物,它叫做**請求對象**。
下面我們以 fs.open() 方法作為例子,來探索 Node 與底層之間是如何執行異步 I/O 調用以及回調函數究竟是如何被調用執行的:
```js
fs.open = function (path, flags, mode, callback) {
// ...
ImageBitmapRenderingContext.open(pathModule._makeLong(path),
stringToFlags(flags),
mode,
callback)
}
```
fs.open() 的作用是根據指定路徑和參數取打開一個文件,從而得到一個文件描述符,這是后續所有 I/O 操作的初始操作。JavaScript 層面的代碼通過調用 C++ 核心模塊進行下層的操作,如圖所示:

從 JavaScript 調用 Node 的核心模塊,核心模塊調用 C++ 內建模塊,內建模塊通過 libuv 進行系統調用,這是 Node 里最經典的調用方式。
在 uv_fs_open() 的調用過程中,會創建一個 FSReqWrap 請求對象,從 JavaScript 層傳入的參數和當前方法都被封裝在這個請求對象中,回調函數則被設置在這個對象的 oncomplete_sym 屬性上:
`req_wrap->object_Set(oncomplete_sym, callback)`
對象包裝完畢后,在 Windows 下,則調用 QueueUserWorkItem() 方法將這個 FSReqWrap 對象推入線程池中等待執行,該方法的代碼如下:
`QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTEDEFAULT)`
第一個參數是要執行的方法的引用,第二個參數是這個方法運行時所需要的參數,第三個參數是執行的標志。當線程池中有可用線程時,就會調用 us_fs_thread_proc() 方法,該方法會根據傳入參數的類型調用相應的底層函數。
至此,JavaScript 調用立即返回,JavaScript 線程可以繼續執行當前任務的后續操作。當前的 I/O 操作在線程池中等待執行,不管它是否阻塞 I/O,都不會影響到 JavaScript 線程的后續執行,如此就達到了異步的目的。
總結:請求對象是異步 I/O 過程中的重要產物,所有的狀態都保存在這個對象中,包括送入線程池等待執行以及 I/O 操作完畢后的回調處理。
## 執行回調
線程池中的 I/O 操作調用完畢后,會將獲取的結果存儲在 req->result 屬性上,然后通知 IOCP(Windows),告訴當前對象操作已經完成,并將線程歸還線程池。
事件循環中的 I/O 觀察者,在每次 Tick 的執行中,會檢查線程池中是否有執行完的請求,如果存在,會把請求對象加入到 I/O 觀察者的隊列中,然后將其當做事件處理。
I/O 觀察者回調函數的行為就是取出請求對象的 result 屬性作為參數,然后調用執行。
至此,整個異步 I/O 的流程完全結束,如圖所示:

# 第 5 章 內存控制
## V8 的垃圾回收機制與內存限制
在一般的后端開發語言中,基本的內存使用上沒有什么限制,然而在 Node 中通過 JavaScript 使用內存時會發現只能使用部分內存(64 位系統下約為 1.4 GB,32 位系統下約為 0.7 GB)。在這樣的限制下,將會導致 Node 無法直接操作大內存對象,比如無法將一個 2GB 的文件讀入內存中進行字符串分析處理。(stream 模塊解決了這個問題)
造成這個問題的主要原因在于 Node 基于 V8 構建,V8 的內存管理機制在瀏覽器的應用場景下綽綽有余,但在 Node 中卻限制了開發者。所以我們有必要知曉 V8 的內存管理策略。
## V8 的對象分配
在 V8 中,所有的 JavaScript 對象(object)都是通過堆來進行分配的,Node 提供了 V8 中內存使用量的查看方式,如下:
```js
process.memoryUsage()
{ rss: 21434368,
heapTotal: 7159808,
heapUsed: 4455120,
external: 8224 }
```
其中,heapTotal 和 heapUsed 是 V8 的堆內存使用情況,前者是已申請到的堆內存,后者是當前使用的量。如果已申請的堆空閑內存不夠分配新的對象,將繼續申請堆內存,直到堆的大小超過 V8 的限制為止。
至于 V8 為何要限制堆的大小,主要是內存過大會導致垃圾回收引起 JavaScript 線程暫停執行的時間增長,應用的性能和響應會直線下降,這樣的情況不僅僅是后端服務無法接受,前端瀏覽器也無法接受。因此,在當時的考慮下直接限制堆內存是一個好的選擇。
不過 V8 也提供了選項讓我們打開這個限制,Node 在啟動時可以傳遞如下的選項:
```js
node --max-old-space-size=1700 test.js // 單位為 MB 設置老生代的內存空間
node --max-new-space-size=1024 test.js // 單位為 KB 設置新生代的內存空間
```
上述參數在 V8 初始化時生效,一旦生效就不能再改變。
## V8 的垃圾回收機制
V8 的垃圾回收策略主要基于分代式垃圾回收機制,在實際應用中,人們發現沒有一種垃圾回收算法能夠勝任所有的場景,因為對象的生存周期長短不一,不同的算法只能針對特定情況具有最好的效果。因此,現代的垃圾回收算法按對象的存活時間將內存的垃圾回收進行不同的分代,然后分別對不同分代的內存施以更高效的算法。
在 V8 中,主要將內存分為新生代和老生代。新生代的對象為存活時間較短的對象,老生代的對象為存活時間較長或常駐內存的對象。

*Scavenge 算法*
在分代的基礎上,新生代的對象主要通過 Scavenge 算法進行垃圾回收,在 Scavenge 的具體實現中,主要采用了 Cheney 算法。
Cheney 算法是一種采用復制的方式實現的垃圾回收算法,它將堆內存一分為二,每一部分空間稱為 semispace。在這兩個 semispace 空間中,只有一個處于使用中,另一個處于閑置狀態。處于使用狀態的 semispace 空間稱為 From 空間,處于閑置狀態的空間稱為 To 空間。

當我們分配對象時,先是在 From 空間中進行分配。當開始進行垃圾回收時,會檢查 From 空間的存活對象,這些存活對象將被復制到 To 空間中,而非存活對象占用的空間將被釋放。
完成復制后,From 空間和 To 空間的角色發生對換。
- Scavenge 的缺點是只能使用堆內存中的一半
- Scavenge 是典型的犧牲空間換取時間的算法,適合應用于新生代中,因為新生代中對象的生命周期較短
- 當一個對象經過多次復制仍然存活時,它將會被認為是生命周期較長的對象,其隨后會被移動到老生代中,這一過程稱為**晉升**
*Mark-Sweep & Mark-Compact*
老生代中的對象生命周期較長,存活對象占較大比重,V8 在老生代主要采用 Mark-Sweep 和 Mark-Compact 相結合的方式進行垃圾回收
Mark-Sweep:標記清除,其分為標記和清除兩個階段。在標記階段遍歷堆中的所有對象,并標記活著的對象,在清除階段只清除沒有被標記的對象。Mark-Sweep 最大的問題在于進行一次標記清除回收后,內存空間會出現不連續的狀態,內存碎片會對后續的內存分配造成問題,比如碎片空間不足以分配一個大對象導致提前觸發垃圾回收。
于是就有了 Mark-Compact:標記整理,簡單來說就是標記完成后加一個整理階段,存活對象往一端移動(合并),整理完成后直接清理掉邊界外的內存。

*Incremental Marking*
為了避免出現 JavaScript 應用邏輯與垃圾回收器看到的不一致的情況,垃圾回收的 3 種基本算法需要將應用邏輯暫停下來,待執行玩垃圾回收后再恢復執行應用邏輯,這種行為被稱為全停頓(stop-the-world)。
對于新生代來說,全停頓的影響不大,但是對于老生代就需要改善。
為了降低全堆垃圾回收帶來的停頓時間,V8 采用了增量標記(incremental marking)的技術,大概是將原本一口氣停頓完成的動作拆分為許多小“步進”,每做完一“步進”就讓 JavaScript 應用邏輯執行一小會兒,垃圾回收與應用邏輯交替執行直到標記階段完成。

V8 后續還引入了延遲清理(lazy sweeping)、增量式整理(incremental compaction)、[并發標記](https://www.oschina.net/translate/v8-javascript-engine) 等技術,感興趣的可以自行了解。
## 查看垃圾回收日志
啟動時添加 --trace_gc 參數,這樣在進行垃圾回收時,將會從標準輸出中打印垃圾回收的日志信息。
下面是一段示例,執行結束后,將會在 gc.log 文件中得到所有垃圾回收信息:
```shell
node --trace_gc -e "var a = []; for (var i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log
```
通過在 Node 啟動時使用 --prof 參數,可以得到 V8 執行時的性能分析數據:
```shell
node --prof test.js
```
# 第 6 章 理解 Buffer
在網絡流和文件的操作中需要處理大量二進制數據,JavaScript 自有的字符串遠遠不能滿足這些需求,于是 Buffer 對象應運而生。
Buffer 是一個像 Array 的對象,但它主要用于操作字節,其是一個典型的 JavaScript 與 C++ 結合的模塊,它將性能相關部分用 C++ 實現,將非性能相關部分應用 JavaScript 實現。
Node 在進程啟動時就已經加載了 Buffer 模塊,并將其放在全局對象 global 上,使用時無須 require。
## Buffer 對象
Buffer 對象類似于數組,它的元素為 16 進制的兩位數,即 0 到 255 的數值
```js
let str = '深入淺出node.js'
let buf = new Buffer(str, 'utf-8')
console.log(buf)
// => <Buffer e6 b7 b1 e5 85 a5 e6 b5 85 e5 87 ba 6e 6f 64 65 2e 6a 73>
```
可以看到,不同編碼的字符串占用的元素個數不同,中文字在 UTF-8 編碼下占用 3 個元素,字母和半角標點符號占用 1 個元素。
Buffer 與 Array 類型很相似,可以訪問 length 屬性得到長度,也可以通過下標訪問元素,構造對象時也十分相似,代碼如下:
```js
let buf = new Buffer(100) // 分配一個長 100 字節的 Buffer 對象
console.log(buf.length) // 100
console.log(buf[10]) // 初始化為 0?
buf[10] = 100 // 可以通過下標進行賦值
console.log(buf[10]) // => 100
// 給元素的賦值如果小于 0,就將該值逐次加 256,直到得到一個 0 ~ 255 之間的整數;
// 如果得到的數值大于 255,就逐次減 256,直到得到 0 ~ 255 區間內的整數;
// 如果是小數,舍棄小數部分,只保留整數部分
buf[20] = -100
console.log(buf[20]) // 156
buf[21] = 300
console.log(buf[21]) // 44
buf[22] = 3.1415
console.log(buf[22]) // 3
```
## Buffer 內存分配
Buffer 對象的內存分配不是在 V8 的堆內存中,而是在 Node 的 C++ 層面實現內存的申請的。因為處理大量的字節數據不能采用需要一點內存就向操作系統申請一點內存的方式,這可能造成大量的內存申請的系統調用,對操作系統有一定壓力。為此 Node 在內存的使用上應用的是在 C++ 層面申請內存,在 JavaScript 中分配內存的策略。
為了高效地使用申請來的內存,Node 采用了 slab 分配機制,這里不做記錄了。
## Buffer 的轉換
Buffer 對象可以與字符串之間相互轉換,其支持的字符串編碼類型包括以下幾種:
- ASCII
- UTF-8
- UTF-16LE/UCS-2
- Base64
- Binary
- Hex
字符串轉 Buffer:通過構造函數完成,存儲的只能是一種編碼類型。
```js
new Buffer(str, [encoding]) // encoding 參數不傳遞時,默認按 UTF-8 編碼進行轉碼和存儲
```
Buffer 轉字符串:Buffer 對象的 toString() 方法可以將 Buffer 對象轉換為字符串:
```js
buf.toString([encoding], [start], [end])
// encoding 默認為 UTF-8
// start、end 可實現局部的轉換
```
## Buffer 的拼接
......
## Buffer 與性能
......
# 第 7 章 網絡編程
## 構建 TCP 服務
```js
// 構造 TCP 服務器
const net = require('net')
const server = net.createServer(function (socket) {
// 新的連接
socket.on('data', function (data) {
socket.write('你好')
})
socket.on('end', function () {
console.log('連接斷開')
})
socket.write('TCP 連接示例 \n')
})
server.listen(8124, function () {
console.log('server bound')
})
```
```js
// 構造 TCP 客戶端
const net = require('net')
const client = net.connect({ port: 8124 }, function () { // 'connect' listener
console.log('client connected')
client.write('world!\r\n')
})
client.on('data', function (data) {
console.log(data.toString())
client.end()
})
client.on('end', function () {
console.log('client disconnected')
})
```
TCP 服務的事件:在上面的示例中,代碼分為服務器事件和連接事件。
1.服務器事件,通過 net.createServer() 創建的服務器而言,它具有如下事件
- listening: 調用 server.listen() 后觸發
- connection:每個客戶端套接字連接到服務器端時觸發
- close:服務器關閉時觸發
- error:服務器發生異常時觸發
2.連接事件:服務器可以同時與多個客戶端保持連接,每個連接是典型的**可讀可寫 Stream 對象**,既可以通過 data 事件從一端讀取另一端發來的數據,也可以通過 write() 方法從一端向另一端發送數據,它具有如下事件:
- data:當一端調用 write() 發送數據時,另一端會觸發 data 事件,事件傳送的數據即是 write() 發送的數據
- end:當連接中的任意一端發送了 FIN 數據時,將會觸發該事件
- connect:與服務器端連接成功時觸發
- drain:當任意一端調用 write() 發送數據時,當前這端會觸發該事件
- error:異常發生時觸發
- close:套接字完全關閉時觸發
- timeout:當一定時間后連接不再活躍時,該事件將會被觸發,通知用戶當前該連接已經被閑置了
## 構建 UDP 服務
TCP 中所有的會話基于連接完成,UDP 中一個套接字可以與多個 UDP 服務通信,其提供面向事務的簡單不可靠信息傳輸服務,在網絡差的情況下存在丟包嚴重的問題,但是由于它無須連接,資源消耗低,處理快速且靈活,所有常用于那種即使丟失一兩個數據包也不會產生重大影響的場景,比如音頻、視頻等。DNS 服務也是基于 UDP 實現的。
```js
// 構建 UDP 服務器
// UDP 套接字一旦創建,既可以作為客戶端發送數據,也可以作為服務器端接收數據
const dgram = require('dgram')
const server = dgram.createSocket('udp4')
server.on('message', function (msg, rinfo) {
console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`)
})
server.on('listening', function () {
const address = server.address()
console.log(`server is listening at ${address.address}: ${address.port}`)
})
server.bind(41234)
// 該套接字將接收所有網卡上 41234 端口上的消息,綁定完成后將觸發 listening 事件
```
```js
// 構建 UDP 客戶端
const dgram = require('dgram')
const message = new Buffer('深入淺出Node.js')
const client = dgram.createSocket('udp4')
client.send(message, 0, message.length, 41234, 'localhost', function (err, bytes) {
client.close()
})
// socket.send(buf, offset, length, port, address, [callback])
/*
buf: 要發送的 Buffer
offset: Buffer 的偏移
length: Buffer 的長度
port: 目標端口
address: 目標地址
callback: 發送后的回調
*/
```
UDP 套接字事件:
- message:當 UDP 套接字偵聽網卡端口后,接收到消息觸發該事件
- listening:UDP 套接字開始偵聽時觸發該事件
- close:調用 close() 方法后觸發
- error:異常發生時觸發
## HTTP
### 1.HTTP 請求
報文頭第一行如 GET / HTTP/1.1 被解析之后分解為如下屬性:
- req.method:請求方法,值為 GET、POST、DELETE、PUT、CONNECT 等
- req.url:這里值為 /
- req.httpVersion:值為 1.1
其余報頭是很規律的 Key:Value 形式,被解析后放置在 req.headers 屬性上,例如
```js
headers:
{
'user-agent': ...,
host: '127.0.0.1:1337',
accept: '*/*'
}
```
報文體部分則抽象為一個只讀流對象,如果業務邏輯需要讀取報文體中的數據,則要在這個數據流結束后才能進行操作:
```js
function (req, res) {
let buffers = []
req.on('data', function (trunk) {
buffers.push(trunk)
}).on('end', function () {
let buffer = Buffer.concat(buffers)
// TODO
res.end('hello world')
})
}
```
這里的 request 對象和 response 對象都是相對較為底層的封裝,express 等框架在這兩個對象的基礎上進行了高層封裝。
### 2.HTTP 響應
HTTP 響應對象封裝了對底層連接的寫操作,可以將其看成一個可寫的流對象。它影響響應報文頭部信息的 API 為 res.setHeader() 和 res.writeHead()。
我們可以調用 setHeader 進行多次設置,但只有調用 writeHead 后,報頭才會寫入到連接中。
報文體部分則是調用 res.write() 和 res.end() 方法實現的,其差別在于 res.end() 會先調用 write() 發送數據,然后發送信號告知服務器這次響應結束。
需要注意以下幾點:
- 報頭是在報文體發送前發送的,一旦開始了數據的發送,writeHead() 和 setHeader() 將不再生效,這是由協議的特性決定的
- 務必調用 res.end() 以結束請求,否則客戶端將一直處于等待的狀態。當然,也可以通過延遲 res.end() 的方式實現客戶端與服務器之間的長連接
### 3.HTTP 服務的事件
- connection:開啟 HTTP 請求和響應前,客戶端與服務器需要建立底層的 TCP 連接,該連接建立時觸發一次 connection 事件
- request:服務器解析出 HTTP 請求頭后觸發
- close:調用 server.close() 后觸發
- checkContinue:某些客戶端再發送較大的數據時,并不會將數據直接發送,而是先發送一個頭部帶 Expect:100-continue 的請求到服務器,服務器將會觸發 checkContinue 事件;如果沒有為服務器監聽這個事件,服務器將會自動響應客戶端 100 Continue 的狀態碼,表示接收數據上傳;如果不接受較大的數據,響應客戶端 400 Bad Request 拒絕客戶端繼續發送即可。需要注意的是,當該事件發生時不會觸發 request 事件,兩個事件之間互斥。當客戶端收到 100 Continue 后重新發送請求時,才會觸發 request 事件。
- connect:當客戶端發起 CONNECT 請求時觸發,發起 CONNECT 請求通常在 HTTP 代理時出現;如果不監聽該事件,發起該請求的連接將會關閉。
- upgrade:當客戶端要求升級連接的協議時,需要和服務器協商,客戶端會在請求中帶上 Upgrade 字段,服務器會在接收到這樣的請求時觸發該事件,如果不監聽該事件,發起該請求的連接將會關閉。(對 WebSocket 很重要)
- clientError:連接的客戶端發 error 事件時,這個錯誤會傳送到服務器端,觸發該事件。
### 4.HTTP 客戶端
```js
const options = {
hostname: '127.0.0.1',
port: 1334,
path: '/',
method: 'GET'
}
/*
options 參數決定了 HTTP 請求頭中的內容
host: 服務器的域名或 IP 地址,默認 localhsot
hostname: 服務器名稱
port: 服務器端口,默認 80
localAddress: 建立網絡連接的本地網卡
socketPath: Domain 套接字路徑
method: HTTP 請求方法,默認 GET
path: 請求路徑,默認 /
headers: 請求頭對象
auth: Basic 認證,這個值將被計算成請求頭中的 Authorization 部分
*/
const req = http.request(options, function (res) {
console.log(`STATUS: ${res.statusCode}`)
console.log(`HEADERS: ${JSON.stringify(res.headers)}`)
res.setEncoding('utf-8')
res.on('data', function (chunk) {
console.log(chunk)
})
})
```
HTTP 客戶端事件:
- response
- socket
- connect
- upgrade
- continue
## 構建 WebSocket 服務
- WebSocket 客戶端基于事件的編程模型與 Node 中自定義事件相差無幾
- WebSocket 實現了客戶端與服務器端之間的長連接,而 Node 事件驅動的方式十分擅長與大量的客戶端保持高并發連接
WebSocket 與傳統 HTTP 相比有如下好處:
- 客戶端與服務器端只建立一個 TCP 連接,可以使用更少的連接
- WebSocket 服務器端可以推送數據到客戶端,這遠比 HTTP 請求響應模式更靈活高效
- 有更輕量級的協議頭,減少數據傳送量
WebScoket 協議主要分為兩個部分:握手和數據傳輸
客戶端建立連接時,通過 HTTP 發起請求報文,如下所示:
```shell
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
```
與普通的 HTTP 協議略有區別的部分在于如下這些請求頭:
```js
Upgrade: websocket
Connection: Upgrade
// 以上表示請求服務器端升級協議為 WebSocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 用于安全校驗
Sec-WebSocket-Protocol: chat, superchat // 子協議
Sec-WebSocket-Version: 13 // 版本號
```
服務端在處理完請求后,響應如下報文:告知客戶端正在更換協議,更新應用層協議為 WebSocket 協議,并在當前的套接字上應用新協議
```shell
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocal: caht
```
一旦 WebSocket 握手成功,服務器端與客戶端將會呈現對等的效果,都能接收和發送消息。
*****
在握手順利完成后,當前連接將不再進行 HTTP 的交互,而是開始 WebSocket 的數據幀協議。

```js
// 實例化一個 WebSocket 對象,并傳入要連接的 URL
var socket = new WebSocket('url') // url 中要使用 ws:// 來代替 http:// ; 使用 wss 來代替 https://
// 成功建立連接時會觸發 open 事件
socket.onopen = function () {
console.log('established')
}
// 發生錯誤時會觸發 error 事件
socket.onerror = function () {
console.log('error')
}
// 當連接關閉時會觸發 close 事件
socket.onclose = function () {
console.log('closed!')
}
// 使用 send() 方法發送數據,只能接受字符串,json 對象要先序列化成 json 字符串
socket.send(str)
// 當服務器端向客戶端發來消息,WebSocket 對象就會觸發 message 事件
socket.onmessage = function (event) {
console.log(event.data)
}
// 調用 close() 方法,會關閉 WebScoket 連接
socket.close()
```
使用的話還是建議直接用社區的 socket.io 比較好。
## HTTPS
Node 在網絡安全上提供了 3 個模塊,分別是 crypto、tls、https。其中 crypto 主要用于加密解密,SHA1、MD5 等加密算法都在其中有體現;tls 模塊用于建立 TLS/SSL 加密的 TCP 連接;https 模塊與 http 模塊接口一致,區別僅在于它建立于安全的連接上。
> TLS(Transport Layer Security,安全傳輸層協議)可以認為就是 SSL(Secure Socket Layer,安全套階層)的標準化后的名稱,在傳輸層提供對網絡連接加密的功能
中間人攻擊:客戶端與服務器端在交換公鑰的過程中,中間人對客戶端扮演服務器的角色,對服務器端扮演客戶端的角色,因此客戶端和服務器端幾乎感受不到中間人的存在。

因此,需要引入數字證書來對公鑰進行驗證。
數字證書中包含了服務器的名稱和主機名、服務器的公鑰、簽名頒發機構的名稱、來自簽名頒發機構的簽名。在連接建立前,會通過證書中的簽名確認收到的公鑰是來自目標服務器的,從而產生信任關系。
CA(Certificate Authority,數字證書認證中心)的作用是為站點頒發證書,且這個證書中具有 CA 通過自己的公鑰和私鑰實現的簽名。
為了得到簽名證書,服務器端需要通過自己的私鑰生成 CSR(Certificate Signing Request,證書簽名請求)文件。CA 機構將通過這個文件頒發屬于該服務器端的簽名證書,只要通過 CA 機構就能驗證證書是否合法。
通過 CA 機構頒發證書是一個煩瑣的過程,需要付出一定的經理和費用,對于中小型企業,多半是采用自簽名證書來構建安全網絡的。所謂自簽名證書,就是自己扮演 CA 機構,給自己的服務器端頒發簽名證書。
......
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs