不少同窗或多或少都使用過 Node 建立 HTTP Server 處理 HTTP 請求,多是簡易的博客,或者已是負載千萬級請求的大型服務。可是咱們可能並無深刻了解過 Node 建立 HTTP Server 的過程,但願借這篇文章,讓你們對 Node 更加了解。html
先上流程圖,幫助你們更容易的理解源碼node
咱們先看一個簡單的建立 HTTP Server 的例子,基本的過程能夠分爲兩步linux
createServer
獲取 server
對象server.listen
開啓監聽服務const http = require('http')
// 建立 server 對象
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('響應內容');
});
// 開始監聽 3000 端口的請求
server.listen(3000)
複製代碼
這個過程是很是簡單,下面咱們會根據這個過程,結合源碼,開始分析 Node 建立 HTTP Server 的具體內部過程。git
在此以前,爲了更好的理解代碼,咱們須要瞭解一些基本的概念:github
fd - 文件描述符數據庫
文件描述符(File descriptor)是一個用於表述指向文件的引用的抽象化概念。文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的,該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。服務器
handle - 句柄app
句柄(handle)是 Windows 操做系統用來標識被應用程序所創建或使用的對象的整數。其本質至關於帶有引用計數的智能指針。當一個應用程序要引用其餘系統(如數據庫、操做系統)所管理的內存塊或對象時,可使用句柄。Unix 系統的文件描述符基本上也屬於句柄。socket
本文中的 handle
能夠理解爲相關對象的引用。async
文中用 ...
符合表示略去了部分和本文討論內容關聯性較低的,不影響主要邏輯的代碼,如參數處理、屬性賦值等。
createServer
是個工廠方法,返回了 _http_server
模塊中的 Server
類的實例,而 Server
是從 _http_server
文件導出的
const {
Server,
} = require('_http_server');
// http.createServer
function createServer(opts, requestListener) {
return new Server(opts, requestListener);
}
複製代碼
從 _http_server
模塊的 Server
類中能夠看出,http.Server
是繼承於 net.Server
的
function Server(options, requestListener) {
// 能夠不使用 new 直接調用 http.Server()
if (!(this instanceof Server)) return new Server(options, requestListener);
// 參數適配
// ...
// 繼承
net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.on('request', requestListener);
}
// ...
this.on('connection', connectionListener);
// ...
}
// http.Server 繼承自 net.Server
ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
ObjectSetPrototypeOf(Server, net.Server);
...
複製代碼
這裏的繼承關係也比較好理解:Node 中的 net.Server
是用於建立 TCP 或 IPC 服務器的模塊。咱們都知道,HTTP
是應用層協議,而 TCP
是傳輸層協議。HTTP
經過 TCP
傳輸數據,並進行再次的解析。Node 中的 HTTP 模塊基於 TCP 模塊作了再封裝,實現了不一樣的解析處理邏輯,即出現了咱們看到的繼承關係。
相似的,net.Server
繼承了 EventEmitter
類,擁有許多事件觸發器,包含一些屬性信息,感興趣的同窗能夠自行查閱。
至此,咱們能夠看到,createServer
只是 net.Server
的實例化過程,並無建立服務監聽,而是由 server.listen
方法實現。
當建立完成 server
實例後,一般須要調用 server.listen
方法啓動服務,開始處理請求,如 Koa 的 app.listen
。listen
方法支持多種使用方式,下面咱們一一分析
server.listen(handle[, backlog][, callback])
第一種是不太常見的用法,Node 容許咱們啓動一個服務器,監聽已經綁定到端口、Unix 域套接字或 Windows 命名管道的,給定的 handle
上的鏈接。
handle
對象能夠是服務器、套接字(任何具備底層 _handle 成員的東西),也能夠是具備 fd
(文件描述符) 屬性的對象,如咱們經過 createServer
建立的 Server 對象。
當識別到是 handle 對象以後,就會調用 listenInCluster
方法,從方法的名字,咱們能夠猜想到這個就是啓動服務監聽的方法:
// handle 是具備 _handle 屬性的對象
if (options instanceof TCP) {
this._handle = options;
this[async_id_symbol] = this._handle.getAsyncId();
listenInCluster(this, null, -1, -1, backlogFromArgs);
return this;
}
// 當 handle 是具備 fd 屬性的對象
if (typeof options.fd === "number" && options.fd >= 0) {
listenInCluster(this, null, null, null, backlogFromArgs, options.fd);
return this;
}
複製代碼
server.listen([port[, host[, backlog]]][, callback])
第二種是咱們常見的監聽端口,Node 容許咱們建立一個服務器,監聽給定的 host 上的端口,host 能夠是 IP 地址,或者域名連接,當 host 是域名連接時,Node 會先使用 dns.lookup
獲取 IP 地址。最後,檢驗完端口合法後,一樣是調用了 listenInCluster
方法,源碼🔗。
[server.listen(path[, backlog][, callback])](http://nodejs.cn/s/yW8Zc1)
第三種,Node 容許啓動一個 IPC 服務器監聽指定的 IPC 路徑,即 Windows 上的命名管道 IPC以及 其餘類 Unix 系統中的 Unix Domain Socket。
這裏的 path
參數是識別 IPC 鏈接的路徑。 在 Unix 系統上,參數 path
表現爲文件系統路徑名,在 Windows 上,path
必須是以 \\?\pipe\
或 \\.\pipe\
爲入口。
而後,一樣是調用了 listenInCluster
方法,源碼🔗。
還有一種調用方法 server.listen(options[, callback])
是端口和 IPC 路徑的另一種調用方式,這裏就很少說了。
最後就是對不符合上述全部條件的異常狀況,拋出錯誤。
至此,咱們能夠看到,server.listen
方法對不一樣的調用方式作了解析,並調用了 listenInCluster
方法。
首先,咱們要對 clsuter 作一個簡單的介紹。
咱們都知道 JavaScript 是單線程運行的,一個線程只會在一個 CPU 核心上運行。而現代的處理都是多核心的,爲了充分利用多核,就須要啓用多個 Node.js 進程去處理負載任務。
Node 提供的 cluster
模塊解決了這個問題 ,咱們可使用 cluster
建立多個進程,而且同時監聽同一個端口
,而不會發生衝突,是否是很神奇?不要着急,下面咱們就會解密這個神奇的 cluster
模塊。
先看一個 cluster
的簡單用法:
const cluster = require('cluster');
const http = require('http');
if (cluster.isMaster) {
// 衍生工做進程。
for (let i = 0; i < 4; i++) {
cluster.fork();
}
} else {
// 工做進程能夠共享任何 TCP 鏈接。
// 在本例子中,共享的是 HTTP 服務器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
}
複製代碼
基於 cluster
的用法,負責啓動其餘進程的叫作 master
進程,不作具體的工做,只負責啓動其餘進程。其餘被啓動的進程則叫 worker
進程,它們接收請求,並對外提供服務。
listenInCluster
方法主要作了一件事:區分 master
進程(cluster.isMaster)和 worker
進程,採用不一樣的處理策略:
master
進程:直接調用 server._listen
啓動監聽worker
進程:使用 clsuter._getServer
處理傳入的 server
對象,修改 server._handle
再調用了 server._listen
啓動監聽function listenInCluster(...) {
// 引入 cluster 模塊
if (cluster === undefined) cluster = require('cluster');
// master 進程
if (cluster.isMaster || exclusive) {
server._listen2(address, port, addressType, backlog, fd, flags);
return;
}
// 非 master 進程,即經過 cluster 啓動的子進程
const serverQuery = {
address: address,
port: port,
addressType: addressType,
fd: fd,
flags,
};
// 調用 cluster 的方法處理
cluster._getServer(server, serverQuery, listenOnMasterHandle);
function listenOnMasterHandle(err, handle) {
// ...
server._handle = handle;
server._listen2(address, port, addressType, backlog, fd, flags);
}
}
複製代碼
咱們先看 master
進程的處理方法 server._listen2
,server._listen2
是 setupListenHandle
的別名。
setupListenHandle
主要是負責根據 server
監聽鏈接的不一樣類型,調用 createServerHandle
方法獲取 handle
對象,並調用 handle.listen
方法開啓監聽。
function setupListenHandle(address, port, addressType, backlog, fd, flags) {
// 若是是 handle 對象,須要創一個 handle 對象
if (this._handle) {
// do nothing
} else {
let rval = null;
// 在 host 和 port 省略,且沒有指定 fd 的狀況下
// 若是 IPv6 可用,服務器將會接收基於未指定的 IPv6 地址 (::) 的鏈接
// 不然接收基於未指定的 IPv4 地址 (0.0.0.0) 的鏈接。
if (!address && typeof fd !== 'number') {
rval = createServerHandle(DEFAULT_IPV6_ADDR, port, 6, fd, flags);
if (typeof rval === 'number') {
rval = null;
address = DEFAULT_IPV4_ADDR;
addressType = 4;
} else {
address = DEFAULT_IPV6_ADDR;
addressType = 6;
}
}
// fd 或 IPC
if (rval === null)
rval = createServerHandle(address, port, addressType, fd, flags);
// 若是 createServerHandle 返回的是數字,則代表出現了錯誤,進程退出
if (typeof rval === 'number') {
const error = uvExceptionWithHostPort(rval, 'listen', address, port);
process.nextTick(emitErrorNT, this, error);
return;
}
this._handle = rval;
}
...
// 開始監聽
const err = this._handle.listen(backlog || 511);
...
// 觸發 listening 方法
}
複製代碼
createServerHandle
負責調用 C++
中 tcp_warp.cc
和 pipe_wrap
模塊建立 PIPE
和 TCP
服務。PIPE
和 TCP
對象都擁有 listen
方法,listen
方法是對 uvlib
中的 [uv_listen](http://docs.libuv.org/en/v1.x/stream.html?highlight=uv_listen#c.uv_listen)
方法的封裝,與 Linux 中的 [listen(2)](https://man7.org/linux/man-pages/man2/listen.2.html)
相似。能夠調用系統能力,開始監聽傳入的鏈接,並在收到新鏈接後回調請求信息。
PIPE
是對 Unix 上的流文件(包括 socket,pipes)以及 Windows 上的命名管道的抽象封裝,TCP
就是對 TCP 服務的封裝。
function createServerHandle(address, port, addressType, fd, flags) {
// ...
let isTCP = false;
// 當 fd 選項存在時
if (typeof fd === 'number' && fd >= 0) {
try {
handle = createHandle(fd, true);
} catch (e) {
debug('listen invalid fd=%d:', fd, e.message);
// uvlib 中的錯誤碼,表示非法的參數,是個負數
return UV_EINVAL;
}
...
} else if (port === -1 && addressType === -1) {
// 當 port 和 address 不存在時,即監聽 Socket 或 IPC 等
// 建立 Pipe Server
handle = new Pipe(PipeConstants.SERVER);
...
} else {
// 建立 TCB SERVER
handle = new TCP(TCPConstants.SERVER);
isTCP = true;
}
// ...
return handle;
}
複製代碼
master
進程的 server.listen
處理邏輯較爲簡單,能夠歸納爲直接調用 libuv
,使用系統能力,開啓監聽服務。
若是當前進程不是 master
進程,事情就會變得複雜許多。
listenInCluster
方法會調用 cluster
模塊導出的 _getServer
方法,cluster
模塊會經過當前進程是否包含 NODE_UNIQUE_ID
判斷當前進程是否子進程,分別使用 child
或 master
文件的導出變量,相應的處理方法也會有所不一樣
const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);
複製代碼
咱們所說的 worker
進程,沒有 NODE_UNIQUE_ID
環境變量,會使用 child
模塊導出的 _getServer
方法。
worker
進程的 _getServer
方法主要作了如下兩件事情:
internalMessage
,即進程間通訊的方式,向 master
進程傳遞消息,調用 queryServe
,註冊當前 worker
進程的信息。若 master
進程是第一次接收到監聽此端口/fd 的 worker
,則起一個內部 TCP 服務器,來承擔監聽該端口/fd 的職責,隨後在 master
中記錄下該 worker
。worker
進程中的 net.Server
實例的 listen
方法裏監聽端口/fd的部分,使其再也不承擔監聽職責。// obj 是 net.Server 或 Socket 的實例
cluster._getServer = function(obj, options, cb) {
let address = options.address;
// ...
// const indexesKey = ...;
// indexes 爲 Map 對象
indexes.set(indexesKey, index);
const message = {
act: 'queryServer',
index,
data: null,
...options
};
message.address = address;
// 發送 internalMessage 通知 Master 進程
// 接受 Master 進程的回調
send(message, (reply, handle) => {
if (typeof obj._setServerData === 'function')
obj._setServerData(reply.data);
if (handle)
// 關閉鏈接時,移除 handle 避免內存泄漏
shared(reply, handle, indexesKey, cb); // Shared listen socket.
else
// 僞造了 listen 等方法
rr(reply, indexesKey, cb); // Round-robin.
});
// ...
};
複製代碼
master
中的 queryServer
接收到到消息後,會根據不一樣的條件(平臺、協議等)分別建立 RoundRobinHandle
和 SharedHandle
,即 cluster
兩種分發處理鏈接的方法。
同時 master
進程會將監聽端口、地址等信息組成的 key
做爲惟一標誌,記錄 handle
和對應 worker
的信息。
function queryServer(worker, message) {
// ...
const key = `${message.address}:${message.port}:${message.addressType}:` +
`${message.fd}:${message.index}`;
let handle = handles.get(key);
if (handle === undefined) {
let address = message.address;
...
let constructor = RoundRobinHandle;
if (schedulingPolicy !== SCHED_RR ||
message.addressType === 'udp4' ||
message.addressType === 'udp6') {
constructor = SharedHandle;
}
handle = new constructor(key, address, message);
handles.set(key, handle);
}
// ...
handle.add(worker, (errno, reply, handle) => {
const { data } = handles.get(key);
// ...
send(worker, {
errno,
key,
ack: message.seq,
data,
...reply
}, handle);
});
}
複製代碼
RoundRobinHandle
(也是除 Windows 外全部平臺的默認方法)的處理模式爲:由 master
進程負責監聽端口,接收新鏈接後再將鏈接循環分發給 worker
進程,即將請求放到一個隊列中,從空閒的 worker
池中分出一個處理請求,處理完成後在放回 worker
池中,以此類推
function RoundRobinHandle(key, address, { port, fd, flags }) {
this.key = key;
this.all = new Map();
this.free = new Map();
this.handles = [];
this.handle = null;
// 建立 Server
this.server = net.createServer(assert.fail);
// 開啓監聽,多種狀況,省略
// this.server.listen(...)
this.server.once('listening', () => {
this.handle = this.server._handle;
// 收到請求,分發處理
this.handle.onconnection = (err, handle) => this.distribute(err, handle);
this.server._handle = null;
this.server = null;
});
}
// ...
RoundRobinHandle.prototype.distribute = function(err, handle) {
this.handles.push(handle);
const [ workerEntry ] = this.free;
if (ArrayIsArray(workerEntry)) {
const [ workerId, worker ] = workerEntry;
this.free.delete(workerId);
this.handoff(worker);
}
};
RoundRobinHandle.prototype.handoff = function(worker) {
if (!this.all.has(worker.id)) {
return; // Worker is closing (or has closed) the server.
}
const handle = this.handles.shift();
if (handle === undefined) {
this.free.set(worker.id, worker); // Add to ready queue again.
return;
}
const message = { act: 'newconn', key: this.key };
sendHelper(worker.process, message, handle, (reply) => {
if (reply.accepted)
handle.close();
else
this.distribute(0, handle);
this.handoff(worker);
});
};
複製代碼
SharedHandle
的處理模式爲:master
進程建立監聽服務器 ,再將服務器的 handle
發送 worker
進程,由 worker
進程負責直接接收鏈接
function SharedHandle(key, address, { port, addressType, fd, flags }) {
this.key = key;
this.workers = new Map();
this.handle = null;
this.errno = 0;
let rval;
if (addressType === 'udp4' || addressType === 'udp6')
rval = dgram._createSocketHandle(address, port, addressType, fd, flags);
else
rval = net._createServerHandle(address, port, addressType, fd, flags);
if (typeof rval === 'number')
this.errno = rval;
else
this.handle = rval;
}
// 添加存儲 worker 信息
SharedHandle.prototype.add = function(worker, send) {
assert(!this.workers.has(worker.id));
this.workers.set(worker.id, worker);
// 向 worker 進程發送 handle
send(this.errno, null, this.handle);
};
// ..
複製代碼
PS:Windows 之因此不採用 RoundRobinHandle 的緣由是由於性能緣由。從理論上來講,第二種方法應該是效率最佳的。 但在實際狀況下,因爲操做系統調度機制的難以捉摸,會使分發變得不穩定,可能會出現八個進程中有兩個分擔了 70% 的負載。相比而言,輪訓的方法會更加高效。
在 worker
進程中,每一個 worker
再也不獨立開啓監聽服務,而是由 master
進程開啓一個統一的監聽服務,接受請求鏈接,再將請求轉發給 worker
進程處理。
在不一樣的狀況下,Node 建立 HTTP Server 的流程是不一致的。當進程爲 master
進程時,Node 會直接經過 libuv
調用系統能力開啓監聽。當進程爲 child
進程(worker
進程)時,Node 會使用 master
進程開啓間監聽,並經過輪訓或共享 Handle 的方式將鏈接分發給 child
進程處理。
最後,寫文章不容易,若是你們喜歡的話,歡迎一鍵三聯~