當前位置:
首頁 > 最新 > 走進Node.js之HTTP實現分析

走進Node.js之HTTP實現分析

上文「走進Node.js啟動過程」中我們算是成功入門了。既然 Node.js 的強項是處理網路請求,那我們就來分析一個 HTTP 請求在 Node.js 中是怎麼被處理的,以及 JavaScript 在這個過程中引入的開銷到底有多大。

Node.js 採用的網路請求處理模型是 IO 多路復用。它與傳統的主從多線程並發模型是有區別的:只使用有限的線程數(1個),所以佔用系統資源很少;操作系統級的非同步 IO 支持,可以減少用戶態/內核態切換,並且本身性能更高(因為直接與網卡驅動交互);JavaScript 天生具有保護程序執行現場的能力(閉包),傳統模型要麼依賴應用程序自己保存現場,或者依賴線程切換時自動完成。當然,並不能說 IO 多路復用就是最好的並發模型,關鍵還是看應用場景。

我們來看「hello world」版 Node.js 網路伺服器:

代碼思路分析

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(https://github.com/nodejs/http-parser),它是用來解析 HTTP 請求/響應消息的,本身十分高效:沒有任何系統調用,沒有內存分配操作,純 C 實現。

connection 事件

當伺服器接受了一個連接請求後,會觸發 connection 事件。我們可以在這個結點獲取到套接字文件描述符,之後就可以在這個文件描述符上做流式讀或寫,也就是所謂的全雙工模式。

上文提到 net.Server 的 listen 方法會創建 TCP 對象,並且提供 TCP 對象的 onconnection 事件回調方法;這裡可以利用欄位net.Server.maxConnections 做過載保護,後面會講到。並且會把clientHandle(本次連接的套接字文件描述符)封裝成 net.Socket 對象,作為connection 事件的參數。我們來看看調用過程:

tcp_wrap.cc

OnConnection在connection_wrap.cc中定義

上文提到的 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 的包裝,它會註冊上面所有的回調函數。

同時,暴露給 JavaScript 5 個事件:

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 修改一下:

客戶端代碼如下:

套接字復用的時序如下:

2. Expect頭

如果客戶端在發送 POST 請求之前,由於傳輸的數據量比較大,期望向伺服器確認請求是否能被處理;這種情況下,可以先發送一個包含頭 Expect:100-continue 的 HTTP 請求。

如果伺服器能處理此請求,則返迴響應狀態碼100(Continue);否則,返回 417(Expectation Failed)。默認情況下,Node.js 會自動響應狀態碼 100;同時,http.Server 會觸發事件 checkContinue和 checkExpectation 來方便我們做特殊處理。

具體規則是:當伺服器收到頭欄位 Expect 時:如果其值為 100-continue,會觸發 checkContinue 事件,默認行為是返回 100;如果值為其它,會觸發 checkExpectation 事件,默認行為是返回 417。

例如,我們通過 curl 發送HTTP請求:

交互過程如下

我們接收到 2 個響應,分別是狀態碼 100 和 200。前一個是 Node.js 的默認行為,後一個是應用程序代碼行為。

3. HTTP代理

在實際開發時,用到 HTTP 代理的機會還是挺多的,比如,測試說線上出 bug了,觸屏版頁面顯示有問題;我們一般第一時間會去看 API 返回是否正常,這個時候在手機上設置好代理就能輕鬆捕獲 HTTP 請求了。

老牌的代理工具有fiddler,charles。其實,nodejs 下也有,例如 node-http-proxy,anyproxy。基本思路是監聽 request 事件,當客戶端與代理建立 HTTP 連接之後,代理會向真正請求的伺服器發起連接,然後把兩個套接字的流綁在一起。我們可以實現一個簡單的代理伺服器:

驗證下是否真的起作用,curl 通過代理伺服器訪問我們的「hello world」版Node.js 伺服器:

優化策略

Node.js 在實現 HTTP 伺服器時,除了利用高性能的 http-parser,自身也做了些性能優化。

1. http_parser 對象緩存池

http-parser 對象處理完一個請求之後不會被立即釋放,而是被放入緩存池(/lib/internal/freelist),最多緩存 1000 個 http-parser 對象。

2. 預設 HTTP 頭總數

HTTP 協議規範並沒有限定可以傳輸的 HTTP 頭總數上限,http-parser 為了避免動態分配內存,設定上限默認值是 32。其他 web 伺服器實現也有類似設置;例如,apache 能處理的 HTTP 請求頭默認上限(LimitRequestFields)是100。

如果請求消息中頭欄位真超過了 32 個,Node.js 也能處理,它會把已經解析的頭欄位通過事件 kOnHeaders 保存到 JavaScript 這邊然後繼續解析。 如果頭欄位不超過 32 個,http-parser 會直接處理完並觸發 on_headers_complete 一次性傳遞所有頭欄位;所以我們在利用 Node.js 作為web 伺服器時,應盡量把頭欄位控制在 32 個之內。

3. 過載保護

理論上,Node.js 允許的同時連接數只與進程可以打開的文件描述符上限有關。但是隨著連接數越來越多,佔用的系統資源也越來越多,很有可能連正常的服務都無法保證,甚至可能拖垮整個系統。

這時,我們可以設置 http.Server 的maxConnections,如果當前並發量大於伺服器的處理能力,則伺服器會自動關閉連接。另外,也可以設置 socket 的超時時間為可接受的最長響應時間。

性能實測

為了簡單分析下 Node.js 引入的開銷,現在基於 libuv 和 http_parser 編寫一個純 C 的 HTTP 伺服器。基本思路是,在默認事件循環隊列上監聽指定 TCP 埠;如果該埠上有請求到達,會在隊列上插入一個一個的任務;當這些任務被消費時,會執行 connection_cb。見核心代碼片段:

connection_cb 調用 uv_accept 會負責與發起請求的客戶端實際建立套接字,並註冊流操作回調函數 read_cb:

上文中read_cb用於讀取客戶端請求數據,並發送響應數據:

全部源碼請參見 simple HTTP server(https://github.com/Hujiang-FE/simple-http-server)。我們使用 apache benchmark(https://httpd.apache.org/docs/2.4/programs/ab.html)來做壓力測試:並發數為 5000,總請求數為 100000。

測試結果如下:0.8秒(C) vs 5秒(Node.js)

我們再看看內存佔用,0.6MB(C) vs 51MB(Node.js)

Node.js 雖然引入了一些開銷,但是從代碼實現行數上確實要簡潔很多。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 滬江技術學院 的精彩文章:

React 表單:Refs 的運用
專訪「Android 首席醫生」徐宜生:善於利用工具來解決問題的程序員才是好的工程師

TAG:滬江技術學院 |

您可能感興趣

用PyTorch實現Mask R-CNN
Python實現TFTP文件傳輸
ASP.NET Core Web API下事件驅動型架構的實現(三):基於RabbitMQ的事件匯流排
Github 項目推薦 用PyTorch 實現 OpenNMT
FAIR聯合INRIA提出DensePose-RCNN,更好地實現人體姿態估計
GAN的Keras 實現案例集合——Keras-GAN
「CVPR Oral」TensorFlow實現StarGAN代碼全部開源,1天訓練完
netty整合springMVC,實現高效的HTTP服務請求
阿里將 TVM 融入 TensorFlow,在 GPU 上實現全面提速
YOLOv3 的最小化 PyTorch 實現
TU Ilmenau提出新型Complex-YOLO,實現點雲上實時3D目標檢測
1.29 VR掃描:HTC申請Cardboard類移動VR專利;Facebook或將在移動AR/VR中實現全身追蹤
FAIR最新視覺論文集錦:FPN,RetinaNet,Mask 和 Mask-X RCNN(含代碼實現)
商湯聯合提出基於FPGA的快速Winograd演算法:實現FPGA之上最優的CNN表現與能耗
HTC高管展示SteamVR Tracking 2.0技術 實現跨房間VR使用
利用谷歌object detection API實現Oxford-IIIT Pets Dataset 目標檢測趟坑記錄
LVS/DR+keepalived負載均衡實現
MXNet開放支持Keras,高效實現CNN與RNN的分散式訓練
Dovey Wan口中航母級別的CELER NETWORK要實現每秒數十億筆交易!
淺入淺出TensorFlow 6—實現AlexNet和VGG等經典網路