摘要:請求的來到在中,若要收到一個請求,首先需要創(chuàng)建一個類的實例,然后監(jiān)聽它的事件。不斷解析推入的請求體數(shù)據(jù)。處理具體解析完畢的請求。比較繞,以一個簡化的圖示來總結(jié)這部分邏輯響應(yīng)該請求到了響應(yīng)時,事情已經(jīng)簡單許多了,傳入的已經(jīng)獲取到了。
如果大家使用 Node.js 寫過 web 應(yīng)用,那么你一定使用過 http 模塊。在 Node.js 中,起一個 HTTP server 十分簡單,短短數(shù)行即可:
"use stirct" const { createServer } = require("http") createServer(function (req, res) { res.writeHead(200, { "Content-Type": "text/plain" }) res.end("Hello World ") }) .listen(3000, function () { console.log("Listening on port 3000") })
$ curl localhost:3000 Hello World
就這么簡單,因為 Node.js 把許多細(xì)節(jié)都已在源碼中封裝好了,主要代碼在 lib/_http_*.js 這些文件中,現(xiàn)在就讓我們照著上述代碼,看看從一個 HTTP 請求的到來直到響應(yīng),Node.js 都為我們在源碼層做了些什么。
HTTP 請求的來到在 Node.js 中,若要收到一個 HTTP 請求,首先需要創(chuàng)建一個 http.Server 類的實例,然后監(jiān)聽它的 request 事件。由于 HTTP 協(xié)議屬于應(yīng)用層,在下層的傳輸層通常使用的是 TCP 協(xié)議,所以 net.Server 類正是 http.Server 類的父類。具體的 HTTP 相關(guān)的部分,是通過監(jiān)聽 net.Server 類實例的 connection 事件封裝的:
// 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 傳輸過來的數(shù)據(jù):
// 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)的數(shù)據(jù)結(jié)構(gòu),實現(xiàn)很簡單,個人覺得是為了盡可能的對 parser 進(jìn)行重用,并避免了不斷調(diào)用構(gòu)造函數(shù)的消耗,且設(shè)有數(shù)量上限(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; };
由于數(shù)據(jù)是從 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 請求從接收到完全解析,會挨個經(jīng)歷 parser 上的如下事件監(jiān)聽器:
parserOnHeaders:不斷解析推入的請求頭數(shù)據(jù)。
parserOnHeadersComplete:請求頭解析完畢,構(gòu)造 header 對象,為請求體創(chuàng)建 http.IncomingMessage 實例。
parserOnBody:不斷解析推入的請求體數(shù)據(jù)。
parserOnExecute:請求體解析完畢,檢查解析是否報錯,若報錯,直接觸發(fā) clientError 事件。若請求為 CONNECT 方法,或帶有 Upgrade 頭,則直接觸發(fā) connect 或 upgrade 事件。
parserOnIncoming:處理具體解析完畢的請求。
所以接下來,我們的關(guān)注點自然是 parserOnIncoming 這個監(jiān)聽器,正是這里完成了最終 request 事件的觸發(fā),關(guān)鍵步驟代碼如下:
// 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 發(fā)來的請求,源碼中分別維護(hù)了兩個隊列,用于緩沖 IncomingMessage 實例和對應(yīng)的 ServerResponse 實例。先來的 ServerResponse 實例先占用 socket ,監(jiān)聽其 finish 事件,從各自隊列中釋放該 ServerResponse 實例和對應(yīng)的 IncomingMessage 實例。
比較繞,以一個簡化的圖示來總結(jié)這部分邏輯:
到了響應(yīng)時,事情已經(jīng)簡單許多了,傳入的 ServerResponse 已經(jīng)獲取到了 socket。http.ServerResponse 繼承于一個內(nèi)部類 http.OutgoingMessage,當(dāng)我們調(diào)用 ServerResponse#writeHead 時,Node.js 為我們拼湊好了頭字符串,并緩存在 ServerResponse 實例內(nèi)部的 _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; }
緊接著在調(diào)用 ServerResponse#end 時,將數(shù)據(jù)拼湊在頭字符串后,添加對應(yīng)的尾部,推入 TCP ,具體的寫入操作在內(nèi)部方法 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 " + this._trailer + " ", "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); };最后
到這,一個請求就已經(jīng)通過 TCP ,發(fā)回給客戶端了。其實本文中,只涉及到了一條主線進(jìn)行解析,源碼中還考慮了更多的情況,如超時,socket 被占用時的緩存,特殊頭,上游突然出現(xiàn)問題,更高效的已寫頭的查詢等等。非常值得一讀。
參考:
https://github.com/nodejs/node/blob/master/lib/_http_common.js
https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js
https://github.com/nodejs/node/blob/master/lib/_http_server.js
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/79356.html
摘要:文本已收錄至我的倉庫,歡迎前后端分離這個詞相信大家都聽過,不知道大家是怎么理解的呢。流下不學(xué)無術(shù)的淚水目前我了解到的前后端分離,首先部署是分離的至少不會跟綁定在一起部署接口只返回數(shù)據(jù)關(guān)于前端這幾大框架這幾個我都是沒有寫過的,所以也就不多了。 前言 只有光頭才能變強(qiáng)。文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/ZhongFuCheng3y/3y ...
摘要:原生應(yīng)用是一個基于引擎的運(yùn)行環(huán)境使用了一個事件驅(qū)動非阻塞式的模型,使其輕量又高效的包管理器,是全球最大的開源庫生態(tài)系統(tǒng)本文主要介紹構(gòu)建一個應(yīng)用的基本步驟和模塊,并假定你已經(jīng)對有一定的了解本文引用部分代碼作為例子,如果希望參看全部源碼,歡迎去 原生 Node.js 應(yīng)用 Node.js 是一個基于 Chrome V8 引擎的 JavaScript 運(yùn)行環(huán)境Node.js 使用了一個事件驅(qū)...
摘要:域套接字使用或指定請求方法的字符串。請求路徑包含非法字符時拋出異常。保持資源池周圍的套接字在未來被用于其它請求。默認(rèn)值為當(dāng)使用的時候,通過正在保持活動的套接字發(fā)送包的頻繁程度。 文章來源:小青年原創(chuàng)發(fā)布時間:2016-09-29關(guān)鍵詞:JavaScript,nodejs,http,url ,Query String,爬蟲轉(zhuǎn)載需標(biāo)注本文原始地址: http://zhaomenghuan....
摘要:事實上,協(xié)議確實是基于協(xié)議實現(xiàn)的。的可選參數(shù)用于監(jiān)聽事件另外,它也監(jiān)聽事件,只不過回調(diào)函數(shù)是自己實現(xiàn)的。并且會把本次連接的套接字文件描述符封裝成對象,作為事件的參數(shù)。過載保護(hù)理論上,允許的同時連接數(shù)只與進(jìn)程可以打開的文件描述符上限有關(guān)。 作者:正龍(滬江Web前端開發(fā)工程師)本文為原創(chuàng)文章,轉(zhuǎn)載請注明作者及出處 上文走進(jìn)Node.js啟動過程中我們算是成功入門了。既然Node.js的強(qiáng)...
閱讀 2130·2021-11-18 10:07
閱讀 3507·2021-09-04 16:48
閱讀 3214·2019-08-30 15:53
閱讀 1235·2019-08-30 12:55
閱讀 2453·2019-08-29 15:08
閱讀 3149·2019-08-29 15:04
閱讀 2879·2019-08-29 14:21
閱讀 2907·2019-08-29 11:21