小白科普:從輸入網(wǎng)址到最后瀏覽器呈現(xiàn)頁(yè)面內(nèi)容,中間發(fā)生了什么?
1.前言
這篇文章是應(yīng)網(wǎng)友之邀所寫(xiě),主要描述一下我們?cè)L問(wèn)網(wǎng)站時(shí), 從輸入網(wǎng)址到最后瀏覽器呈現(xiàn)內(nèi)容,中間發(fā)生了什么。
之前寫(xiě)過(guò)兩篇文章《我是一個(gè)網(wǎng)卡》,《我是一個(gè)路由器》描述了一個(gè)電腦如何通過(guò)DHCP、ARP、NAT等上式獲取IP、然后訪問(wèn)網(wǎng)絡(luò)的過(guò)程,主要專(zhuān)注在傳輸層和網(wǎng)絡(luò)層。
今天的文章主要專(zhuān)注于應(yīng)用層,我拿了一個(gè)很簡(jiǎn)單的網(wǎng)絡(luò)結(jié)構(gòu)來(lái)講。假定本機(jī)已經(jīng)獲取了IP地址,各種網(wǎng)絡(luò)基礎(chǔ)設(shè)施已經(jīng)準(zhǔn)備好了。
由于知識(shí)點(diǎn)太多,我肯定會(huì)漏掉部分內(nèi)容,歡迎在留言中補(bǔ)充, 以后我會(huì)根據(jù)大家建議再寫(xiě)文章擴(kuò)展。
2.準(zhǔn)備
當(dāng)你在瀏覽器中輸入網(wǎng)址(例如www.coder.com)并且敲了回車(chē)以后, 瀏覽器首先要做的事情就是獲得coder.com的IP地址,具體的做法就是發(fā)送一個(gè)UDP的包給DNS服務(wù)器,DNS服務(wù)器會(huì)返回coder.com的IP, 這時(shí)候?yàn)g覽器通常會(huì)把IP地址給緩存起來(lái),這樣下次訪問(wèn)就會(huì)加快。
比如Chrome, 你可以通過(guò)chrome://net-internals/#dns來(lái)查看。
有了服務(wù)器的IP, 瀏覽器就要可以發(fā)起HTTP請(qǐng)求了,但是HTTP Request/Response必須在TCP這個(gè)“虛擬的連接”上來(lái)發(fā)送和接收。
想要建立“虛擬的”TCP連接,TCP郵差需要知道4個(gè)東西:(本機(jī)IP, 本機(jī)端口,服務(wù)器IP, 服務(wù)器端口),現(xiàn)在只知道了本機(jī)IP,服務(wù)器IP, 兩個(gè)端口怎么辦?
本機(jī)端口很簡(jiǎn)單,操作系統(tǒng)可以給瀏覽器隨機(jī)分配一個(gè), 服務(wù)器端口更簡(jiǎn)單,用的是一個(gè)“眾所周知”的端口,HTTP服務(wù)就是80, 我們直接告訴TCP郵差就行。
經(jīng)過(guò)三次握手以后,客戶(hù)端和服務(wù)器端的TCP連接就建立起來(lái)了! 終于可以發(fā)送HTTP請(qǐng)求了。
之所以把TCP連接畫(huà)成虛線,是因?yàn)檫@個(gè)連接是虛擬的, 詳情可參見(jiàn)之前的文章《TCP/IP之大明郵差》,《張大胖的Socket》
3.Web服務(wù)器
一個(gè)HTTP GET請(qǐng)求經(jīng)過(guò)千山萬(wàn)水,歷經(jīng)多個(gè)路由器的轉(zhuǎn)發(fā),終于到達(dá)服務(wù)器端(HTTP數(shù)據(jù)包可能被下層進(jìn)行分片傳輸,略去不表)。
Web服務(wù)器需要著手處理了,它有三種方式來(lái)處理:
(1) 可以用一個(gè)線程來(lái)處理所有請(qǐng)求,同一時(shí)刻只能處理一個(gè),這種結(jié)構(gòu)易于實(shí)現(xiàn),但是這樣會(huì)造成嚴(yán)重的性能問(wèn)題。
(2) 可以為每個(gè)請(qǐng)求分配一個(gè)進(jìn)程/線程,但是當(dāng)連接太多的時(shí)候,服務(wù)器端的進(jìn)程/線程會(huì)耗費(fèi)大量?jī)?nèi)存資源,進(jìn)程/線程的切換也會(huì)讓CPU不堪重負(fù)。
(3) 復(fù)用I/O的方式,很多Web服務(wù)器都采用了復(fù)用結(jié)構(gòu),例如通過(guò)epoll的方式監(jiān)視所有的連接,當(dāng)連接的狀態(tài)發(fā)生變化(如有數(shù)據(jù)可讀), 才用一個(gè)進(jìn)程/線程對(duì)那個(gè)連接進(jìn)行處理,處理完以后繼續(xù)監(jiān)視,等待下次狀態(tài)變化。 用這種方式可以用少量的進(jìn)程/線程應(yīng)對(duì)成千上萬(wàn)的連接請(qǐng)求。
(碼農(nóng)翻身注:詳情參見(jiàn)《Http Server:一個(gè)差生的逆襲》)
我們使用Nginx這個(gè)非常流行的Web服務(wù)器來(lái)繼續(xù)下面的故事。
對(duì)于HTTP GET請(qǐng)求,Nginx利用epoll的方式給讀取了出來(lái), Nginx接下來(lái)要判斷,這是個(gè)靜態(tài)的請(qǐng)求還是個(gè)動(dòng)態(tài)的請(qǐng)求???
如果是靜態(tài)的請(qǐng)求(HTML文件,JavaScript文件,CSS文件,圖片等),也許自己就能搞定了(當(dāng)然依賴(lài)于Nginx配置,可能轉(zhuǎn)發(fā)到別的緩存服務(wù)器去),讀取本機(jī)硬盤(pán)上的相關(guān)文件,直接返回。
如果是動(dòng)態(tài)的請(qǐng)求,需要后端服務(wù)器(如Tomcat)處理以后才能返回,那就需要向Tomcat轉(zhuǎn)發(fā),如果后端的Tomcat還不止一個(gè),那就需要按照某種策略選取一個(gè)。
例如Ngnix支持這么幾種:
- 輪詢(xún):按照次序挨個(gè)向后端服務(wù)器轉(zhuǎn)發(fā)
- 權(quán)重:給每個(gè)后端服務(wù)器指定一個(gè)權(quán)重,相當(dāng)于向后端服務(wù)器轉(zhuǎn)發(fā)的幾率。
- ip_hash: 根據(jù)ip做一個(gè)hash操作,然后找個(gè)服務(wù)器轉(zhuǎn)發(fā),這樣的話同一個(gè)客戶(hù)端ip總是會(huì)轉(zhuǎn)發(fā)到同一個(gè)后端服務(wù)器。
- fair:根據(jù)后端服務(wù)器的響應(yīng)時(shí)間來(lái)分配請(qǐng)求,響應(yīng)時(shí)間段的優(yōu)先分配。
不管用哪種算法,某個(gè)后端服務(wù)器最終被選中,然后Nginx需要把HTTP Request轉(zhuǎn)發(fā)給后端的Tomcat,并且把Tomcat輸出的HttpResponse再轉(zhuǎn)發(fā)給瀏覽器。
由此可見(jiàn),Nginx在這種場(chǎng)景下,是一個(gè)代理人的角色。
5.應(yīng)用服務(wù)器
Http Request終于來(lái)到了Tomcat,這是一個(gè)由Java寫(xiě)的、可以處理Servlet/JSP的容器,我們的代碼就運(yùn)行在這個(gè)容器之中。
如同Web服務(wù)器一樣, Tomcat也可能為每個(gè)請(qǐng)求分配一個(gè)線程去處理,即通常所說(shuō)的BIO模式(Blocking I/O 模式)。
也可能使用I/O多路復(fù)用技術(shù),僅僅使用若干線程來(lái)處理所有請(qǐng)求,即NIO模式。
不管用哪種方式,Http Request 都會(huì)被交給某個(gè)Servlet處理,這個(gè)Servlet又會(huì)把Http Request做轉(zhuǎn)換,變成框架所使用的參數(shù)格式,然后分發(fā)給某個(gè)Controller(如果你是在用Spring)或者Action(如果你是在Struts)。
剩下的故事就比較簡(jiǎn)單了(不,對(duì)碼農(nóng)來(lái)說(shuō),其實(shí)是最復(fù)雜的部分),就是執(zhí)行碼農(nóng)經(jīng)常寫(xiě)的增刪改查邏輯,在這個(gè)過(guò)程中很有可能和緩存、數(shù)據(jù)庫(kù)等后端組件打交道,最終返回HTTP Response,由于細(xì)節(jié)依賴(lài)業(yè)務(wù)邏輯,略去不表。
根據(jù)我們的例子,這個(gè)HTTP Response應(yīng)該是一個(gè)HTML頁(yè)面。
6.歸途
Tomcat很高興地把Http Response發(fā)給了Ngnix 。
Ngnix也很高興地把Http Response 發(fā)給了瀏覽器。
發(fā)完以后TCP連接能關(guān)閉嗎?
如果使用的是HTTP1.1, 這個(gè)連接默認(rèn)是keep-alive,也就是說(shuō)不能關(guān)閉;
如果是HTTP1.0,要看看之前的HTTP Request Header中有沒(méi)有Connetion:keep-alive,如果有,那也不能關(guān)閉。
7.瀏覽器再次工作
瀏覽器收到了Http Response,從其中讀取了HTML頁(yè)面,開(kāi)始準(zhǔn)備顯示這個(gè)頁(yè)面。
但是這個(gè)HTML頁(yè)面中可能引用了大量其他資源,例如js文件,CSS文件,圖片等,這些資源也位于服務(wù)器端,并且可能位于另外一個(gè)域名下面,例如static.coder.com。
瀏覽器沒(méi)有辦法,只好一個(gè)個(gè)地下載,從使用DNS獲取IP開(kāi)始,之前做過(guò)的事情還要再來(lái)一遍。不同之處在于不會(huì)再有應(yīng)用服務(wù)器如Tomcat的介入了。
如果需要下載的外部資源太多,瀏覽器會(huì)創(chuàng)建多個(gè)TCP連接,并行地去下載。
但是同一時(shí)間對(duì)同一域名下的請(qǐng)求數(shù)量也不能太多,要不然服務(wù)器訪問(wèn)量太大,受不了。所以瀏覽器要限制一下, 例如Chrome在Http1.1下只能并行地下載6個(gè)資源。
當(dāng)服務(wù)器給瀏覽器發(fā)送JS,CSS這些文件時(shí),會(huì)告訴瀏覽器這些文件什么時(shí)候過(guò)期(使用Cache-Control或者Expire),瀏覽器可以把文件緩存到本地,當(dāng)?shù)诙握?qǐng)求同樣的文件時(shí),如果不過(guò)期,直接從本地取就可以了。
如果過(guò)期了,瀏覽器就可以詢(xún)問(wèn)服務(wù)器端,文件有沒(méi)有修改過(guò)?(依據(jù)是上一次服務(wù)器發(fā)送的Last-Modified和ETag),如果沒(méi)有修改過(guò)(304 Not Modified),還可以使用緩存。否則的話服務(wù)器就會(huì)被最新的文件發(fā)回到瀏覽器。
當(dāng)然如果你按了Ctrl+F5,會(huì)強(qiáng)制地發(fā)出GET請(qǐng)求,完全無(wú)視緩存。
注:在Chrome下,可以通過(guò) chrome://view-http-cache/ 命令來(lái)查看緩存。
現(xiàn)在瀏覽器得到了三個(gè)重要的東西:
1.HTML ,瀏覽器把它變成DOM Tree
2. CSS, 瀏覽器把它變成CSS Rule Tree
3. JavaScript, 它可以修改DOM Tree
瀏覽器會(huì)通過(guò)DOM Tree和CSS Rule Tree生成所謂“Render Tree”,計(jì)算每個(gè)元素的位置/大小,進(jìn)行布局,然后調(diào)用操作系統(tǒng)的API進(jìn)行繪制,這是一個(gè)非常復(fù)雜的過(guò)程,略去不表。
到目前為止,我們終于在瀏覽器中看到了www.coder.com的內(nèi)容。
【本文為51CTO專(zhuān)欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)作者微信公眾號(hào)coderising獲取授權(quán)】







































