(六)網絡編程之化身一個請求感受瀏覽器輸入URL后奇妙的網絡之旅!

引言

? ?在瀏覽器上輸入一個URL后發生了什么? 這也是面試中老生常談的話題,包括網上也有大量關于這塊的內容:

百度數據

從百度的搜索結果來看,能夠搜到七千多萬條記錄,因此本篇不會再以那種前篇一律的方式贅述,而是以目前較新的網絡內容,結合系統中的大部分服務,將自己類比成一個請求,切身感受到每個技術棧的具體細節,徹底從“根兒上”理解客戶端請求-服務端響應的全過程。

本篇以https://www.juejin.cn/為例進行分析,當然,這里假設掘金后端是Java做的(實際上掘金好像是基于Node做的后端)。

分享一個趣事,我發現掘金貌似使用的是.cn后綴的域名,并非通常使用的.com域名,好像www.juejin.com被人搶注了....^_^

一、地址欄輸入后本地會發生的事情

? ?當我們在瀏覽器的地址欄中,輸入xxx內容后,瀏覽器的進程首先會判斷輸入的內容:

  • 如果是普通的字符,那瀏覽器會使用默認的搜索引擎去對于輸入的xxx生成URL
  • 如若輸入的是網址,那瀏覽器會拼接協議名形成完整的URL

當然,在地址欄中輸入某個內容后,也會進行一些額外操作,例如:安全檢查、訪問限制等,但總歸而言,瀏覽器做的第一件工作則是生成URL,當按下回車后,瀏覽器進程會將生成的完整URL發送到網絡進程:

用戶-瀏覽器-網絡進程

當網絡進程收到傳過來的URL后,首先并不會直接發出網絡請求,而是會先查詢本地緩存:
本地緩存查詢

觀察上述流程,當網絡進程收到傳來的URL后,會首先通過URL作為Key,在本地緩存中進行查詢:

  • ①如果本地中是否有緩存:
    • 沒有:發起網絡請求去服務器獲取資源,成功后將結果渲染頁面并寫入緩存。
    • 有:繼續判斷本地中的緩存內容是否已經過期,沒有則直接使用本地緩存。
  • ②如果本地中的緩存已經過期,則會攜帶If-Modified-Since、If-None-Match等標識向服務器發起請求,先判斷服務器中的資源是否更新過:
    • 未更新:服務器返回304狀態碼,并繼續讀取之前的緩存內容使用。
  • ③如若服務器的資源更新過,那么也會向服務器發起請求獲取資源。

如果在本地緩存中,無法命中緩存,或者本地緩存已過期并服務器資源更新過,那么此刻網絡進程才會真正向目標網站發起網絡請求。

二、一個全新的“我”誕生過程與前期的經歷

? ?當客戶端的網絡進程,在查詢緩存無果后,會真正開始發送網絡請求,但要牢記:客戶端的網絡進程并非直接向目標網站發起請求的,前期還需經過一些細節處理。

當然,為了能夠更直觀的感受整個過程,在這里我們將自己“化身”為一個請求,站在請求的角度切身體驗一段奇特的“網絡旅途”。

2.1、“我”誕生前的準備 - 解析URL

? ?在網絡進程發起請求之前,會首先對瀏覽器進程傳過來的URL進行解析,一般來說完整的URL結構如下:

URL完整結構

但上述結構使用較少,通常情況下,瀏覽器會使用的URL的常用結構如下:
URL常用結構

URL中每個字段的釋義如下:

  • scheme:表示使用的協議類型,例如http、https、ftp、chrome等。
  • ://:協議類型與后續描述符之間的分隔符。
  • domainName:網站域名,經DNS解析后會得到具體服務器IP
  • /path:請求路徑,代表客戶端請求的資源所在位置,不同層級目錄之間用/區分。
  • ?query1=value:請求參數,?后面表示請求的參數,采用K-V鍵值對形式。
  • &query2=value:多個請求參數,不同的參數之間用&分割。
  • #fragment:表示所定位資源的一個錨點,瀏覽器可根據這個錨點跳轉對應的資源位置。

網絡進程會根據URL的結構對目標URL進行解析,其中有兩個關鍵信息:

  • 首先會解析得到協議名,例如http、https,這關乎到后續默認使用的端口號。
  • 然后會解析得到域名,這個將關乎到后續具體請求的服務器地址。

