Node.js 之 HTTP實現詳細分析

分針網每日分享:Node.js 之 HTTP實現詳細分析前端

 

Node.js的強項是處理網絡請求,那咱們就來分析一個HTTP請求在Node.js中是怎麼被處理的,以及JavaScript在這個過程當中引入的開銷到底有多大。
 
Node.js採用的網絡請求處理模型是IO多路複用。它與傳統的主從多線程併發模型是有區別的:只使用有限的線程數(1個),因此佔用系統資源不多;操做系統級的異步IO支持,能夠減小用戶態/內核態切換,而且自己性能更高(由於直接與網卡驅動交互);JavaScript天生具備保護程序執行現場的能力(閉包),傳統模型要麼依賴應用程序本身保存現場,或者依賴線程切換時自動完成。固然,並不能說IO多路複用就是最好的併發模型,關鍵仍是看應用場景。
 
咱們來看「hello world」版Node.js網絡服務器:
 
require('http').createServer((req, res) => {
res .end('hello world');
}).listen(3333);
 
代碼思路分析
 
createServer([requestListener])
 
createServer建立了http.Server對象,它繼承自net.Server。事實上,HTTP協議確實是基於TCP協議實現的。createServer的可選參數requestListener用於監聽request事件;另外,它也監聽connection事件,只不過回調函數是http.Server本身實現的。而後調用listen讓http.Server對象在端口3333上監聽鏈接請求並最終建立TCP對象,由tcp_wrap.h實現。最後會調用TCP對象的listen方法,這才真正在指定端口開始提供服務。咱們來看看涉及到的全部JavaScript對象:
 
 
 
涉及到的C++類大多隻是對libuv作了一層包裝並公佈給JavaScript,因此不在這裏特別列出。咱們有必要提一下http-parser,它是用來解析http請求/響應消息的,自己十分高效:沒有任何系統調用,沒有內存分配操做,純C實現。
 
connection事件
 
當服務器接受了一個鏈接請求後,會觸發connection事件。咱們能夠在這個結點獲取到套接字文件描述符,以後就能夠在這個文件描述符上作流式讀或寫,也就是所謂的全雙工模式。上文提到net.Server的listen方法會建立TCP對象,而且提供TCP對象的onconnection事件回調方法;這裏能夠利用字段net.Server.maxConnections作過載保護,後面會講到。而且會把clientHandle(本次鏈接的套接字文件描述符)封裝成net.Socket對象,做爲connection事件的參數。咱們來看看調用過程:
 
tcp_wrap.cc
 
void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
backlog ,
OnConnection );
args .GetReturnValue().Set(err);
}
 
OnConnection 在connection_wrap.cc中定義
 
// ...省略不重要的代碼
uv_stream_t * client_handle =
reinterpret_cast <uv_stream_t*>(&wrap->handle_);
// uv_accept can fail if the new connection has already been closed, in
// which case an EAGAIN (resource temporarily unavailable) will be
// returned.
if (uv_accept(handle, client_handle))
return;
 
// Successful accept. Call the onconnection callback in JavaScript land.
argv [1] = client_obj;
// ...省略不重要的代碼
wrap_data ->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
 
上文提到的clientHandle其實是uv_accept的第二個參數,指服務當前鏈接的套接字文件描述符。net.Server的字段 _handle 會在JavaScript側存儲該字段。最後咱們上一張流程圖:
 
 
 
request事件
 
connection事件的回調函數connectionListener(lib/_http_server.js)中,首先獲取http-parser對象,設置parser.onIncoming回調(立刻會用到)。當鏈接套接字有數據到達時,調用http-parser.execute方法。http-parser在解析過程當中會觸發以下回調函數:
 
on_message_begin:在開始解析HTTP消息以前,能夠設置http-parser的初始狀態(注意http-parse有多是複用的而不是重每次新建立)
 
on_url:解析請求的url,對響應消息不起做用
 
