引言
? ?在瀏覽器上輸入一個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
中每個字段的釋義如下:
-
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
的“大家族”是專門負責這個的!我要去找它們問問看~
不過在問
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
大叔后,獲得了目的地址的我,此時已經知道該去往何處啦!但在正式出發前,由于前路坎坷,途中會存在各類危機(網絡阻塞、網絡延遲、第三方劫持等),因此為了我的安全出行,首先還需為我建立一條安全的通道,所以我還需要等一會兒才能出發,俺們一起來瞅瞅建立安全通道的過程是什么樣的:
看著好復雜啊~,但似乎大體就分為了兩個過程:
首先是
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
大哥貼的傳輸頭里面,放了好多好多東西,讓我感覺腦袋沉沉的。
過了傳輸層這一站之后,我又來到了網絡層,果不其然,網絡層里面最常見的還是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
的默認端口是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
的location
路由匹配規則、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
的位置,我順著這個編號慢慢找著,突然在我的前方,出現了一只大老虎,哦不,應該是一只大貓咪,它長這個樣子:
它的長相似乎有些報看,但在它的腦門上正好寫著我要找到
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-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
又會去調用相關組件處理執行后的結果。 - 最后當結果處理完成后,才會將渲染后的結果響應回客戶端。
OK~,話接前文,前面經過
HandlerMapping
管理局的管理人員查詢后,我們已經找到了本次任務處理的具體工作室了...
- 線程:客官,咱們到了!
- 線程:這個工作室中已經寫明了您本次任務如何處理的具體步驟,接下的事情都將由我為您效勞。
- 線程:您要隨我一起去看看具體的處理過程嘛?
- 我:好呀,好呀,一起去!
隨著線程的工作開始,我們一路走過了service
層、dao/mapper
層,在service
層辦事時,我們遇到了強大的Redis
哥哥,Redis
哥哥看到我們之后,問清楚了我們本次到來的目的,然后它說:“來自遠方的貴客,請稍等,讓我先看看我這里有沒有您需要的東西!”
這個場景似曾相識哎,我記得來的路上也有個
CDN
老爺爺跟我說過同樣的話~
- Redis:來自遠方的客人,很抱歉我這里沒有您要的東西。
-
Redis:您本次的路途還需繼續前行,您可以去找一下
MyBatis
哪小子,它也許能夠幫到您。
根據Redis
的指示,線程Thread-xxx
領著我最終見到了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/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后究竟發生了什么?》