深入學習 Node.js Http
響應報文示例
HTTP/1.1 200 OK Server: bfe/1.0.8.18 Date: Thu, 30 Mar 2017 12:28:00 GMT Content-Type: text/html; charset=utf-8 Connection: keep-alive Cache-Control: private Expires: Thu, 30 Mar 2017 12:27:43 GMT
Expect 請求頭
Expect 是一個請求訊息頭,包含一個期望條件,表示伺服器只有在滿足此期望條件的情況下才能妥善地處理請求。規範中只規定了一個期望條件,即Expect: 100-continue
,對此伺服器可以做出如下回應:
-
ofollow,noindex">
100
:表示訊息頭中的期望條件可以得到滿足,請求可以順利進行。 -
417
(Expectation Failed) 表示伺服器不能滿足期望的條件,也可以是其他任意表示客戶端錯誤的狀態碼(4xx)。
常見的瀏覽器不會發送Expect
訊息頭,但是其他型別的客戶端如 cURL 預設會這麼做。目前規範中只規定了Expect: 100-continue
這一個期望條件。100-continue 握手的目的是允許客戶端在傳送包含請求體的訊息前,判斷源伺服器是否願意在客戶端傳送請求體前接收請求。
在實際開發過程中,需謹慎使用 Expect: 100-continue,因為如果遇到不支援 HTTP/1.1協議的伺服器或代理伺服器可能會引起問題。
FreeList
在 Node.js 中為了避免頻繁建立和銷燬物件,實現了一個通用的 FreeList 機制。在 http 模組中,就利用到了 FreeList 機制,即用來動態管理 HTTPParser 物件:
var parsers = new FreeList('parsers', 1000, function() { var parser = new HTTPParser(HTTPParser.REQUEST); //... }
是不是感覺很高大尚,其實 FreeList 的內部實現很簡單,具體如下:
class FreeList { constructor(name, max, ctor) { this.name = name; // 管理的物件名稱 this.ctor = ctor; // 管理物件的建構函式 this.max = max; // 儲存物件的最大值 this.list = []; // 儲存物件的陣列 } alloc() { return this.list.length ? this.list.pop() : this.ctor.apply(this, arguments); } free(obj) { if (this.list.length < this.max) { this.list.push(obj); return true; } return false; } }
在處理 HTTP 請求的場景下,當新的請求到來時,我們通過呼叫parsers.alloc()
方法來獲取 HTTPParser 物件,從而解析 HTTP 請求。當完成 HTTP 解析任務後,我們可以通過呼叫parsers.free()
方法來歸還 HTTPParser 物件。
IncomingMessage
在 Node.js 伺服器接收到請求時,會利用 HTTPParser 物件來解析請求報文,為了便於開發者使用,Node.js 會基於解析後的請求報文建立 IncomingMessage 物件,IncomingMessage 建構函式(程式碼片段)如下:
function IncomingMessage(socket) { Stream.Readable.call(this); this.socket = socket; this.connection = socket; this.httpVersion = null; this.complete = false; this.trailers = {}; this.headers = {}; // 解析後的請求頭 this.rawHeaders = []; // 原始的頭部資訊 // request (server) only this.url = ''; // 請求url地址 this.method = null; // 請求地址 } util.inherits(IncomingMessage, Stream.Readable);
Http 協議是基於請求和響應,請求物件我們已經介紹了,那麼接下來就是響應物件。在 Node.js 中,響應物件是 ServerResponse 類的例項。
ServerResponse
function ServerResponse(req) { OutgoingMessage.call(this); if (req.method === 'HEAD') this._hasBody = false; this.sendDate = true; this._sent100 = false; this._expect_continue = false; if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te); this.shouldKeepAlive = false; } } util.inherits(ServerResponse, OutgoingMessage);
通過以上程式碼,我們可以發現 ServerResponse 繼承於 OutgoingMessage。在 OutgoingMessage 物件中會包含用於生成響應報文的相關資訊,這裡就不詳細展開,有興趣的小夥伴可以檢視_http_outgoing.js
檔案。
Node.js Http
Http 基本使用
simple_server.js
const http = require("http"); const server = http.createServer((req, res) => { res.end("Hello Semlinker!"); }); server.listen(3000, () => { console.log("server listen on 3000"); });
當執行完node simple_server.js
命令後,你可以通過http://localhost:3000/
這個 url 地址來訪問我們本地的伺服器。不出意外的話,你將在開啟的頁面中看到 “Hello Semlinker!”。
雖然以上的示例很簡單,但對於之前沒有服務端經驗或者剛接觸 Node.js 的小夥伴來說,可能會覺得這是一個很神奇的事情。接下來我們來通過以上簡單的示例,分析一下 Node.js 的 Http 模組。
Http 伺服器
顯而易見,http.createServer()
方法用來建立伺服器,該方法的實現如下:
function createServer(requestListener) { return new Server(requestListener); }
在createServer
函式內部,我們通過呼叫 Server 建構函式來建立伺服器。因此,接下來的重點就是分析 Server 構造函數了,該函式的內部實現如下:
function Server(options, requestListener) { if (!(this instanceof Server)) return new Server(options, requestListener); if (typeof options === 'function') { requestListener = options; options = {}; } else if (options == null || typeof options === 'object') { options = util._extend({}, options); } net.Server.call(this, { allowHalfOpen: true }); if (requestListener) { this.on('request', requestListener); } this.on('connection', connectionListener); this.timeout = 2 * 60 * 1000; // 設定超時時間 } util.inherits(Server, net.Server);
看到this.on('request',requestListener)
和this.on('connection',connectionListener)
這兩行,不知道小夥伴們有沒有想起我們的 EventEmitter。如果對它還不瞭解的小夥伴,可以參考之前的文章 ——深入學習 Node.js EventEmitter
。
通過以上原始碼,目前我們得出了一個結論,在觸發request
事件後,就會呼叫我們設定的 requestListener 函式,即執行以下程式碼:
(req, res) => { res.end("Hello Semlinker!"); }
那麼什麼時候會觸發 request 事件呢?而 connection 事件和 connectionListener 又是什麼?帶著這些問題,我們來繼續學習 Http 模組。
connection 事件,顧名思義用來跟蹤網路連線。這裡,我們重點來看一下 connectionListener 函式:
function connectionListener(socket) { defaultTriggerAsyncIdScope( getOrSetAsyncId(socket), connectionListenerInternal, this, socket ); }
該函式內竟然還有一個 connectionListenerInternal,那隻能繼續往下分析了,connectionListenerInternal 函式(程式碼片段)的內部實現如下:
function connectionListenerInternal(server, socket) { httpSocketSetup(socket); if (socket.server === null) socket.server = server; if (server.timeout && typeof socket.setTimeout === 'function') socket.setTimeout(server.timeout); socket.on('timeout', socketOnTimeout); // 處理超時情況 var parser = parsers.alloc(); // 獲取parser物件 parser.reinitialize(HTTPParser.REQUEST); parser.socket = socket; socket.parser = parser; parser.incoming = null; var state = { outgoing: [], incoming: [], //... }; parser.onIncoming = parserOnIncoming.bind(undefined, server, socket, state); }
在 connectionListenerInternal 函式內部,我們終於見到了 “預備知識” 章節中介紹的 parsers 物件(FreeList 例項)。現在是時候來目睹一下 HTTPParser 物件的芳容了:
var parsers = new FreeList('parsers', 1000, function() { var parser = new HTTPParser(HTTPParser.REQUEST); parser._headers = []; parser._url = ''; parser._consumed = false; parser.socket = null; parser.incoming = null; parser.outgoing = null; parser[kOnHeaders] = parserOnHeaders; parser[kOnHeadersComplete] = parserOnHeadersComplete; parser[kOnBody] = parserOnBody; return parser; });
以 parser 開頭的這些物件,都是定義在_http_common.js
檔案中的函式物件。這裡我就不羅列出相關的程式碼了,只對它們的作用做一些簡單的總結:
- parserOnHeaders:當請求頭跨多個 TCP 資料包或者過大無法再一個執行週期內處理完才會呼叫該方法。
- kOnHeadersComplete:請求頭解析完成後,會呼叫該方法。方法內部會建立 IncomingMessage 物件,填充相關的屬性,比如 url、httpVersion、method 和 headers 等。
- parserOnBody:不斷解析已接收的請求體資料。
這裡需要注意的是,請求報文的解析工作是由 C++ 來完成,內部通過 binding 來實現,具體參考deps/http_parser
目錄。
const { methods, HTTPParser } = process.binding('http_parser');
介紹完 HTTPParser 物件,我們繼續回到 connectionListenerInternal 函式中,在最後一行我們設定 parser 物件的 onIncoming 屬性為繫結後的 parserOnIncoming 函式,該函式的實現如下(程式碼片段):
function parserOnIncoming(server, socket, state, req, keepAlive) { state.incoming.push(req); // 緩衝IncomingMessage例項 var res = new server[kServerResponse](req); if (socket._httpMessage) { state.outgoing.push(res); // 緩衝ServerResponse例項 } else { res.assignSocket(socket); } // 判斷請求頭是否包含expect欄位且http協議的版本為1.1 if (req.headers.expect !== undefined && (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) { // continueExpression: /(?:^|\W)100-continue(?:$|\W)/i // Expect: 100-continue if (continueExpression.test(req.headers.expect)) { res._expect_continue = true; if (server.listenerCount('checkContinue') > 0) { server.emit('checkContinue', req, res); } else { res.writeContinue(); server.emit('request', req, res); } } else if (server.listenerCount('checkExpectation') > 0) { server.emit('checkExpectation', req, res); } else { // HTTP協議中的417Expectation Failed 狀態碼錶示客戶端錯誤,意味著伺服器無法滿足 // Expect請求訊息頭中的期望條件。 res.writeHead(417); res.end(); } } else { server.emit('request', req, res); } return 0; }
通過觀察上面的程式碼,我們終於發現了request
事件的蹤跡。在 parserOnIncoming 函式內,我們會基於 req 請求物件建立 ServerResponse 響應物件,在建立響應物件後,會判斷請求頭是否包含 expect 欄位,然後針對不同的條件做出不同的處理。對於之前最早的示例來說,程式會直接走else
分支,即觸發request
事件,並傳遞當前的請求物件和響應物件。
現在我們來回顧一下整個流程:
-
呼叫
http.createServer()
方法建立 server 物件,該物件建立完後,我們呼叫listen()
方法執行監聽操作。
-
當 server 接收到客戶端的連線請求,在成功建立 socket 物件後,會觸發
connection
事件。 -
當
connection
事件觸發後,會執行對應的connectionListener
回撥函式。在函式內部會利用 HTTPParser 物件,對請求報文進行解析。 - 在完成請求頭的解析後,會建立 IncomingMessage 物件,並填充相關的屬性,比如 url、httpVersion、method 和 headers 等。
-
在配置完 IncomingMessage 物件後,會呼叫 parserOnIncoming 函式,在該函式內會構建 ServerResponse 響應物件,如果請求頭不包含 expect 欄位,則 server 就會觸發
request
事件,並傳遞當前的請求物件和響應物件。 -
request
事件觸發後,就會執行我們設定的requestListener
函式。
其實我們不但可以通過 Node.js 的 Http 模組建立 Http 伺服器,也可以利用該模組提供的 request() 或 get() 方法,向其它的 Http 伺服器傳送 Http 請求。比如:
const http = require("http"); http.get('http://jsonplaceholder.typicode.com/users', (res) => { console.log(`Got response: ${res.statusCode}`); let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { console.log(JSON.parse(data)); }); }).on('error', (e) => { console.log(`Got error: ${e.message}`); });
不過在實際專案中,我們一般會使用其它功能更加完善的第三方 Http 客戶端庫,比如Request 、Axios 和SuperAgent 等。
總結
本文基於一個簡單的伺服器示例,一步一步分析了 Node.js Http 模組中請求物件、響應物件內部的建立過程,此外還介紹了 Server 內部兩個重要的事件:connection
與request
。
在文中我們只分析request
事件的觸發時機,並未介紹connection
事件的觸發時機。此外也沒有繼續深入分析 server 物件listen()
方法內部執行流程。感興趣的同學,可以閱讀深入學習 Node.js Net 這篇文章。