on_status, 解析狀態碼,只對http響應消息起做用
 
on_head_field, 頭字段名稱
 
on_head_value:頭字段對應值
 
on_headers_complete:當全部頭解析完成時
 
on_body:解析http消息中包含的payload
 
on_message_complete:解析工做結束
 
Node.js中Parser類是對http-parser的包裝,它會註冊上面全部的回調函數。同時,暴露給JavaScript5個事件:
kOnHeaders,kOnHeadersComplete,kOnBody,kOnMessageComplete,kOnExecute。在lib/_http_common.js中監聽了這些事件。其中,當須要強制把頭字段回傳到JavaScript時會觸發kOnHeaders;例如,頭字段個數超過32,或者解析結束時仍然有頭字段沒有回傳給JavaScript。當調用完http_parser_execute後觸發kOnExecute。kOnHeadersComplete事件觸發時,會調用parser的onIncoming回調函數。僅僅HTTP頭解析完成以後,就會觸發request事件。執行流程以下:
 
 
 
總結
 
說了那麼多,其實仍然離不開最基礎的套接字編程步驟,對於服務器端依次是:create、bind,listen、accept和close。客戶端會經歷create、bind、connect和close。想了解更多套接字編程的同窗能夠參考《UNIX網絡編程》。
 
HTTP場景分析
 
上面提到的Node.js版hello world只涵蓋了HTTP處理最基本的狀況,可是也足以說明Node.js處理得很是簡潔。如今,咱們來分析一些典型的HTTP場景。
 
1. keep-alive
 
對於前端應用,HTTP請求瞬間數量比較多,但每一個請求傳輸的數據通常不大;這時,用同一個TCP鏈接處理同一個用戶發出的HTTP請求能夠顯著提升性能。可是keep-alive也不是萬能的,若是用戶每次只發起一個請求,它反而會由於延長鏈接的生存時間,浪費服務器資源。
 
針對同一個鏈接,Node.js會維持一個incoming隊列和一個outgoing隊列。應用程序經過監聽request事件,能夠訪問ServerResponse和IncomingMessage對象,當請求處理完成以後(調用response.end()),ServerResponse會響應finish事件。若是它是本次鏈接上最後一個response對象,則準備關閉鏈接;不然,繼續觸發request事件。每一個鏈接最長超時時間默認爲2分鐘,能夠經過http.Server.setTimeout調整。
如今把咱們的Node.js版hello world修改一下
 
var delay = [2000, 30, 500];
var i = 0;
require('http').createServer((req, res) => {
// 爲了讓請求模擬更真實,會調整每一個請求的響應時間
setTimeout(() => {
res .end('hello world');
}, delay[i]);
i = (i+1)%(delay.length);
}).listen(3333, () => {
// listen的回調函數
console .log('listen at 3333');
});
 
客戶端代碼以下:
 
var http = require('http');
 
// 設置HTTP agent開啓keep-alive模式
// 套接字的打開時間維持1分鐘
var agent = new http.Agent({
keepAlive : true,
keepAliveMsecs : 60000
});
 
// 每次請求結束以後,都會再發起一次請求
// doReq每調用一次只會觸發2次請求
function doReq(again, iter) {
let request = http.request({
hostname : '192.168.1.10',
port : 3333,
agent :agent
}, (res) => {
console .log(`${new Date().valueOf()} ${iter} ${again} Headers: ${JSON.stringify(res.headers)}`);
console .log(request.socket.localPort);
// 設置解析響應的編碼格式
res .setEncoding('utf8');
// 接收響應
res .on('data', (chunk) => {
console .log(`${new Date().valueOf()} ${iter} ${again} Body: ${chunk}`);
});
if (again) doReq(false, iter);
});
// 發起請求
request .end();
}
 
for (let i = 0; i < 3; i++) {
doReq(true, i);
}
 
套接字複用的時序以下:
 
 
 
本文轉自: http://www.f-z.cn/id/287
相關文章
相關標籤/搜索