天天看點

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

作者:java保佑我發大财

引言

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

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

當我們在浏覽器的位址欄中,輸入xxx内容後,浏覽器的程序首先會判斷輸入的内容:

  • 如果是普通的字元,那浏覽器會使用預設的搜尋引擎去對于輸入的xxx生成URL。
  • 如若輸入的是網址,那浏覽器會拼接協定名形成完整的URL。

當然,在位址欄中輸入某個内容後,也會進行一些額外操作,例如:安全檢查、通路限制等,但總歸而言,浏覽器做的第一件工作則是生成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,這關乎到後續預設使用的端口号。
  • 然後會解析得到域名,這個将關乎到後續具體請求的伺服器位址。

假設浏覽器傳輸過來的URL為https://juejin.cn/user/862486453028888/posts,那麼在這個階段會确定後續請求的伺服器端口号為443,請求的目标域名為www.juejin.cn。其實在這裡主要是根據浏覽器的輸入資訊,去解析出一些“誕生我(請求)”的前置要素。

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

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

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

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

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

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

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

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

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

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

2.4、誕生“我的身體” - 建構請求封包

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅
偷偷吐槽一句:TCP大哥貼的傳輸頭裡面,放了好多好多東西,讓我感覺腦袋沉沉的。

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

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

不過剛剛對外連結路層的時候,好像有個人跟我說:你這個樣子是無法在媒體上行走的,你要記得改變一下啊!

我還沒聽得太清楚,就來到了實體層這關,這層和之前我“家裡”以及之前的關卡環境都不一樣,實體層的小夥伴們好像都有實際的形态,但之前接觸所有内容都是虛拟的概念形态哎~。

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

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

2.5、踏上路途的我 - 資料傳輸

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

三、“我”在後端伺服器中多姿多彩的曆程

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

3.1、東跑西颠的經曆 - 接入層轉發

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

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

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

讓我回想一下,HTTP的預設端口是80,HTTPS的預設端口是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:不可以噢!按照規則的話,你就應該去我給你的位址哈。
  • 我:好吧,那我去啦!

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

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

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

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

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

根據Nacos給我的位址,我又來到了一台新的伺服器面前,我記得Nacos給了我一個端口号,要我來到這裡之後找編号為8080的位置,我順着這個編号慢慢找着,突然在我的前方,出現了一隻大老虎,哦不,應該是一隻大貓咪,它長這個樣子:

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

它的長相似乎有些報看,但在它的腦門上正好寫着我要找到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道:這就是負責您本次任務的最終工作室啦!我這就帶您過去。

先上一張SpringMVC的原理圖:

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

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

不知諸位是否還記得,最開始學習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程式的所有請求全部轉入DispatcherServlet的doGet、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中配置了一條/比對規則,所有的請求都會被轉入到DispatcherServlet的doGet、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又會去調用相關元件處理執行後的結果。
  • 最後當結果處理完成後,才會将渲染後的結果響應回用戶端。
  • 線程:客官,咱們到了!
  • 線程:這個工作室中已經寫明了您本次任務如何處理的具體步驟,接下的事情都将由我為您效勞。
  • 線程:您要随我一起去看看具體的處理過程嘛?
  • 我:好呀,好呀,一起去!

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

這個場景似曾相識哎,我記得來的路上也有個CDN老爺爺跟我說過同樣的話~
  • Redis:來自遠方的客人,很抱歉我這裡沒有您要的東西。
  • Redis:您本次的路途還需繼續前行,您可以去找一下MyBatis哪小子,它也許能夠幫到您。

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅
原來Redis哥哥口中的MyBatis竟然是個鳥叔叔[吐舌~]

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

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

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

然後道:“小家夥,你又需要再跑一段遠路咯,而且隻能你去,Thread-xxx隻能在這裡等你”。

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

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

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

正當我吃完一塊西瓜、喝完一瓶冰闊樂時,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層依次解封的過程,主要是為了将我從後端帶回來的資料解析出來。網絡程序在解析到資料後,我的使命就此完成啦!緊接着網絡程序會将資料交給浏覽器老大,然後老大會派遣一個小弟(渲染程序)對資料進行處理,我瞅了幾眼,大體過程是這樣的:

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅
  • 首先渲染小弟會根據HTML、CSS資料生成DOM結構樹和CSS規則樹。
  • 然後結合結構樹和規則樹生成渲染樹,再根據渲染樹計算每一個節點的布局。
  • 最後根據計算好的布局繪制頁面,繪制完成後通知另一個小弟(呈現器)顯示内容。

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

網絡程式設計之化身一個請求感受浏覽器輸入URL後奇妙的網絡之旅

五、網絡之旅篇總結

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

  • ①使用者在位址欄輸入内容,浏覽器判斷後生成相應的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四次揮手,斷開連接配接。
  • ⑱浏覽器建立渲染子程序,然後根據資料生成渲染樹,最後繪制并顯示頁面。

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