1. 程式人生 > >從URL輸入後 瀏覽器做了哪些行為?

從URL輸入後 瀏覽器做了哪些行為?

故事其實並不是從在瀏覽器的位址列輸入一個網址,或者我們抓著滑鼠點選一個連結開始,事情的開端要追溯到伺服器啟動監聽服務的時候,在某個未知的時刻,一臺機房裡普普通通的刀鋒伺服器,加上電,啟動了作業系統,隨著作業系統的就緒,伺服器啟動了 http 服務程序,這個 http 服務的守護程序,(daemon),可能是 apache,也可能是 nginx,不管怎麼說,這個 http 服務程序開始定位到伺服器上的 www 資料夾,一般是位於 /var/www ,然後啟動了一些附屬的模組,例如 php,或者,使用 fastcgi 方式連線到 php 的 fpm 管理程序,然後,向作業系統申請了一個 tcp 連線,然後繫結在了 80 埠,呼叫了 accept 函式,開始了默默的監聽,監聽著可能來自位於地球任何一個地方的請求,隨時準備做出響應。

這個時候,典型的情況下,機房裡面應該還有一個數據庫伺服器,或許,還有一臺快取伺服器,如果對於流量巨大的網站,那麼動態指令碼的直譯器可能還有單獨的物理機器來跑,如果是中小的站點,那麼,上述的各色服務,甚至都可能在一臺物理機上,這些服務監聽之間的關係,可以參考這裡,再搭一次 Apache PHP MySQL 環境,不管怎麼說,他們做好了準備,靜候差遣。

上回說道伺服器啟動了監聽服務,準備迎接來自客戶機的請求。那麼,我們以一次典型的瀏覽請求來解析:

當我們開始在瀏覽器中輸入網址的時候,瀏覽器其實就已經在智慧的匹配可能得 url 了,他會從歷史記錄,書籤等地方,找到已經輸入的字串可能對應的 url,然後給出智慧提示,對於 google chrome 那種變態的瀏覽器,他甚至會直接從快取中把網頁展示出來,就是說,你還沒有按下 enter,頁面就出來了,這個過於奇葩,我們不詳細講。

在例如輸入了 baidu 或者 qq 之類的域名後,我們可以按下 ctrl + enter ,來自動補全,生成 qq.com 或者 baidu.com 的網址,然後發起請求。請求一旦發起,瀏覽器首先要做的事情就是解析這個域名,一般來說,瀏覽器會首先檢視本地硬碟的 hosts 檔案,看看其中有沒有和這個域名對應的規則,如果有的話就直接使用 hosts 檔案裡面的 ip 地址,說道這裡,大家可能想到,這個地方就存在安全隱患了,如果有病毒把一些常用的域名,修改 hosts  檔案,指向一些惡意的 ip,那麼瀏覽器也會不加判斷的去連線,是的,這正是很多病毒的慣用手法。

如果在本地的 hosts 檔案沒有能夠找到對應的 ip 地址,瀏覽器會向 dns 域名解析伺服器發起域名解析請求,dns 的域名解析是遞迴的,(還有另外 dns 是迭代的),遞迴的 dns 首先會檢視自己的 dns 快取,如果快取能夠命中,那麼就從快取中把 ip 地址返回給瀏覽器,如果找不到對應的域名的 ip 地址,那麼就向上轉發請求,然後把得到的這個域名對應的 nameserver 的地址取得,再向這個 namserver 去請求域名對應的 ip,最後把這個 ip 地址返回給瀏覽器,不過怎麼說,這個遞迴查詢的過程,對於瀏覽器來說是透明的,他只要坐等 ip地址送回來就可以了。對於 DNS 的進一步理解,可以看這裡,

進一步理解 DNS

