若是你們使用 Node.js 寫過 web 應用,那麼你必定使用過 http
模塊。在 Node.js 中,起一個 HTTP server 十分簡單,短短數行便可:node
'use stirct' const { createServer } = require('http') createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }) res.end('Hello World\n') }) .listen(3000, function () { console.log('Listening on port 3000') })
$ curl localhost:3000 Hello World
就這麼簡單,由於 Node.js 把許多細節都已在源碼中封裝好了,主要代碼在 lib/_http_*.js
這些文件中,如今就讓咱們照着上述代碼,看看從一個 HTTP 請求的到來直到響應,Node.js 都爲咱們在源碼層作了些什麼。git
在 Node.js 中,若要收到一個 HTTP 請求,首先須要建立一個 http.Server
類的實例,而後監聽它的 request
事件。因爲 HTTP 協議屬於應用層,在下層的傳輸層一般使用的是 TCP 協議,因此 net.Server
類正是 http.Server
類的父類。具體的 HTTP 相關的部分,是經過監聽 net.Server
類實例的 connection
事件封裝的:github
// lib/_http_server.js // ... function Server(requestListener) { if (!(this instanceof Server)) return new Server(requestListener); net.Server.call(this, { allowHalfOpen: true }); if (requestListener) { this.addListener('request', requestListener); } // ... this.addListener('connection', connectionListener); // ... } util.inherits(Server, net.Server);
這時,則須要一個 HTTP parser 來解析經過 TCP 傳輸過來的數據:web
// lib/_http_server.js const parsers = common.parsers; // ... function connectionListener(socket) { // ... var parser = parsers.alloc(); parser.reinitialize(HTTPParser.REQUEST); parser.socket = socket; socket.parser = parser; parser.incoming = null; // ... }
值得一提的是,parser 是從一個「池」中獲取的,這個「池」使用了一種叫作 free list(wiki)的數據結構,實現很簡單,我的以爲是爲了儘量的對 parser 進行重用,並避免了不斷調用構造函數的消耗,且設有數量上限(http
模塊中爲 1000
):緩存
// lib/freelist.js 'use strict'; exports.FreeList = function(name, max, constructor) { this.name = name; this.constructor = constructor; this.max = max; this.list = []; }; exports.FreeList.prototype.alloc = function() { return this.list.length ? this.list.pop() : this.constructor.apply(this, arguments); }; exports.FreeList.prototype.free = function(obj) { if (this.list.length < this.max) { this.list.push(obj); return true; } return false; };
因爲數據是從 TCP 不斷推入的,因此這裏的 parser 也是基於事件的,很符合 Node.js 的核心思想。使用的是 http-parser 這個庫:數據結構
// lib/_http_common.js // ... const binding = process.binding('http_parser'); const HTTPParser = binding.HTTPParser; const FreeList = require('internal/freelist').FreeList; // ... var parsers = new FreeList('parsers', 1000, function() { var parser = new HTTPParser(HTTPParser.REQUEST); // ... parser[kOnHeaders] = parserOnHeaders; parser[kOnHeadersComplete] = parserOnHeadersComplete; parser[kOnBody] = parserOnBody; parser[kOnMessageComplete] = parserOnMessageComplete; parser[kOnExecute] = null; return parser; }); exports.parsers = parsers; // lib/_http_server.js // ... function connectionListener(socket) { parser.onIncoming = parserOnIncoming; }
因此一個完整的 HTTP 請求從接收到徹底解析,會挨個經歷 parser 上的以下事件監聽器:app
parserOnHeaders
:不斷解析推入的請求頭數據。curl
parserOnHeadersComplete
:請求頭解析完畢,構造 header 對象,爲請求體建立 http.IncomingMessage
實例。socket
parserOnBody
:不斷解析推入的請求體數據。函數
parserOnExecute
:請求體解析完畢,檢查解析是否報錯,若報錯,直接觸發 clientError
事件。若請求爲 CONNECT 方法,或帶有 Upgrade 頭,則直接觸發 connect
或 upgrade
事件。
parserOnIncoming
:處理具體解析完畢的請求。
因此接下來,咱們的關注點天然是 parserOnIncoming
這個監聽器,正是這裏完成了最終 request
事件的觸發,關鍵步驟代碼以下:
// lib/_http_server.js // ... function connectionListener(socket) { var outgoing = []; var incoming = []; // ... function parserOnIncoming(req, shouldKeepAlive) { incoming.push(req); // ... var res = new ServerResponse(req); if (socket._httpMessage) { // 這裏判斷若爲真,則說明 socket 正在被隊列中以前的 ServerResponse 實例佔用 outgoing.push(res); } else { res.assignSocket(socket); } res.on('finish', resOnFinish); function resOnFinish() { incoming.shift(); // ... var m = outgoing.shift(); if (m) { m.assignSocket(socket); } } // ... self.emit('request', req, res); } }
能夠看出,對於同一個 socket 發來的請求,源碼中分別維護了兩個隊列,用於緩衝 IncomingMessage
實例和對應的 ServerResponse
實例。先來的 ServerResponse
實例先佔用 socket ,監聽其 finish
事件,從各自隊列中釋放該 ServerResponse
實例和對應的 IncomingMessage
實例。
比較繞,以一個簡化的圖示來總結這部分邏輯:
到了響應時,事情已經簡單許多了,傳入的 ServerResponse
已經獲取到了 socket。http.ServerResponse
繼承於一個內部類 http.OutgoingMessage
,當咱們調用 ServerResponse#writeHead
時,Node.js 爲咱們拼湊好了頭字符串,並緩存在 ServerResponse
實例內部的 _header
屬性中:
// lib/_http_outgoing.js // ... OutgoingMessage.prototype._storeHeader = function(firstLine, headers) { // ... if (headers) { var keys = Object.keys(headers); var isArray = Array.isArray(headers); var field, value; for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; if (isArray) { field = headers[key][0]; value = headers[key][1]; } else { field = key; value = headers[key]; } if (Array.isArray(value)) { for (var j = 0; j < value.length; j++) { storeHeader(this, state, field, value[j]); } } else { storeHeader(this, state, field, value); } } } // ... this._header = state.messageHeader + CRLF; }
緊接着在調用 ServerResponse#end
時,將數據拼湊在頭字符串後,添加對應的尾部,推入 TCP ,具體的寫入操做在內部方法 ServerResponse#_writeRaw
中:
// lib/_http_outgoing.js // ... OutgoingMessage.prototype.end = function(data, encoding, callback) { // ... if (this.connection && data) this.connection.cork(); var ret; if (data) { this.write(data, encoding); } if (this._hasBody && this.chunkedEncoding) { ret = this._send('0\r\n' + this._trailer + '\r\n', 'binary', finish); } else { ret = this._send('', 'binary', finish); } if (this.connection && data) this.connection.uncork(); // ... return ret; } OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) { if (typeof encoding === 'function') { callback = encoding; encoding = null; } var connection = this.connection; // ... return connection.write(data, encoding, callback); };
到這,一個請求就已經經過 TCP ,發回給客戶端了。其實本文中,只涉及到了一條主線進行解析,源碼中還考慮了更多的狀況,如超時,socket 被佔用時的緩存,特殊頭,上游忽然出現問題,更高效的已寫頭的查詢等等。很是值得一讀。
參考: