http2/https不在本文的討論範圍,本文基於Nodejs v13.1.0node
閱讀本篇文章以前,請閱讀前置文章:git
閱讀完本篇文章以後,但願你能夠掌握如下知識點:github
由於咱們知道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.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客戶端使用的。
tcp_wrap
這個內建模塊,提供諸如open
、bind
、listen
這類經常使用的tcp服務器方法llhttp
這個C語言包,可見服務器的HTTP報文解析交給執行效率更高的C++端了stream_wrap
這個內建模塊,基於libuv的stream
模塊,繼承自StreamBase
類StreamBase
類,由於該類是stream_wrap
的父類,因此其全部的方法均可以經過stream_wrap
暴露出去關於llhttp
的介紹,在nodejs是如何和libuv以及v8一塊兒合做的?(文末有彩蛋哦)的第一小節**一、Nodejs依賴些啥?**有講到過。
下圖是給出上述文件的一個簡單的關係圖,給你們一個基本印象:
http.createServer()
:服務器的實例化當執行http.createServer
的時候,就是實例化Server
,獲得的Server實例的原型結構以下圖:
對應的實例結構能夠經過Debug模式看到:
在這個階段埋下兩個重要的事件:request
和connection
。
到這裏實例化完成,是否是很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
方法上,作了不少操做,主要有如下幾件:
data
、end
、error
、close
、drain
、resume
、pause
等事件進行監聽,並綁定對應回調parser.onIncoming
綁定函數關於parser的實現也是咱們搞懂整個環節的一部分,這一部分咱們在下一小節說起。
到這裏,整個流程你們是否是有一個比較清晰的認識了?
可是,好像有一點尚未說起到,你們知道是什麼嗎?想一想看,整個環節還缺乏了啥?
沒錯!就是請求的數據是如何從底層傳遞到咱們的應用程序的?這也是咱們下一節要講的內容。
咦~從上面一路下來,貌似整個流程已經結束了,至始至終都沒有看到任何和請求數據相關的,除了監聽data
事件,那麼data
事件是怎麼觸發的?是否底層調用和咱們以前前置知識的最後一個步驟一模一樣呢?帶着這麼多疑問,咱們將視線轉到剛纔有一個一筆帶過的環節:new Socket
new Socket
其實不簡單首先咱們須要明確的一點是:
Socket
實例是一個繼承雙工流的套接字,所以關於流的一切用法,在Socket
上均可以用。實例化Socket
涉及到的一些流程以下所示:
看過這篇文章Nodejs流學習系列之一: Readable Stream的童鞋都知道,Socket
實現了可讀流的_read
方法,也就是上圖中用①標註出來的方法,_read
是用於從底層讀取數據緩存到可讀流的緩存中。
上圖中C++端的調用關係,請參考文件stream_base.cc
和stream_base.h
,有點C++基礎的能夠看下源碼,裏面能夠看到那些複雜的C++概念:虛函數、虛類、重載、友類等。咱們在這裏只提一點:
爲何ReadStartJS
調用了ReadStart
的時候,走到了LibuvStreamWrap::ReadStart
?
由於LibuvStreamWrap
是StreamBase
的派生類,而StreamBase
又是StreamResource
虛類的派生類,在stream_wrap.h
中聲明瞭重載掉純虛函數(int ReadStart() override
):virtual int ReadStart() = 0;
,因此你看到的調用關係纔是上述流程圖所示。
上述流程印證了咱們在前置知識中提到的4.3步驟:
4.三、開始讀取客戶端請求的數據:uv_read_start()
咱們給uv_read_start
設置的分配緩存回調如同上述流程所示。
注意:上述圖的C++空間中有一塊顏色特殊的註釋,咱們給客戶端的handle實例添加了一個onread回調,這點待會在下一節會有用到
終於來到了咱們整個環節的最後一部分,興不興奮?激不激動?能看到這裏的童鞋都很贊👍!
這個環節也是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
,進行各類操做。
上面咱們一筆帶過了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年的目標提早完成咯~