假設瀏覽器傳輸過來的URLhttps://juejin.cn/user/862486453028888/posts,那么在這個階段會確定后續請求的服務器端口號為443,請求的目標域名為www.juejin.cn。其實在這里主要是根據瀏覽器的輸入信息,去解析出一些“誕生我(請求)”的前置要素。

2.2、“我”該去往的具體位置 - DNS域名解析

? ?在上個階段已經大概知道“我”該去往何處啦!但我具體地址該到那里呢?“我”好像不大清楚,要不找個人問問吧^_^。我記得好像有個叫做DNS的“大家族”是專門負責這個的!我要去找它們問問看~

DNS查詢過程

不過在問DNS之前,我先來看看本地有沒有域名與IP的映射緩存,好像沒有~,那我只能去找DNS(-_-),我首先找到了「本地DNS大叔」,把我要查找的域名交給了它,它讓我稍等片刻,它給我找一下,讓我們一起來看看「本地DNS大叔」是怎么查找的:

  • ①首先「本地DNS大叔」找了它的「根DNS族長」,族長告訴它應該去找「頂級DNS長老」。
  • ②「本地DNS大叔」根據族長的示意去找了「頂級DNS長老」,然而長老又告訴它應該去找「授權DNS執事」。
  • ③「本地DNS大叔」又根據長老的示意找到了「授權DNS執事」,最終在「授權DNS執事」那里查到了我手里域名對應著的具體IP地址。
  • ④「本地DNS大叔」拿著從「授權DNS執事」那里查到的IP,最終把它交給了我,為了下次不麻煩大叔,所以我獲取了IP后,將其緩存在了本地。

呼~,我終于知道我該去哪兒啦!準備出發咯!

更為詳細且專業性的查詢過程請參考:《HTTP/HTTPS-DNS域名解析系統》

2.3、確保路途安全 - TCP與TLS握手

? ?問過DNS大叔后,獲得了目的地址的我,此時已經知道該去往何處啦!但在正式出發前,由于前路坎坷,途中會存在各類危機(網絡阻塞、網絡延遲、第三方劫持等),因此為了我的安全出行,首先還需為我建立一條安全的通道,所以我還需要等一會兒才能出發,俺們一起來瞅瞅建立安全通道的過程是什么樣的:

TLS/1.2握手完整流程

看著好復雜啊~,但似乎大體就分為了兩個過程:

首先是TCP的三次握手過程,聽說這個階段是為了確保目的地能夠正常接收我、也是為了給我建立出一條可靠的出行通道、并且為我計算一下出行失敗之后多久重新出發的時間等目的(也就是為了測試雙方是否能正常通信、建立可靠連接以及預測超時時間等)。

其實按照之前的“交通規則”,在建立好TCP連接之后,我就可以繼續走下一步啦,但現在有很多壞人,在我們出行的道路上劫持我們,然后竊取、篡改俺們攜帶的數據,所以如今出行變得很不安全,因此還需要還需要建立一條安全的出行通道,就是TLS大叔的安全連接~(HTTP+TLS=HTTPS):

TLS握手階段,在這個階段中,TLS大叔為了俺的安全出行,會通過很多手段:非對稱加密、對稱加密、第三方授權等,先和俺的目的地交換一個密鑰,然后再通過這個密鑰對我加密一下,確保我被壞人抓到了也無法得到俺護送的數據^_^

詳細且專業性的過程請參考之前的:《計網基礎TCP/IP綜述-TCP三次握手》《全解HTTP/HTTPS-SLL、TLS詳解》

2.4、誕生“我的身體” - 構建請求報文

? ?經歷上述過程后,安全的出行道路已經建立好啦!但此刻的我還不算完整,所以需要先構建一個“身體”,也就是HTTP請求報文:

請求報文

“我的身體”主要由請求行、請求頭、空行以及請求主體四部分組成,里面包含了“我本次出遠門的需要護送的數據和一些其他信息”。同時,為了我能夠在“出行的道路上(傳輸介質)”安全且正常傳輸,我還需要經過層層封裝:
數據封裝過程

首先為了確保俺護送的數據安全,TLS大叔會先對我的數據進行一次加密,把我原本攜帶的明文數據轉變為看都看不懂的密文,類似下面這個樣子:
加密結果

經過加密后的我會緊接著來到傳輸層,傳輸層會在我的腦袋上再貼上一個傳輸頭,如果是TCP大哥的話,它會給我貼上一個TCP頭,但如果傳輸層的UDP大哥在的話,它給我貼的就是UDP頭。但不管是誰貼的,在這個傳輸頭內,為了防止我迷路和走丟,TCP、UDP兩位大哥哥都會細心的在里面寫清楚“我來自哪里,該去往何處”,也就是源地址和目的地址:

TCP、UDP頭

偷偷吐槽一句:TCP大哥貼的傳輸頭里面,放了好多好多東西,讓我感覺腦袋沉沉的。

過了傳輸層這一站之后,我又來到了網絡層,果不其然,網絡層里面最常見的還是IP大叔,IP大叔看到我之后,又在我的腦袋上貼上了一個網絡頭,也就是給我又加了一個IP頭。

噠噠噠~,我出了網絡層這關之后,又來到了數據鏈路層,這關則是由大名鼎鼎的“以太網家族”駐守,在這里我和之前兩關不同,除開在我腦袋上貼了一個鏈路頭之外,還給我在尾巴上多加了一個鏈路尾。

不過剛剛出鏈路層的時候,好像有個人跟我說:你這個樣子是無法在介質上行走的,你要記得改變一下啊!

我還沒聽的太清楚,就來到了物理層這關,這層和之前我“家里”以及之前的關卡環境都不一樣,物理層的小伙伴們好像都有實際的形態,但之前接觸所有內容都是虛擬的概念形態哎~。

在我對比物理層大哥們的異樣差距時,一不愣神發現我的身體好像發生了“翻天覆地”的變化,整個我似乎都變為了0、1構成了,正當納悶時,物理層的某個大哥哥告訴我說:“只有變成這樣子,你才可以在出行的道路上行走哈,所以我們給你轉換了一下形態,你現在已經可以出發了”。

原來是這樣呀,好像鏈路層的時候有人跟我說過哎~

同樣對于更為專業、詳細的過程可參考之前的:《HTTP/HTTPS-HTTP報文組成》《計網基礎之TCP/IP-網絡分層模型》等內容。

2.5、踏上路途的我 - 數據傳輸

? ?GO~GO~GO~,終于出發啦!我終于踏上了網絡之旅!呼呼呼~

路由器中轉

咔!我來到了第一個中轉站,聽別人說,好像它的名字叫做路由器,首先路由器大哥把我的身體按照之前封裝的步驟層層解封了,但解封到傳輸層的時候,看到了我腦袋上的傳輸頭,似乎路由器大哥發現了TCP哥哥寫的目的地址,發現我的目的地還在更遠的位置,然后路由器大哥又按照原本的步驟把我的身體封裝回去了,然后還親切的給我指出了接下來該往那條路走,我又該繼續前行啦....

我一邊走著,一邊在思考:好像路由器大哥就是負責給俺們指路的,防止俺們走丟~
具體可參考:《TCP/IP-IP尋址與路由控制》

三、“我”在后端服務器中多姿多彩的歷程

? ?啊!路途好遙遠呀,我一路走了很久很久,也遇到了很多很多的中轉站,每次當我不知道怎么走時,路由器大哥都會溫馨的給我指出接下來該走的路途。期間我也走過很多很多路,曾踩著雙絞銅線、同軸電纜、光纖前行,當然,可不要小看俺,就算沒有物理連接的情況下,我也可以通過無線電技術,通過空氣前行呢!

再次聲明,文中所謂的道路,就是指數據傳輸的介質。

3.1、東跑西顛的經歷 - 接入層轉發

? ?走著走著,突然前方遇到一個叫做CDN的老爺爺,它問我說要去哪里,我說要去xx地方辦事,和藹的CDN老爺爺跟我說,我來看看我這里有沒有你要的東西,如果有的話,就不用麻煩你這個小家伙一直跑下去了。可是很遺憾,CDN老爺爺說它哪兒沒有我要的東西,因此我只能繼續前行下去。

? ?記不清過了多久,一路跌跌撞撞,在迷迷糊糊中我來到了一個地方,但當我還在分辨時,刷的一下,很快啊,我就被丟到了其他地方,當我回頭看的時候,發現剛剛哪個地方,大寫著LVS

LVS一般會作為大型網站的網關接入層,負責提供更高的并發性能,具體可參考《億級流量架構設計-LVS篇》

再直視前方,前方有一個東西很眼熟,難道這就是當初聽說過的服務器嗎?帶著一臉疑惑的我慢慢走了進去,我發現內部空間很大,上面漂浮著一塊大陸,名為Linux大陸,上面有好多好多的“城市(進程)”林立著,那我該去哪一座呢?讓我想想!

對了,記起來了好像!!當時出門的時候有人跟我說過:如果你到了目的地之后,不知道該找誰,那么可以根據默認的編號(端口號)去找!

讓我回想一下,HTTP的默認端口是80HTTPS的默認端口是443,我目前屬于HTTPS派別的請求,那么我應該去找編號為443的城市!出發出發~

順著我的推理,我來到了編號443城市的城門口,當我邁進城門后,嗖的一下,我被一個叫做Nginx的大叔抓了過去....

  • Nginx:小家伙,你是來干嘛的?
  • :我帶了一些數據過來找地址為IP:443的地方辦事!
  • Nginx:噢~,原來是這樣啊,我就是負責監聽443編號的守門將。
  • Nginx:小家伙,你過來讓我看看....

話音剛落,Nginx三下五除二的就把我的身體拆開了,然后得到了HTTP報文,然后從HTTP報文的請求行中,發現了我本次旅途的具體目標:/user/862486453028888/posts,然后Nginx大叔又把我組裝了回去,然后根據它內部配置的規則,然后道:

  • Nginx:小家伙,我剛才看了一下,你應該要去的具體位置是xxx.xxx.xxx.xxx:xx,快去吧。
  • :你怎么知道我要去的是這里?
  • Nginx:我剛剛看了一下,你要去的具體位置為IP:443/user/....,根據目前的規則以及我代理的地址,你就應該去這里!
  • :大叔大叔,給我看看你代理了那些地址唄。
  • Nginx:你可以過來看看。
  • :哇,為什么這么多!我可不可以去找其他的地址,找其他人幫我辦事呀?
  • Nginx:不可以噢!按照規則的話,你就應該去我給你的地址哈。
  • :好吧,那我去啦!

這里的規則是什么呢?其實就是Nginxlocation路由匹配規則、upstream代理集群列表以及負載均衡算法,具體可參考:《Nginx篇:反向代理與負載均衡》《負載均衡算法原理篇》

順著Nginx大叔給的地址,我又來到了另外一臺服務器,上面同樣有一塊Linux大陸,然后根據地址在上面找到了一個名為Gateway的東東,聽它自己介紹,好像屬于系統網關。但當我找它辦事時,它卻跟我說:“我不負責具體的業務處理,根據你的目標/user/....,你應該去找Nacos注冊局,問它們要一下USER-SERVICE的具體地址,所以,小家伙你還得繼續奔波哦”!

好的好的,感謝Gateway叔叔指路,那我現在就去啦!

噠噠噠~,邁著愉快的步伐我來到了Nacos注冊局,然后將Gateway叔叔給我的名字:USER-SERVICE交給了它們的工作人員,它們的工作人員經過一番查詢之后告訴我,這個“品牌”多有個分部,你可以去其中任意一處分部處理你的任務,你可以去:xxx.xxx.xxx.xxx:8080這個地址噢!

這里的“品牌”是指后端的具體服務,分部是指服務集群中的每個節點。

好的好的,那我就去你說的這個xxx.xxx.xxx.xxx:8080地址啦!

我一邊在路上走著,一邊想了一下剛剛過程發生的事情,然后把這個經歷畫成了一副邏輯圖,如下:

轉發過程

回去的時候我一定要跟小伙伴們分享一下這個有趣的經歷,耶!

3.2、我遇到了一只大貓咪-叫作Tomcat

? ?根據Nacos給我的地址,我又來到了一臺新的服務器面前,我記得Nacos給了我一個端口號,要我來到這里之后找編號為8080的位置,我順著這個編號慢慢找著,突然在我的前方,出現了一只大老虎,哦不,應該是一只大貓咪,它長這個樣子:

Tomcat

它的長相似乎有些報看,但在它的腦門上正好寫著我要找到8080地址,那我要找的應該就是它了吧!終于到了!我慢慢靠近了這只大貓咪,然后跟它說要找它辦事,Tomcat說要看看我的數據,然后又把我的身體按照之前封裝的方式逆向拆開了,從而還原了我最初的身體-HTTP請求報文,最后Tomcat說:“我確實是你本次要找的最終目標,不過要辦你這件事情得到我肚子里面去噢”!

說罷,Tomcat張開了它的血盆大口,一口將我吞了下去.....,正當我以為我完蛋的時候,我卻發現Tomcat內部別有乾坤,上面似乎也有一塊小陸地漂浮著,當我湊近的時候才看清楚,原來上面寫的是JVM呀!

我二話不說,一腳踏上了這塊陸地,正當我看著上面密密麻麻的“屋子(Java方法)”迷茫時,此時我正前方就走來了一個人,然后對我做了一個自我介紹:

來自遠方的尊敬客人,您好呀,歡迎光臨JVM神州,我叫Thread-xxx,是線程家族的一員,您接下來的整個旅途,我終將陪伴在您左右,您需要辦的所有事情,都會由我代勞,客官這邊請(45度鞠身)~

然后我一邊走著,一邊跟Thread-xxx聊著:

  • :為什么是你來接我呀?
  • 線程:因為每位從遠方到來的客人,我們線程家族都會派遣一位子弟迎接。
  • 線程:本次輪到我了,因而由我為您本次的旅途提供服務。
  • :噢噢噢,那我們接下來該去哪兒呢?
  • 線程:這需要看客官您本次的目的啦!可以讓我看看您本次的旅程嗎?
  • :可以呀,看吧,[我將請求請求行中的資源地址擺了出來]。
  • 線程/user/....,原來您是要去這里呀,這邊請~。
  • 線程:我們首先要去找DispatcherServlet辦事處,才能繼續前行。

PS:接下來是講述Java-SpringMVC框架的執行過程,非Java開發可忽略細節。

隨著Thread-xxx的步伐,我們找到了線程口中所說的DispatcherServlet辦事處,該辦事處的工作人員首先看了一下我本次的具體目的地(資源地址),然后說:您需要先去問一下HandlerMapping管理局,讓它給你找一下具體負責這塊業務的工作室。

緊接著線程Thread-xxx又帶我來到了HandlerMapping管理局找到了其中的管理人員,該管理人員讓我先把要找的資源位置給它,然后只見它拿著我的目標地址作為條件,然后輸入進了查詢器,一瞬間便查出來了我本次的最終目的地:UserController工作室!

線程Thread-xxx道:這就是負責您本次任務的最終工作室啦!我這就帶您過去。

這其實本質上就是SpringMVC中,請求定位具體Java方法的邏輯,但由于之前沒出過《SpringMVC的原理篇》,因此接下來從專業性的角度簡單敘述一下SpringMVC的核心原理。


先上一張SpringMVC的原理圖:

SpringMVC工作原理

觀察如上流程圖,其實看起來難免有些生澀,那此刻咱們換成簡單一點的方式敘述,不再通過這種源碼性的流程去理解。

不知諸位是否還記得,最開始學習SpringMVC時的配置過程,接下來我們簡單回憶一下:

①配置springmvc-servlet.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 通過context:component-scan元素掃描指定包下的控制器-->
    <!-- 掃描com.xxx.xxx及子子孫孫包下的控制器(掃描范圍過大,耗時)-->
    <context:component-scan base-package="com.xxx.xxx"/>
    
    <!-- ViewResolver -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- viewClass需要在pom中引入兩個包:standard.jar and jstl.jar -->
        <property name="viewClass"
                  value="org.springframework.web.servlet.view.JstlView"></property>
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

在這第一步中,最重要的就是配置一下掃描包的位置,以及配置一下視圖解析器。

②配置web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <!-- Spring MVC servlet -->
  <servlet>
    <servlet-name>SpringMVC</servlet-name>
    <!--配置一下DispatcherServlet的位置-->
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--指定springMVC的初始化文件位置,默認值為:/WEB-INF/springmvc-servlet.xml-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/springmvc-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <!--web.xml 3.0的新特性,是否支持異步-->
    <!--<async-supported>true</async-supported>-->
  </servlet>
  
  <!--關鍵!!!配置一條請求路徑映射,"/"代表匹配所有路徑的請求-->
  <!--也就是當有請求到來時,都會被進入前面servlet-name=SpringMVC的servlet中-->
  <servlet-mapping>
    <servlet-name>SpringMVC</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

在第二步中,主要會配置一條請求路徑的映射位置,將進入WEB程序的所有請求全部轉入DispatcherServletdoGet、doPost方法中。
同時由于web.xml中配置了一個servlet:DispatcherServlet,所以在程序啟動時,首先會加載DispatcherServlet,加載時會執行初始化操作,會調用initStrategies()方法:

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

重點看其中的第四個初始化操作:調用initHandlerMappings()方法,由于之前在web.xml中指定了初始化文件的位置:/WEB-INF/springmvc-servlet.xml,那么緊接著SpringMVC會去讀取該配置文件中的base-package掃描包路徑,然后會開始掃描這個包路徑下的所有類:

  • 首先會掃描出該包中帶有@Controller注解的類。
  • 然后會對掃描出的所有類進一步做掃描,會掃描到所有方法上存在@RequestMapping注解的方法。
  • 最后會以兩個注解上配置的值組合起來做為Key,然后通過反射機制,將方法變為一個Method對象,封裝成InvocationHandler實例作為Value,一起加入到一個大的Map容器中。

上述流程下來大致諸位有些暈乎乎的,那么簡單舉個例子:

@Controller("/user")
public class UserController {

    @RequestMapping("/get")
    public String get(User user) {
        ......
    }
    
    @RequestMapping("/add")
    public String add(User user) {
        ......
    }
}

上述這個案例中,最終在初始化之后,會被以下面這種形式加入Map容器:

// 這里是偽代碼,主要是為了闡述邏輯
Map<String,InvocationHandler> map = new HashMap<>();
// 后面的UserController#get()就是以反射獲取到的Method方法實例
map.put("/user/get",InvocationHandler(UserController#get()));
map.put("/user/add",InvocationHandler(UserController#add()));

最終當請求到來時,由于之前web.xml中配置了一條/匹配規則,所有的請求都會被轉入到DispatcherServletdoGet、doPost中,在該方法內首先會以HTTP請求報文-請求行中的資源路徑作為Key,然后在這個Map容器里面進行匹配,從而定位到具體的Java方法并執行。

OK,最后在簡單的把完整流程敘述一遍:

  • 其實在咱們把一個JavaWeb程序打成war包丟入Tomcat并啟動時,Tomcat就會先去加載web.xml文件。
  • 在加載web.xml配置文件時,會碰到DispacherServlet需要被加載。
  • 當加載DispacherServlet時,其實就是把SpringMVC的組件初始化,以及將所有Controller中的URL資源全部映射到容器中存儲。
  • 然后當請求進入Tomcat經過DispacherServlet時,DispacherServlet就去容器中找到這個請求的URL資源。
  • 找到請求的資源路徑對應的Java方法后,會調用組件通過反射機制去執行具體的Controller方法。
  • 當執行完畢之后,又會回到DispacherServlet,此時DispacherServlet又會去調用相關組件處理執行后的結果。
  • 最后當結果處理完成后,才會將渲染后的結果響應回客戶端。

OK~,話接前文,前面經過HandlerMapping管理局的管理人員查詢后,我們已經找到了本次任務處理的具體工作室了...

  • 線程:客官,咱們到了!
  • 線程:這個工作室中已經寫明了您本次任務如何處理的具體步驟,接下的事情都將由我為您效勞。
  • 線程:您要隨我一起去看看具體的處理過程嘛?
  • :好呀,好呀,一起去!

隨著線程的工作開始,我們一路走過了service層、dao/mapper層,在service層辦事時,我們遇到了強大的Redis哥哥,Redis哥哥看到我們之后,問清楚了我們本次到來的目的,然后它說:“來自遠方的貴客,請稍等,讓我先看看我這里有沒有您需要的東西!”

這個場景似曾相識哎,我記得來的路上也有個CDN老爺爺跟我說過同樣的話~

  • Redis:來自遠方的客人,很抱歉我這里沒有您要的東西。
  • Redis:您本次的路途還需繼續前行,您可以去找一下MyBatis哪小子,它也許能夠幫到您。

根據Redis的指示,線程Thread-xxx領著我最終見到了MyBatis,它長這個樣子:

MyBatis

原來Redis哥哥口中的MyBatis竟然是個鳥叔叔[吐舌~]

MyBatis簡單看了一下我本次的任務:

  • 鳥叔:來自遠方的貴客,這件事我確實可以幫到您,請稍等。
  • 然后“鳥叔”一頓操作,竟鼓搗出了一個我看不懂的東西,然后遞給了我。
  • 鳥叔:這個叫做SQL代碼,是你您次任務的必須之物。
  • 鳥叔:你現在可以拿著它,讓Thread-xxx去帶您找一下JDBC哪個老家伙。

慢慢的,線程又帶我找到了“鳥叔”口中所說的JDBC老爺爺,JDBC老爺爺見到我的到來,眼神中并沒有絲毫的意外之情,似乎早已經習以為然,只見JDBC老爺爺抬起消瘦的右手,指著一個地址:

jdbc:mysql://xxx.xxx.xxx.xxx:3306/db_xxxxx

然后道:“小家伙,你又需要再跑一段遠路咯,而且只能你去,Thread-xxx只能在這里等你”。

我:好吧好吧,那我去啦!

又是孤身一人的旅途,難免有些孤獨感襲來,但還好我早已習慣啦!隨著一路奔波,我來到了JDBC老爺爺給出的地址,這里同樣是位于另外一臺服務器的Linux大陸上,我通過3306這個編號找到了一座叫做MySQL的城池,當我踏入之后發現,與之前踏上JVM神州相同,在我剛踏入MySQL這座大城的時候,有一個自稱為DB連接家族的弟子接待了我。

  • DB連接:您好呀,是JVM神州上那位JDBC前輩介紹過來辦事的,對嗎?
  • :對對對,是的,是的。
  • DB連接:好的,那請把您手中的SQL給我噢。
  • DB連接:那是您本次需要做的任務清單,麻煩交給我一下,由我幫你代勞。
  • :昂,那給給你啦[遞過去]~
  • DB連接:好的,這邊有冰闊樂和西瓜,請您稍等片刻,我去去便回。

這里不再展開敘述SQL的執行細節,因為MySQL也是一門較龐大的內容,在開設的《全解MySQL數據庫》專欄中,之后會出一篇:《一條SQL具體是如何執行的?》文章去詳細闡述。

正當我吃完一塊西瓜、喝完一瓶冰闊樂時,DB連接家族的哪位弟子便回來了,同時懷里抱著一大堆東西(數據),然后丟給了我,道:“這便是您本次需要的數據啦,您本次的任務我都按照清單(SQL)上的記錄,給您一一處理了噢”。

:好的,萬分感謝,那我走啦!

順著來時的原路,我飛速的趕回了JVM神州所在的位置,然后映入眼簾的第一眼就是:Thread-xxx哪個家伙在原地站著,老老實實的等候著我的回歸,我悄悄的繞到了Thread-xxx身后,然后從背后拍了一巴掌:

  • :嘿,我回來啦!等了我這么久,有沒有想我~
  • 線程:并未,我是在履行線程家族該有的職責。
  • :額....,無趣。
  • :我事情已經辦好了,我要走了噢。
  • 線程:好的,那由我來送您。

一路跟隨著Thread-xxx的腳步,兜兜轉轉的我們最終又回到了DispatcherServlet辦事處,經過它們內部人員的一頓操作之后,我就打算返航啦!一路走走停停,我走到了JVM神州的邊緣。

  • 線程:遠方的客人,我只能送您到這里啦。
  • :就要說再見了嗎?
  • 線程:是的,按照我們Java線程家族的規則,正常情況下我是不能踏出JVM神州的。
  • :好吧好吧,那就再見啦,Thread-xxx~,我會記得你的。
  • 線程:好的,那祝您歸途一路順風,期待您的下次光臨!再見啦!
  • :拜拜[揮手]~

我告別了Thread-xxx,也從此離開了JVM神州,最終我從Tomcat這只大貓咪的口中飛了出來,正式踏上了歸途。

四、大功告成的我該返航咯 - 服務器響應

? ?諸多經歷過后,現在的我攜帶著本次任務的結果踏上了回家之路,首先我又路過了Gateway叔叔那里,然后我又回到了Nginx大叔所在的城池,不過Nginx大叔把我的身體改為了應答報文結構,并且往其中還寫入了一些東西,聽說是讓我回去交給瀏覽器老大的。

? ?然而在我返航之前,似乎這邊也有加密層、傳輸層、網絡層、鏈路層、物理層這些關卡,和我當時出發的過程一樣,我身上被一層一層的貼了很多東西,并且最終也被改為了0、1組成的身體結構,這個過程是多么的熟悉吶!

我又踏上了哪不知有多遙遠的路途,與來時的路一樣,其中也遇到了很多中轉站,也走過各種各樣的道路,當然,為了防止我迷路,在Nginx大叔那里,也在我的腦袋上貼了一個TCP頭,里面寫清楚了我來自那里,該去向何方.....

在迷迷糊糊中不斷前行,終于看到了我的出生地,看到了網絡進程和瀏覽器老大~,哦豁!我回來啦!

在進入家門之前,我又會經歷物理層、鏈路層、網絡層、傳輸層、TLS層依次解封的過程,主要是為了將我從后端帶回來的數據解析出來。網絡進程在解析到數據后,我的使命就此完成啦!緊接著網絡進程會將數據交給瀏覽器老大,然后老大會派遣一個小弟(渲染進程)對數據進行處理,我瞅了幾眼,大體過程是這樣的:

渲染過程

  • 首先渲染小弟會根據HTML、CSS數據生成DOM結構樹和CSS規則樹。
  • 然后結合結構樹和規則樹生成渲染樹,再根據渲染樹計算每一個節點的布局。
  • 最后根據計算好的布局繪制頁面,繪制完成后通知另一個小弟(呈現器)顯示內容。

最后,因為我至此已經正常返航了,所以為了節省資源開銷,會將我出發前構建的安全通道(TCP、TLS連接)關閉,這個過程會由TCP大哥去經過四次揮手完成,如下:

TCP四次揮手

具體過程可參考:《計網基礎與TCP/IP-TCP四次揮手》

五、網絡之旅篇總結

? ?綜上所述,用戶在瀏覽器地址欄輸入內容后,我們站在一個“網絡請求”的角度,切身感受了一場奇妙的網絡之旅,從客戶端發送請求到服務端返回響應,整個流程咱們都“親身”體驗了一回,最后寫個流程總結:

  • ①用戶在地址欄輸入內容,瀏覽器判斷后生成相應的URL并傳給網絡進程。
  • ②網絡進程先查詢本地緩存,沒有則解析URL并向DNS發送請求,得到IP
  • ③網絡進程先與目標服務器進行TCP、TLS多次握手,建立TCP、TLS安全連接。
  • ④緊接著組裝請求報文,并由各個分層對數據進行封裝,最終轉為0、1格式。
  • ⑤基于建立好的連接,利用物理介質傳輸數據,通過路由器控制數據的傳輸方向。
  • ⑥請求會先去到CDN查詢是否有緩存的內容,如果沒有則繼續向下請求。
  • ⑦請求來到LVS后被轉發到Nginx,再由Nginx轉發到Gateway網關。
  • Gateway網關根據配置好的API分發規則,將請求分發到具體服務。
  • ⑨緊接著再從Nacos注冊中心內,查詢出該服務的具體服務實例IP
  • ⑩請求來到具體的服務器后,先通過端口號找到具體的WEB服務進程Tomcat
  • ?Tomcat基于SpringMVC的工作流程為請求定位到具體的Java后端方法。
  • ?線程執行Java方法時,先去Redis中查詢是否有數據,沒有則查詢MySQL
  • ?查詢DB前先通過MyBatis生成SQL語句,然后再通過DB連接執行SQL
  • ?請求根據已配置的數據源地址,來到MySQL并執行SQL語句,從而獲得數據。
  • ?經過報文組裝、數據封裝、請求轉發等操作,向客戶端響應數據(原路返回)。
  • ?應答報文經物理介質傳輸后,最終抵達客戶端網絡進程(可能會將數據加入緩存)。
  • ?網絡進程將數據交給瀏覽器之后,根據情況準備做TCP四次揮手,斷開連接。
  • ?瀏覽器創建渲染子進程,然后根據數據生成渲染樹,最后繪制并顯示頁面。

至此整個流程結束,當然,這個過程中并未涉及到太多的技術棧,也包括對于整個前/后端系統內部的執行細節并未闡述,這是由于整個系統的全細節執行流程較為龐大,展開敘述之后難以收尾,因而在本篇中則抓住核心點去敘說。

最后,對于請求執行的完整經歷,也畫成了一副流程圖,但由于文件過大會失真,因而可點擊鏈接在線訪問:《瀏覽器輸入URL后究竟發生了什么?》

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,156評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,866評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 174,880評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,398評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,202評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,743評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,822評論 3 438
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 41,962評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,476評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,444評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,579評論 1 365
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,129評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,840評論 3 344
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,231評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,487評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,177評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,568評論 2 370

推薦閱讀更多精彩內容