得到 ip 地址後,瀏覽器會開始構造一個 http 請求,一個典型的 http request header 一般需要包括請求的方法,例如 GET 或者 POST 等,不常用的還有 PUT 和 DELETE 方法,更加不常用的還有 HEAD 和 OPTION 以及 TRACE 方法,一般的瀏覽器只能發起 GET 或者 POST 請求,應用層的 http 請求準備好後,瀏覽器在傳輸層發起一條到達伺服器的 tcp 連線,這個時候應該開始三次握手的過程,tcp 包被封裝到網路層的 ip 包裡面,ip 包再被封裝到資料鏈路層的資料幀結構中,再通過物理層的位元流送出去,這些分層的意義在於分工合作,資料鏈路層通過 CSMA/CD 協議保證了相鄰兩臺主機之間的資料報文傳遞,而網路層的 ip 資料包通過不同子網之間的路由器的路由演算法和路由轉發,保證了網際網路上兩臺遙遠主機之間的點對點的通訊,不過這種傳輸是不可靠,於是可靠性就由傳輸層的 tcp 協議來保證,tcp 通過慢開始,乘法減小等手段來進行流量控制和擁塞避免,同時提供了兩臺遙遠主機上程序到程序的通訊,最終保證了 http 的請求頭能夠被遠方的伺服器上正在監聽的 http 伺服器程序收到,終於,資料包在跳與跳之間被拆了又封裝,在子網與子網之間被轉發了又轉發,最後進入了伺服器的作業系統的緩衝區,伺服器的作業系統由此給正在被阻塞住的 accept 函式一個返回,將他喚醒。

請求進入伺服器之後,伺服器上的的 http 監聽程序會得到這個請求,然後一般情況下會啟動一個新的子程序去處理這個請求,同時父程序繼續監聽。http 伺服器首先會檢視重寫規則,然後如果是檔案真實存在,例如一些圖片,或者 css js 等的靜態檔案,就會直接把這個檔案返回,如果是一個動態的請求,那麼會根據 url 重寫模組的規則,把這個請求重寫到一個 rest 風格的 url 上,然後根據動態語言的指令碼,來決定呼叫什麼型別的動態檔案指令碼直譯器來處理這個請求。

我們以 php 語言為例來說的話,請求到達一個 php 的 mvc 框架之後,框架首先應該會初始化一些環境的引數,例如遠端 ip,請求引數等等,然後根據請求的 url 送到一個路由器類裡面去匹配路由,路由由上到下逐條匹配,一旦遇到 url 能夠匹配的上,而且請求的方法也能夠命中的話,那麼請求就會由這個路由所定義的處理方法去處理。

請求進入處理函式之後,如果客戶端所請求需要瀏覽的內容是一個動態的內容,那麼處理函式會相應的從資料來源裡面取出資料,這個地方一般會有一個快取,例如 memcached 來減小 db 的壓力,如果引入了 orm 框架的話,那麼處理函式直接向 orm 框架索要資料就可以了,由 orm 框架來決定是使用記憶體裡面的快取還是從 db 去取資料,一般快取都會有一個過期的時間,而 orm 框架也會在取到資料回來之後,把資料存一份在記憶體快取中的。

orm 框架負責把面向物件的請求翻譯成標準的 sql 語句,然後送到後端的 db 去執行,db 這裡以 mysql 為例的話,那麼一條 sql 進來之後,db 本身也是有快取的,不過 db 的快取一般是用 sql 語言 hash 來存取的,也就是說,想要快取能夠命中,除了查詢的欄位和方法要一樣以外,查詢的引數也要完全一模一樣才能夠使用 db 本身的查詢快取,sql 經過查詢快取器,然後就會到達查詢分析器,在這裡,db 會根據被搜尋的資料表的索引建立情況,和 sql 語言本身的特點,來決定使用哪一個欄位的索引,值得一提的是,即使一個數據表同時在多個欄位建立了索引,但是對於一條 sql 語句來說,還是隻能使用一個索引,所以這裡就需要分析使用哪個索引效率最高了,一般來說,sql 優化在這個點上也是很重要的一個方面。

