1. 簡介
我思考了很多知識組織方法來幫助理解網絡知識,比如按osi模型從底至上,或者按協議種類,或者按網絡發展史。但最終我還是決定選擇用這個經典的問題,將網絡知識串成線。理解從輸入url到看到頁面的過程,弄明白這中間有哪些步驟,再仔細分析這些步驟的原理和行為,是我所能想到最清晰的一條知識脈絡了。
2. 如何看到我們的頁面?
這里我們就以訪問阮一峰老師的一篇博文(HTTP 協議入門)來講解吧,大家閱讀本篇文章前也可以看看這篇文章。我們在瀏覽器輸入“http://www.ruanyifeng.com/blog/2016/08/http.html”以后,瀏覽器如何為我們展現網頁呢?
step1:瀏覽器解析url
瀏覽器會對我們輸入的url進行解析,主要將其分為下部分:協議、網絡地址、資源路徑。其中網絡地址指示該連接網絡上哪一臺計算機,可以是域名或者IP地址,可以包括端口號;協議是從該計算機獲取資源的方式,常見的是HTTP,HTTPS,FTP等。不同協議有不同的通訊內容格式;資源路徑指示從服務器上需要獲取資源的具體路徑。
這里瀏覽器對輸入的url解析為如下內容:
url:http://www.ruanyifeng.com/blog/2016/08/http.html
協議:http
網絡地址(網站名):www.ruanyifeng.com
資源路徑:/blog/2016/08/http
當然,瀏覽器還知道端口信息和參數信息,但這一步還用不上。另外網絡地址由服務器名:www和域名ruanyifeng.com組成。
step2:DNS域名解析
瀏覽器理解用戶輸入的信息,知道用戶想要用http訪問一個網絡地址是“www.ruanyifeng.com”的網站。那么如何找到這個地址呢,就像你打的回家,你跟司機說去阮老師家,他哪兒知道阮老師家是哪里呢?你得告訴他門牌號呀。網站服務器的門牌號就是IP地址。所有瀏覽器首先要確認的是域名所對應的服務器在哪里。將域名解析成對應的服務器IP地址這項工作,是由DNS服務器來完成的。
客戶端收到你輸入的域名地址后,它首先去找本地的hosts文件,檢查在該文件中是否有相應的域名、IP對應關系,如果有,則向其IP地址發送請求,如果沒有,再去找DNS服務器。一般用戶很少去編輯修改hosts文件。
DNS服務器層級如下:
DNS查詢的具體步驟如下:
- 從瀏覽器緩存中查詢。瀏覽器會存儲一定時間的DNS記錄,操作系統不會告訴瀏覽器每個DNS記錄的保存時限,不同瀏覽器設置保存時限為一個固定值(不同瀏覽器情況不同,一般在2-30分鐘)。
- 從操作系統緩存中查詢。如果瀏覽器中沒有包含想要的緩存記錄,那瀏覽器就會發起操作系統請求,繼續查詢操作系統緩存
- 從路由器中查詢DNS緩存。請求持續發送到你的路由,它通常會有自己的DNS緩存。
- 從ISP中查詢DNS緩存。下一個被查詢地方是ISP緩存DNS的服務器。
- 域名服務器迭代查詢,根據返回的地址逐級向上查詢。首先從root域名服務器中查詢如.com域名服務器,然后逐步向前查詢,.com頂級域名服務器到ruanyifeng的域名服務器。一般來說,.com級別的都已經在緩存中了,所以一般不會進行對root域名服務器的查詢。下面給出一張迭代查詢的圖。
DNS服務器遞歸查詢和迭代查詢(轉)
瀏覽器客戶端向本地DNS服務器發送一個含有域名http://www.cnblogs.com的DNS查詢報文。本地DNS服務器把查詢報文轉發到根DNS服務器,根DNS服務器注意到其com后綴,于是向本地DNS服務器返回comDNS服務器的IP地址。本地DNS服務器再次向comDNS服務器發送查詢請求,comDNS服務器注意到其http://www.cnblogs.com后綴并用負責該域名的權威DNS服務器的IP地址作為回應。最后,本地DNS服務器將含有http://www.cnblogs.com的IP地址的響應報文發送給客戶端。
從客戶端到本地服務器屬于遞歸查詢,而DNS服務器之間的交互屬于迭代查詢。
正常情況下,本地DNS服務器的緩存中已有comDNS服務器的地址,因此請求根域名服務器這一步不是必需的。
一些大型網站域名像wikipedia.org或者facebook.com整個域都映射到不止一個IP地址(土豪房子多),我們可以使用如下解決方法:
- Round-robin DNS,DNS輪詢是其中一種方法,是DNS查找時返回多個IP時的解決方案。舉例來說,Facebook.com實際上就對應了四個IP地址。
- Load-balancer,大型的網站一般都會使用高性能的負載均衡器來平衡流量。負載均衡器一直監聽一個特殊的IP地址,并轉發請求到其他的服務器。(譯者注:簡單粗暴點理解就是在用戶和服務器之間加了個中間層,利用監聽和轉發請求達到用戶相對快速訪問,資源最優化使用的目的)
- Geographic DNS,基于地理的DNS,依賴客戶端的地理位置,這是一個很好的存儲靜態資源的方法,不同的服務器可以不更新共享狀態。
- Anycast,一種單個IP地址映射多個物理服務器的技術。
step3:瀏覽器獲取端口號
好了,阮老師家的門牌號知道了,正常來講是可以出發了。可是對于網絡有些不一樣,你還需要指定端口號。端口號之于計算機就像窗口號之于銀行,一家銀行有多個窗口,每個窗口都有個號碼,不同窗口可以負責不同的服務。端口只是一個邏輯概念,和計算機硬件沒有關系。現在可以這么說,阮老師家好幾扇們,辦不同的業務走不同的門,你得告訴師傅你走那扇門,你要不說,就默認你是個普通客人,丟大門得了。http協議默認端口號是80。
step4:TCP建立連接
好了,IP和端口都有了,能出發了么?師傅很熱心,怕你去了對方家里沒人啊,于是根據查到的信息給對方家里打了電話
師傅:“喂,家里有人沒啊?有客人來拜訪啊?”
對方:“有啊,來吧”
師傅:“好嘞”。
在http消息發送前,需要建立客戶端與服務器的TCP鏈接,也就是進行所謂的三次握手。
TCP是因特網中的傳輸層協議,使用三次握手協議建立連接。當主動方發出SYN連接請求后,等待對方回答SYN+ACK,并最終對對方的 SYN 執行 ACK 確認。這種建立連接的方法可以防止產生錯誤的連接,TCP使用的流量控制協議是可變大小的滑動窗口協議。
TCP三次握手的過程如下:
客戶端發送SYN(SEQ=x)報文給服務器端,進入SYN_SEND狀態。
服務器端收到SYN報文,回應一個SYN (SEQ=y)ACK(ACK=x+1)報文,進入SYN_RECV狀態。
客戶端收到服務器端的SYN報文,回應一個ACK(ACK=y+1)報文,進入Established狀態。
三次握手完成,TCP客戶端和服務器端成功地建立連接,可以開始傳輸數據了。
step5: 發送HTTP請求
你們和阮老師家里互相確定了到訪的事情后,你們終于開心地出發了。也就是說,與服務器建立了連接后,就可以向服務器發起請求了。這里我們先看下請求報文的結構:
我們在chrome瀏覽器查看報文首部信息:
我們可以從報文中看到發出的請求的具體信息。具體每個首部字段的作用,會開單章講解。
step6:服務器處理請求
好了,終于到了阮老師家,坐車去阮老師家的例子到此就結束了。不過我們的渲染頁面的目的還沒達到,現在請求只是成功達到了服務器,接下來服務器需要響應瀏覽器的請求。
服務器端收到請求后的由web服務器(準確說應該是http服務器)處理請求,諸如Apache、Ngnix、IIS等。web服務器解析用戶請求,知道了需要調度哪些資源文件,再通過相應的這些資源文件處理用戶請求和參數,并調用數據庫信息,最后將結果通過web服務器返回給瀏覽器客戶端。下面以靜態渲染的頁面為例,ajax渲染不需要在服務器做頁面數據寫入。
step7:返回響應結果
在HTTP里,有請求就會有響應,哪怕是錯誤信息。這里我們同樣看下響應報文的組成結構:
在響應結果中都會有個一個HTTP狀態碼,比如我們熟知的200、301、404、500等。通過這個狀態碼我們可以知道服務器端的處理是否正常,并能了解具體的錯誤。
狀態碼由3位數字和原因短語組成。根據首位數字,狀態碼可以分為五類:
具體的狀態碼信息我會開單章。
另外還有一些其他信息(chrome中顯示的response headers如下):
內容編碼頭部告訴瀏覽器響應體使用了gzip壓縮算法,解壓后就會看到你期望的HTML了。
除了壓縮信息之外,頭部還詳細說明了是否和怎么緩存頁面、設置cookies(在這個響應中沒有)、隱秘信息等
或許有人注意到了設置了內容類型為text/html,這部分頭部說明了瀏覽器將響應內容作為HTML渲染,而不是作為文件下載。瀏覽器將使用頭部決定如何解釋響應結果,當然也會考慮其他因素,比如URL的擴展情況。
step8: 關閉TCP連接
為了避免服務器與客戶端雙方的資源占用和損耗,當雙方沒有請求或響應傳遞時,任意一方都可以發起關閉請求。
建立一個連接需要三次握手,而終止一個連接要經過四次揮手,這是由TCP的半關閉(half-close)造成的。具體過程如下圖所示。
(1) 某個應用進程首先調用close,稱該端執行“主動關閉”(active close)。該端的TCP于是發送一個FIN分節,表示數據發送完畢。
(2) 接收到這個FIN的對端執行 “被動關閉”(passive close),這個FIN由TCP確認。
注意:FIN的接收也作為一個文件結束符(end-of-file)傳遞給接收端應用進程,放在已排隊等候該應用進程接收的任何其他數據之后,因為,FIN的接收意味著接收端應用進程在相應連接上再無額外數據可接收。
(3) 一段時間后,接收到這個文件結束符的應用進程將調用close關閉它的套接字。這導致它的TCP也發送一個FIN。
(4) 接收這個最終FIN的原發送端TCP(即執行主動關閉的那一端)確認這個FIN。
既然每個方向都需要一個FIN和一個ACK,因此通常需要4個分節。
注意:
(1) “通常”是指,某些情況下,步驟1的FIN隨數據一起發送,另外,步驟2和步驟3發送的分節都出自執行被動關閉那一端,有可能被合并成一個分節。[2]
(2) 在步驟2與步驟3之間,從執行被動關閉一端到執行主動關閉一端流動數據是可能的,這稱為“半關閉”(half-close)。
(3) 當一個Unix進程無論自愿地(調用exit或從main函數返回)還是非自愿地(收到一個終止本進程的信號)終止時,所有打開的描述符都被關閉,這也導致仍然打開的任何TCP連接上也發出一個FIN。
無論是客戶還是服務器,任何一端都可以執行主動關閉。通常情況是,客戶執行主動關閉,但是某些協議,例如,HTTP/1.0卻由服務器執行主動關閉。
step9:瀏覽器加載解析渲染
當瀏覽器獲得一個html文件時,會“自上而下”加載,并在加載過程中進行解析渲染。
解析:
- 瀏覽器會將HTML解析成一個DOM樹,DOM 樹的構建過程是一個深度遍歷過程:當前節點的所有子節點都構建好后才會去構建當前節點的下一個兄弟節點。
- 將CSS解析成 CSS Rule Tree 。
- 根據DOM樹和CSSOM來構造 Rendering Tree。注意:Rendering Tree 渲染樹并不等同于 DOM 樹,因為一些像 Header 或 display:none 的東西就沒必要放在渲染樹中了。
這一塊的過程比較復雜,會開單章。
step10:瀏覽器發送嵌入在HTML中的對象的請求
隨著瀏覽器渲染HTML,瀏覽器會注意到有些標簽需要請求其他URLs的資源,瀏覽器將會發送一個GET請求來重新獲取每個文件 。比如js文件,css文件,圖片資源等。
每個URLs會像獲取HTML頁面的過程一樣獲取相應資源。所以,瀏覽器會在DNS中查詢域名,并向URL發送請求,進行重定向(其實以上步驟我是省略了重定向這一步的)等等以上步驟
當然,靜態文件和動態網站不一樣,它們允許被瀏覽器緩存。一些文件可能會根本不經過服務器,直接被從緩存中取出。因為響應結果中返回一個包含著Expires頭的文件,所以瀏覽器知道要緩存一個文件多久。另外每個響應可能包含著ETag頭,其作用類似版本號,如果瀏覽器發現已經擁有了一個文件的ETag,那么就會立即停止此文件傳輸。
step11:瀏覽器發送異步請求
在web2.0時代,即使在頁面渲染后客戶端還是持續與服務器端通信。這個模式被稱為AJAX。我會開單章講述。
3. 總結
以上步驟只是大略地解析了從瀏覽器輸入url到最終頁面展示在用戶眼前的流程,更多細節我會開單章進行講解。
參考
"天龍八步"細說瀏覽器輸入URL后發生了什么
【譯】從輸入URL到頁面渲染完成
從輸入 URL 到頁面加載完的過程中都發生了什么事情?
What really happens when you navigate to a URL
百度百科-TCP
瀏覽器加載、解析、渲染的過程