還沒搞懂nodejs的http服務器?看這一篇就夠了

http2/https不在本文的討論範圍,本文基於Nodejs v13.1.0node

閱讀本篇文章以前,請閱讀前置文章:git

閱讀完本篇文章以後,但願你能夠掌握如下知識點:github

  • 完整的HTTP服務器啓動流程
  • HTTP的數據流轉

一、前置知識回顧

由於咱們知道nodejs啓動的服務器依賴於libuv,因此這裏咱們有必要將libuv如何啓動tcp服務器的過程說一下,後面的內容纔不會看得糊里糊塗。緩存

這個步驟在nodejs深刻學習系列之libuv基礎篇(一)2.2.10小節uv_tcp_t有過簡單的歸納:bash

一、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
二、綁定地址:uv_tcp_bind
三、監聽鏈接:uv_listen
四、每當有一個鏈接進來以後,調用uv_listen的回調,回調裏要作以下事情:
  4.一、初始化客戶端的tcp句柄:uv_tcp_init()
  4.二、接收該客戶端的鏈接:uv_accept()
  4.三、開始讀取客戶端請求的數據:uv_read_start()
  4.四、讀取結束以後作對應操做,若是須要響應客戶端數據,調用uv_write,回寫數據便可。
複製代碼

更多細節參考tcpserver.c服務器

那麼這麼一個過程,nodejs是如何經過v8和js將整個過程實現出來的呢?這也是咱們本文想要闡釋的重點。socket

若是你這個時候問我:明明講的是HTTP,爲啥回顧TCP服務器的啓動啊?那麼請你先面壁思過三秒鐘~tcp

二、從🌰開始

nodejs的魅力在哪裏?那就是啓動服務器。簡簡單單幾行代碼,就能夠啓動一個HTTP服務器:ide

const http = require('http')

const server = http.createServer(() => {})

server.listen(3000)
複製代碼

可是你知道嗎?外表看着簡單,實質心裏是很複雜的,就比如洋蔥,光滑無比的外殼下誰會想到有一圈圈,一圈圈也就算了,還會讓人流淚😭函數

這裏的每一行代碼背後都有着一套複雜的邏輯,咱們將從源碼入手,剖析隱藏在後面的原理。

三、http服務器涉及的主要文件

3.一、Js端

  • http.js:http模塊的主入口文件

  • _http_common.js:公共模塊,好比提供了咱們待會會說起到的http解析器

  • _http_incoming.js:實現了IncomingMessage類,繼承了Stream

  • _http_outgoing.js:實現了OutgoingMessage類,繼承了Strema

  • _http_server.js:http服務器實現主文件

  • net.js:tcp服務器實現主文件

  • internal/net.js:包含一些tcp服務器輔助函數

  • internal/http.js:包含一些http服務器輔助函數

備註:_http_clien.js_http_agent.js是做爲HTTP客戶端使用的。

3.二、C++端

  • tcp_wrap.cc:實現tcp_wrap這個內建模塊,提供諸如openbindlisten這類經常使用的tcp服務器方法
  • connection_wrap.cc:實現一個模板類,主要根據鏈接的類型不一樣,傳參不同,支持TCP和PIPE,當有鏈接上來的時候會調用模板類的onConnection方法
  • node_http_parser.cc:C++端的HTTP解析器,基於llhttp這個C語言包,可見服務器的HTTP報文解析交給執行效率更高的C++端了
  • stream_wrap.cc:實現stream_wrap這個內建模塊,基於libuv的stream模塊,繼承自StreamBase
  • stream_base.cc:實現StreamBase類,由於該類是stream_wrap的父類,因此其全部的方法均可以經過stream_wrap暴露出去

關於llhttp的介紹,在nodejs是如何和libuv以及v8一塊兒合做的?(文末有彩蛋哦)的第一小節**一、Nodejs依賴些啥?**有講到過。

3.三、全部文件的關係圖

下圖是給出上述文件的一個簡單的關係圖,給你們一個基本印象:

四、http.createServer():服務器的實例化

當執行http.createServer的時候,就是實例化Server,獲得的Server實例的原型結構以下圖:

Server的原型對象

對應的實例結構能夠經過Debug模式看到:

在這個階段埋下兩個重要的事件:requestconnection

到這裏實例化完成,是否是很easy?

五、server.listen(3000)