sql 由 db 返回結果集後,再由 orm 框架把結果轉換成模型物件,然後由 orm 框架進行一些邏輯處理,把準備好的資料,送到檢視層的渲染引擎去渲染,渲染引擎負責模板的管理,欄位的友好顯示,也包括負責一些多國語言之類的任務。對於一條請求在 mvc 中的生命週期,可以參考這裡,臨摹了一個 PHP MVC 框架,在檢視層把頁面準備好後,再從動態指令碼直譯器送回到 http 伺服器,由 http 伺服器把這些正文加上一個響應頭,封裝成一個標準的 http 響應包,再通過 tcp ip 協議,送回到客戶機瀏覽器。

歷經千辛萬苦,我們請求的響應終於到達了客戶端的瀏覽器,響應到達瀏覽器之後,瀏覽器首先判斷狀態碼,如果是 200 開頭的就好辦,直接進入渲染流程,如果是 300 開頭的就要去相應頭裡面找 location 域,根據這個 location 的指引,進行跳轉,這裡跳轉需要開啟一個跳轉計數器,是為了避免兩個或者多個頁面之間形成的迴圈的跳轉,當跳轉次數過多之後,瀏覽器會報錯,同時停止。如果是 400 開頭或者 500 開頭的狀態碼,瀏覽器也會給出一個錯誤頁面。

當瀏覽得到一個正確的 200 響應之後,接下來面臨的一個問題就是多國語言的編碼解析了,響應頭是一個 ascii 的標準字符集的文字,這個還好辦,但是響應的正文字質上就是一個位元組流,對於這一坨位元組流,瀏覽器要怎麼去處理呢,首先瀏覽器會去看響應頭裡面指定的 encoding 域,如果有了這個東西,那麼就按照指定的 encoding 去解析字元,如果沒有的話,那麼瀏覽器會使用一些比較智慧的方式,去猜測和判斷這一坨位元組流應該使用什麼字符集去解碼。相關的筆記可以看這裡,瀏覽器對編碼的確定

解決了字符集的問題,接下來就是構建 dom 樹了,在 html 語言巢狀正常而且規範的情況下,這種 xml 標記的語言是比較容易的能夠構建出一棵 dom 樹出來的,當然,對於網際網路上大量的不規範的頁面,不同的瀏覽器應該有自己不同的容錯去處理。構建出來的 dom 本質上還是一棵抽象的邏輯樹,構建 dom 樹的過程中,如果遇到了由 script 標籤包起來的 js 動態指令碼程式碼,那麼會把程式碼送到 js 引擎裡面去跑,如果遇到了 style 標籤包圍起來的 css 程式碼,也會儲存下來,用於稍後的渲染。如果遇到了 img 等引用外部檔案的標籤,那麼瀏覽器會根據指定的 url 再次發起一個新的 http 請求,去把這個檔案拉取回來,值得一提的是,對於同一個域名下的下載過程來說,瀏覽器一般允許的併發請求是有限的,通常控制在兩個左右,所以如果有很多的圖片的話,一般出於優化的目的,都會把這些圖片使用一臺靜態檔案的伺服器來儲存起來,負責響應,從而減少主伺服器的壓力。

dom 樹構造好了之後,就是根據 dom 樹和 css 樣式表來構造 render 樹了,這個才是真正的用於渲染到頁面上的一個一個的矩形框的樹,對於 render 樹上每一個框,需要確定他的 x y 座標,尺寸,邊框,字型,形態,等等諸多方面的東西,render 樹一旦構建完成,整個頁面也就準備好了,可以上菜了。

需要說明的是,下載頁面,構建 dom 樹,構建 render 樹這三個步驟,實際上並不是嚴格的先後順序的,為了加快速度,提高效率,讓使用者不要等那麼久,現在一般都並行的往前推進的,現代的瀏覽器都是一邊下載,下載到了一點資料就開始構建 dom 樹,也一邊開始構建 render 樹,構建了一點就顯示一點出來,這樣使用者看起來就不用等待那麼久了。