引言
在大廠面試過程中經常會遇見一些“變態”的題目,這些題目如果沒有提前準備,一時間還真無法下手,這個系列文章我就想總結總結這些經常會被問到的“變態”題目的應對策略。
總
“說說從網址輸入瀏覽器到網頁呈現到屏幕上,這個過程中發生了什么?”
在大廠面試中經常會遇到這個問題,考得就是計算機網絡的基礎是否扎實,思維是否開放,然而面試過程中嘗嘗一緊張,就只記得一個HTTP請求了,然后面試官就會很不滿意,覺得面試者網絡的基礎很差,特地寫這篇文章總結了一下。
這個過程說起來復雜,但是概括起來說就是以下四個步驟:
- DNS解析
- HTTP請求
- 服務端處理和響應
- 瀏覽器渲染
DNS解析
首先就是要將域名映射成其對應的IP地址,細節步驟如下:
1、去瀏覽器緩存中尋找映射
瀏覽器會緩存這種映射關系,如果在瀏覽器緩存中存在的話就優先在瀏覽器緩存中查找
2、去操作系統緩存中尋找映射
操作系統也會緩存DNS映射,這一步需要設計到系統調用
3、去路由器緩存中尋找映射
路由器中同樣有緩存
4、去本地域名服務器(local name server)中尋找
本地域名服務器(又叫做默認域名服務器)往往就是你在操作系統中配置DNS Server(以win10中DNS配置界面為例):
不過現在大多數情況下采用的都是自動獲取DNS服務器地址了,在你的電腦連上網的一瞬間,電腦會在內網發出一個廣播,當本地域名服務器收到這個廣播之后會進行響應,然后你的電腦就可以根據這個響應的地址來自動配置DNS Server了。
本地域名服務器會有DNS緩存,如果命中的話就會直接從本地域名服務器中返回,不再繼續向上請求。
5、繼續進行遞歸查詢與迭代查詢
如果本地域名服務器中沒有命中,那么就必須去更上層的域名服務器中查找了,域名服務器的層級關系如下:
- 根域名服務器:最高層次的域名服務器,它不會直接存儲域名與ip的映射關系,存儲的其實是頂級域名服務器的域名及其ip地址
- 頂級域名服務器(TLD服務器):管理該頂級域名中所有二級域名服務器,當收到查詢請求時,可能直接返回結果或者下一步應該查詢的域名服務器地址
- 權限域名服務器:負責一個小區域的域名到ip的映射,當收到查詢請求時,可能直接返回結果或者下一步應該查詢的域名服務器地址
如果沒有命中本地域名服務器的話,就會委托本地域名服務器進行“遞歸查詢”,所謂“遞歸查詢”,就是指本地域名服務器會代替你作為DNS客戶端請求根域名服務器。遞歸查詢的含義如圖:
前面介紹過,根域名服務器中并不直接存儲域名到ip的映射關系,而是會返回你下一個應該找的域名服務器(某個頂級域名服務器),本地域名服務器在收到下一個應該找的服務器的地址之后會繼續去請求下一個服務器,不斷重復這個過程,這個過程被稱為“迭代查詢”:
請求域名服務器的總體過程如圖:
從圖中可見,這個過程是一個遞歸查詢與迭代查詢相結合的過程,從主機到本地域名服務器是遞歸查詢,而從本地域名服務器再到更高級別則是迭代查詢。
DNS映射只能將一個域名映射成一個IP,但是現在的系統處于性能的需要,往往會將應用拷貝好幾份到數臺機器上(即有好幾個IP),這個時候就需要另外的技術來實現對這些IP的負載均衡:
- DNS輪詢(Round-robin DNS):對于每個映射請求,順序選擇IP列表的下一個IP返回,循環往復
- 負載均衡器:一個在特定的IP地址上監聽,并負責將請求負載到集群中某臺機器的設備。負載均衡器在硬件層面上有f5,操作系統層面上有lvs,軟件層面上有Nginx,通過層層負載可以對系統進行垂直擴展,通過和上面DNS輪詢配合可以實現系統的水平擴展
- 地理DNS(Geographic DNS):根據客戶端所處的地理位置,解析到離客戶端最近的一臺服務器的IP,CDN經常采用這種方式
- 任播技術(AnyCast):后面提到的DNS的根域名服務器就是使用的這種技術,不過這種技術因為和TCP協議的適應性不是很好,所以在企業中很少使用。
HTTP請求
解析完成后便會將HTTP報文發送請求過去,HTTP請求是基于TCP連接的,所以會先進行TCP三次握手建立連接,HTTP請求報文的內容主要包括請求行,請求頭和請求體構成。
有很多抓取Http報文的工具,比如Chrome瀏覽器自帶的開發者工具,Fiddler等,我用Fiddler隨意抓取了一個HTTP報文如下:
GET http://pos.baidu.com/qctm?conwid=172&conhei=425 HTTP/1.1
Host: pos.baidu.com
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36
Referer: http://img2.mini.cache.wps.cn/mini/static/wps/rightad/42.html
Accept-Encoding: gzip, deflate
Accept-Language: zh_CN
Cookie: pgv_pvi=4605897728; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0;
可以看出HTTP是一個純文本的協議,非常地可讀,上面報文的第一行GET http://pos.baidu.com/qctm?conwid=172&conhei=425 HTTP/1.1
,稱為請求行,表示使用HTTP的GET方法請求鏈接http://pos.baidu.com/qctm?conwid=172&conhei=425
,使用的HTTP協議版本是HTTP/1.1。
第一行以外剩下的內容都是請求頭,可以看到請求頭攜帶了很多信息過去,通過User-Agent字段告知了服務器自己的操作系統類型,瀏覽器類型等等,Referer字段表示這個請求來源于哪個頁面,自己接受什么類型的響應(Accept-Encoding和Accept-Language)等等,w3c有一套完整的規范(RFC文檔)約束HTTP請求頭中有哪些字段以及每個字段的含義。
HTTP協議是一個無狀態的協議,即服務器無法感知到兩次請求的不同,為了彌補這個不足,這個請求頭還攜帶了Cookie,Cookie本質上就是一個用于在不同請求之間同步用戶狀態的kv對,這個Cookier是上一次請求服務器時服務器響應給我的,瀏覽器會負責維護屬于不同網站的Cookie,在請求需要時自動將其攜帶進請求頭中。
在該請求頭中還有一個Connection: keep-alive
字段,這個實在HTTP/1.1中新增的字段,之前的HTTP/1.0,默認每次HTTP連接都是短連接,即完成一個響應后就立即關閉,使用了keep-alive就可以多個請求響應共用一個tcp連接,節約創建和銷毀鏈接的開銷。
該請求報文中沒有請求體,原因是GET方法的HTTP請求都不包含請求體,從字面意思可以看出"GET"是獲取內容的意思,僅僅是獲取內容當然就不需要自己攜帶什么參數,所以就沒有請求體,當然實際開發中如果實在想要通過GET方法傳參數的話,可以通過在請求的URL后面加個"?"以及kv對的形式,比如之前那個請求url:http://pos.baidu.com/qctm?conwid=172&conhei=425
,可以改看出它請求http://pos.baidu.com/qctm
,并且傳遞了兩個參數,一個是conwid
,值為172
,另一個是conhei
,值為425
。
除了GET方法外,開發中最經常用的就是POST方法,POST方法是可以攜帶請求體的,只需要把上面所說的kv對放入請求體中即可達到和上面一樣的攜帶參數的效果(請求體和請求頭中間要空一行),使用POST方法的優勢就是,當報文內容太多時,超出了url的最大長度限制,GET方法無法承受,此時最好就使用POST方法了。
GET方法和POST方法還有一點重大區別就是,GET方法會優先使用瀏覽器緩存,而POST方法每次都會重新請求,很多情況下選擇使用GET方法就是為了合理使用瀏覽器緩存來提升用戶體驗。
GET方法和POST方法外加攜帶一些參數理論上已經可以滿足全部的開發需求,但是HTTP遠不止這兩種方法,HTTP的方法與含義如下:
近些年興起的RESTful風格的url主張要充分利用HTTP中這些方法的語義,盡可能地將參數放置在url路徑中,而不是總是使用GET或者POST方法傳一堆雜亂的參數,一般認為RESTful風格的URL更加可讀。
上面說過HTTP協議是一個純文本協議,因而保密性不是很好,而且也缺乏身份認證機制,所以現在現在大多數網站用的都是http的一個改良版本,稱為HTTPS,它在HTTP和TCP之間增加了一個SSL層(安全套接字層),它占用的是443號端口,而HTTP占用的是80號端口,現在的趨勢是全網使用HTTPS,以保證網絡傳輸的安全,使用fiddler想抓到純http的包很難了,想要更多地了解HTTPS的話可以參考我之前的寫的一篇文章:HTTPS原理總結
服務端處理和響應
按照現代動態網站后端的一般架構,在服務端會先進行好幾輪的負載均衡,然后才能到達真正處理請求的后端程序,如下圖:
這之前在將DNS對IP的負載時也提到過一些,你可能會奇怪為什么圖中第一層的負載均衡器lvs有兩個,其實在同一個時刻,只有其中一個lvs在起作用,它叫做Master,另一個叫做Backup,當Master因為意外而退出的時候,此時Backup就會切換為Master,負責這個監測和切換工作的就是圖中的Keepalived軟件,它使用的技術稱為虛擬ip(virtual ip),在一開始的時候Master占有這個虛擬ip,Master掛了后,Backup則會將這個虛擬IP搶占過來。
經過了負載均衡后達到了后臺應用,后臺應用就是一段用于處理請求的程序(可能是由Java,Python,Ruby,Scala等各種編程語言編寫),這些語言一般也都有自己的應用容器,其實也是個服務器程序,比如Java里面最經常使用的tomcat,程序所做的事情大多就是對數據庫進行各種增刪改查,企業中最常用的數據庫就是mysql和orcale,當網站的數據量或者訪問量特別大的時候,可能需要考慮進行分庫分表,方法包括分片(Sharding),復制(Replication),或者使用一些實現了弱一致性語義的數據庫,比如MongoDB,HBase等等。
后端程序處理完后會返回給客戶端一個HTTP響應,HTTP響應的格式與HTTP請求的格式類似:
HTTP/1.1 200 OK
Cache-Control: private, no-store, no-cache, must-revalidate, post-check=0,
pre-check=0
Expires: Sat, 01 Jan 2000 00:00:00 GMT
P3P: CP="DSP LAW"
Pragma: no-cache
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
X-Cnection: close
Transfer-Encoding: chunked
Date: Fri, 12 Feb 2010 09:05:55 GMT
2b3
????????T?n?@????
第一行是響應行,再往下面的內容,空行之前是響應頭,空行之后是響應體。
亂碼部分是經過壓縮的響應體,壓縮算法就是響應頭中Content-Encoding
字段中的gzip
算法,進過gzip解壓縮即可看到實際內容:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en" id="facebook" class=" no_js">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-language" content="en" />
...
往往會發現其實是一個html頁面。
響應行由協議版本,響應碼和響應碼的描述組成,200就是代表成功的響應碼,還有其他很多其他響應碼,但是他們一般遵循以下規律:
響應碼 | 含義 |
---|---|
1xx | 表示通知信息,如請求收到了或者正在處理 |
2xx | 表示成功 |
3xx | 表示重定向,如要完成請求還必須進一步的行動 |
4xx | 客戶端錯誤 |
5xx | 服務器錯誤 |
瀏覽器渲染頁面
瀏覽器通過上面的請求拿到html后就開始進行渲染,瀏覽器會先渲染出一個大概的結構出來,因為現在前端各項技術的分工非常明確,通過html瀏覽器只能知道結構,然后去請求嵌入在html中的實體,比如圖片,css和js,通過css瀏覽器就知道了網站的樣式,通過js瀏覽器就知道了網站的行為
- 圖片: img標簽
<img src="https://xxxxxx/xxxx.png"/>
- css樣式表,用于決定網站的樣式
<link href="https://xxxxx/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet">
- js, 即JavaScript,用于決定網站的行為
<script src="http://xxxxxxx/jquery/2.1.4/jquery.min.js"></script>
當遇到上面三種標簽的時候,瀏覽器都要為他們每一個單獨發起一個HTTP請求,這中間的過程和前面說的過程一樣。
但是對于html,css和js這樣的靜態資源,基本上是走不到網站的后端的,首先瀏覽器會緩存他們,通過響應頭的Expires
字段,瀏覽器知道他們什么時候會過期,如果沒有過期,就直接從瀏覽器緩存中獲取了。還有,返回html頁面的那個響應報文上很有可能會有一個ETag
字段,ETag
類似于版本號,如果瀏覽器發現自己的緩存里有這個版本號的html的嵌入實體資源,則直接從緩存返回。
即使瀏覽器緩存中沒有,大多數現代的網站都把靜態資源托管到CDN上了,CDN全稱叫內容分發網絡,CDN服務器遍布全國各地,你對于靜態資源的請求往往會被負載到一個比較近的CDN服務器上。
在html中的各項嵌入實體也加載完畢后,瀏覽器開始發出AJAX請求。
在過去AJAX技術沒有被廣泛使用的時候,每次頁面有一丁點動態變化(哪怕只改了一個字),都必須向后端請求一個完整的新網頁,這顯然是很低效的,利用這項技術可以每次只向后端索要少量的數據然后只刷新部分網頁,而不用每次都去請求一個完整的網頁。
上面的介紹聽上去比較玄乎,其實就是在網頁的js代碼中發起http請求向服務器要數據,數據來了后再使用js代碼根據數據對頁面進行動態更新,從這里可以看出前端變得比以前復雜了。
AJAX這個看上去很詭異的單詞其實是"Asynchronous Javascript And XML"的縮寫,中文可以強行翻譯為"異步Javascript和XML",然而名不副實,現在AJAX的前后端交互基本上傳輸的都是json格式,xml格式已經用得非常少了,但是這個名稱還是遺留了下來。
AJAX技術催生了一種叫做WebAPP的架構,舉個例子,現在有很多網頁版office,網頁版photoshop,以及種種將復雜交互集中于一個頁面的應用,這些應用往往只需要在你第一次訪問的時候將頁面下載到本地,之后都使用AJAX與后端交互,然后就能給人一種很接近本地應用的感覺。
AJAX也帶來了前后端分離的趨勢,以前前端只負責寫好一個靜態的頁面,然后后端負責使用模板語言為其添加動態效果,這個模板常常會導致一個分工不明確的模糊地帶,前后端分離則主張將后臺徹底變成一個數據接口,所有交互都由前端掌控,聽上去就是WebAPP的架構,然而并不是所有應用都是WebAPP,淘寶網首先想到了在前端和后端之間引入一個NodeJS中間層(其實NodeJs就是一個能夠執行js語言的服務端應用容器),將前端掌控的范圍進一步擴大到了服務端,更精確的說是MVC中的Controller,這樣方便前端進一步掌控交互邏輯,對交互的性能進行優化。
如果頁面渲染時間太長,會給用戶不太好的體驗,現在有很多降低渲染時長的方法,比如FaceBook發明的BigPipe技術,正常情況下用戶體驗到的動態網站的延時由三部分組成:服務器生成頁面的時間,網絡傳輸的時間和瀏覽器渲染的時間
而BigPipe則將頁面分為很多個部分(稱為一個pagelet),然后在服務器和瀏覽器之間建立一個持續的連接,每一部分類似于流水線一樣源源不斷地從服務器傳輸到瀏覽器。BigPipe首先會選擇輸送一個框架性的HTML結構,然后瀏覽器就可以先根據這個結構進行渲染,然后其他部分再源源不斷地加載進來,用戶就會感覺變快了:
BigPipe的實現依賴于HTTP 1.1所引入的分塊傳輸編碼,即HTTP的頭部字段Transfer-Encoding
的值為chunked
,那么消息體就可以由數量不確定的塊組成,并且以最后一個大小為0的塊結束。
其他補充
從上面的流程來看,都是瀏覽器在需要資源的時候,主動發送請求去服務器上拉(pull)數據,但是有的場景下是需要服務器主動向客戶端推(push)數據的,比如網頁版的聊天室,當你的朋友向你發送一條消息,服務器收到后,必須主動將消息推給你,你才能在瀏覽器上看到。
然而HTTP原生是不支持服務器"推送"的,所以人們在一開始發明很多投機取巧的方法,稱之為Comet,Comet有兩種經典的實現,一個是長輪詢(long polling),即客戶端發出AJAX請求后,服務器將請求一直阻塞在那里直到超時或者服務器有數據推送,一條連接超時后,瀏覽器緊接著開啟下一個長連接,因為每次連接的時間都很長,所以稱之為“長輪詢”。另一個種實現就是借助iframe標簽的src屬性,然后服務器就像“長輪詢”的那樣把iframe標簽的請求阻塞住,推送,超時,重建請求。
后來瀏覽器開始支持WebSocket協議,這是一個雙向的通道,服務端和瀏覽器只需要進過一次HTTP請求進行協議升級后,即可進行基于WebSocket的雙向通信。
在HTTP/2中直接將服務器推送加入HTTP協議中。
End
如果面試中能提到上面總結的這些要點,面試官應該會比較滿意。
參考文獻
- 國外大佬的博客: http://igoro.com/archive/what-really-happens-when-you-navigate-to-a-url/
- 《計算機網絡》(第五版,謝希仁著)
- ETAG:https://www.cnblogs.com/softidea/p/5986339.html
- Long Polling:http://www.lxweimin.com/p/d3f66b1eb748?from=timeline&isappinstalled=0
- BigPipe:http://taobaofed.org/blog/2015/12/17/seller-bigpipe/
- 負載均衡:https://www.cnblogs.com/arjenlee/p/9262737.html
- 《Web全棧工程師的自我修養》(余果 著)
- Comet: https://www.ibm.com/developerworks/cn/web/wa-lo-comet/