當執行到server.listen(3000)的時候,實際調用的是net.js裏面的listen方法。而listen方法最後歸一化調用listenInCluster,在這個方法裏面,能夠解釋**爲何集羣模式下,全部實例監聽相同的端口而不會報端口被佔用的錯誤?**由於,在這個方法中,會去判斷當前實例是不是master,若是是的話纔會去建立新的socket,不然是worker,則監聽master中獲得的socket。

listenInCluster中最後調用_listen2,也就是setupListenHandle

Server.prototype._listen2 = setupListenHandle
複製代碼

setupListenHandle最終調用:createServerHandle,這個時候C++端纔開始參與進來,這個過程的流程以下:

上述流程圖,從listen方法開始到結束,展現瞭如何與V8和libuv的一個完美合做,期間涉及到了三個libuv方法,也就是完成咱們在第一小節前置知識提到的前三個步驟:

一、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
二、綁定地址:uv_tcp_bind
三、監聽鏈接:uv_listen
複製代碼

同時有一個很是重要的點就是,咱們給TCP類的實例方法onconnection賦值了:

this._handle.onconnection = onconnection

對應於C++的代碼是(初始化爲Null):

t->InstanceTemplate()->Set(env->onconnection_string(), Null(env->isolate()));

env->onconnection_string()的定義在env.h

V(onconnection_string, "onconnection")

也就是你們看到的js端的onconnection方法。給這個方法賦值有啥用呢?

你們再仔細看上述流程圖libuv的一部分,最後一個調用的uv_listen傳了一個回調!這部分就是咱們下一節要講的內容。

listen完後的server實例有所變化,關注_handle這個變量(也就是紅框內):

能夠看到_handle此時也就是C++端定義的類TCP,其原型對象是LibuvStreamWrap,關於TCP類的實現能夠看tcp_wrap.cc文件的TCPWrap::Initialize,想要看得懂前提是你得先看過這篇文章:如何正確地使用v8嵌入到咱們的C++應用中

到這裏,服務器算是初始化完畢,接下去的內容更加有意思,請不要走開哦~

六、當有客戶端的鏈接請求進來

在上一小節,咱們埋了一個問題:設置了onconnection的js方法,可是沒有後續了嗎?

固然不是!咱們在前置知識中講到,調用了uv_listen以後,給了一個回調函數,當有鏈接進來的時候,就會調用回調函數。而V8這裏提供的回調就是在上面流程圖右下角用紅色加粗的函數OnConnection。咱們這一小節的內容就從這個函數開始講起。

以下圖展現了當有鏈接請求的時候,從操做系統底層通過Libuv以後,到js端的一個流程圖:

這個過程契合了咱們在前置知識中提到的這兩個步驟:

四、每當有一個鏈接進來以後,調用uv_listen的回調,回調裏要作以下事情:
  4.一、初始化客戶端的tcp句柄:uv_tcp_init()
  4.二、接收該客戶端的鏈接:uv_accept()
複製代碼

此時拿到了客戶端的tcp句柄client_handle經過回調以前設置的onconnection方法,傳值給js端:

wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
複製代碼

js端將該客戶端封裝到socket實例後再給_http_server.js,因而到這裏主動權又回到了js端。

爲了給該socket關聯上http解析器,也爲了在socket上監聽請求的數據,在connectionListenerInternal方法上,作了不少操做,主要有如下幾件:

  • 實例化解析器parser
  • 對socket的dataenderrorclosedrainresumepause等事件進行監聽,並綁定對應回調
  • parser.onIncoming綁定函數

關於parser的實現也是咱們搞懂整個環節的一部分,這一部分咱們在下一小節說起。

到這裏,整個流程你們是否是有一個比較清晰的認識了?

可是,好像有一點尚未說起到,你們知道是什麼嗎?想一想看,整個環節還缺乏了啥?

沒錯!就是請求的數據是如何從底層傳遞到咱們的應用程序的?這也是咱們下一節要講的內容。

七、當有請求數據到來的時候

咦~從上面一路下來,貌似整個流程已經結束了,至始至終都沒有看到任何和請求數據相關的,除了監聽data事件,那麼data事件是怎麼觸發的?是否底層調用和咱們以前前置知識的最後一個步驟一模一樣呢?帶着這麼多疑問,咱們將視線轉到剛纔有一個一筆帶過的環節:new Socket

7.一、new Socket其實不簡單

首先咱們須要明確的一點是:

  • Socket實例是一個繼承雙工流的套接字,所以關於流的一切用法,在Socket上均可以用。

實例化Socket涉及到的一些流程以下所示:

看過這篇文章Nodejs流學習系列之一: Readable Stream的童鞋都知道,Socket實現了可讀流的_read方法,也就是上圖中用①標註出來的方法,_read是用於從底層讀取數據緩存到可讀流的緩存中。

上圖中C++端的調用關係,請參考文件stream_base.ccstream_base.h,有點C++基礎的能夠看下源碼,裏面能夠看到那些複雜的C++概念:虛函數、虛類、重載、友類等。咱們在這裏只提一點:

  • 爲何ReadStartJS調用了ReadStart的時候,走到了LibuvStreamWrap::ReadStart

    由於LibuvStreamWrapStreamBase的派生類,而StreamBase又是StreamResource虛類的派生類,在stream_wrap.h中聲明瞭重載掉純虛函數(int ReadStart() override):virtual int ReadStart() = 0;,因此你看到的調用關係纔是上述流程圖所示。

上述流程印證了咱們在前置知識中提到的4.3步驟:

4.三、開始讀取客戶端請求的數據:uv_read_start()

咱們給uv_read_start設置的分配緩存回調如同上述流程所示。

注意:上述圖的C++空間中有一塊顏色特殊的註釋,咱們給客戶端的handle實例添加了一個onread回調,這點待會在下一節會有用到

7.二、當請求數據到來時

終於來到了咱們整個環節的最後一部分,興不興奮?激不激動?能看到這裏的童鞋都很贊👍!

這個環節也是so easy!奉上經典流程圖:

看上圖知道最後數據到來的時候,最終是會調用到js的終極函數:onStreamRead(stream_base_commons.js),該函數內部還有一些引用C++端的變量,有這麼一張對應圖:

js端將獲得的緩存再經過②箭頭所指的FastBuffer實例化一塊後才調用①箭頭所指的stream.push()方法。

調用這個方法有啥神奇的嗎?線索又斷了?😯,🙅‍♂️,看過這篇文章Nodejs流學習系列之一: Readable Stream的童鞋都知道,當往可讀流中push數據的時候,在flow模式下是會自動觸發data事件的,因而....

大結局來了?

還記得咱們以前在connectionListenerInternal(_http_server.js)中監聽的data事件嗎?

socketOnData成了咱們最後一個銜接其整個流程的最後一扣,該方法也是藉助了咱們在以前提到的parser,進行各類操做。

7.三、大結局之HTTP解析器

上面咱們一筆帶過了parser的分析,這一節終於輪到她粉墨登場了。parser是對C++端內建模塊http_parser的實例化體現:

const parser = new HTTPParser()

實例化也就算了,js端還給其綁定了諸多的js回調:

parser.onIncoming = null;
parser[kOnHeaders] = parserOnHeaders;
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
parser[kOnMessageComplete] = parserOnMessageComplete;
複製代碼

從字面上來看,是讓C++端每解析完HTTP一塊就須要告知js端一次。

socketOnData調用的是parser.execute(d),咱們來看一下完整的解析器流程:

將數據傳給C++端,利用llhttp的高效解析,獲得的HTTP頭部的信息,再回傳給js端,以後emit事件給在一開始咱們就監聽的request事件的回調,從這裏開始,你的應用代碼才正式被執行,如此一鼓作氣!

這個時候,須要你們動動腦筋了:

爲何解析數據要搞得這麼複雜?不能讓C++端接收數據後一併解析完再返回給js端嗎?非要將數據給js端、js端再給C++端、解析完又回傳給js端,繞來繞去的~

歡迎你們留言討論~

八、來個大總結吧

一圖以蔽之來結束本文:

限於篇幅,沒法面面俱到(諸如keepAlive、TCP分片之類的知識),若有想學習更多http服務器的內部實現,歡迎留言~

最後以這篇來結束在耗時兩個月,網上最全的原創nodejs深刻系列文章(長達十來萬字的文章,歡迎收藏)立下的flag。但願整個系列對你們深刻掌握nodejs有必定幫助!

感恩~

2019年的目標提早完成咯~

相關文章
相關標籤/搜索