浏覽器是網絡世界裡至關重要的工具之一。下圖是一個使用者使用浏覽器的基本使用者場景:
編輯
但浏覽器究竟是如何能夠做到這些的呢?了解浏覽器的工作流程和原理,所謂知其然而知其是以然,一定會對我們的工作有所裨益。
首先我們了解下常用的浏覽器和浏覽器的組成部分。
常用浏覽器
常用的桌面浏覽器有Chrome,Internet Explorer, Firefox,Safari, Opera。
常用的移動裝置浏覽器: Android浏覽器,iPhone浏覽器,Chrome,Opera Mini,Opera Mobile, UC浏覽器等等。
浏覽器組成
浏覽器主要分成以下7部分:
編輯
1. UI – The User Interface
其實就是浏覽器呈現給使用者的所有使用者接口。包括位址欄,前進/後退/重新整理按鈕,書簽菜單,曆史記錄等等。
2. 浏覽器引擎 – The Browser Engine
統領UI和渲染引擎,在他們之間傳遞資料和指令。
3. 渲染引擎 – The Rendering Engine
負責顯示請求的内容, 可能是HTML,圖檔,PDF等,渲染引擎會對這些内容進行解析,然後将解析後的内容顯示在浏覽器視窗。
不同的浏覽器使用不同的渲染引擎:
浏覽器 | 渲染引擎(開發語言) |
Chrome | Blink (c++) |
Opera | Blink (c++) |
Safari | Webkit (c++) |
FireFox | Gecko (c++) |
Edge | EdgeHTML (c++) |
IE | Trident (c++) |
WebKit是一個開源渲染引擎,其剛開始隻是一個用于Linux平台的引擎,蘋果公司對其進行了修改,使其可以用與macOS和Windows平台。
4. 網絡 – Networking
用于網絡調用(針對于平台無關的統一接口,在不同的平台上使用不同的底層實作),比如HTTP請求。
5. JavaScript解釋器 – JavaScript Interpreter
準确的說,應該說是JavaScript引擎,JavaScript引擎是一個子系統,用于将JS代碼解析成機器碼然後執行。Google V8引擎是最流行的JavaScript引擎。而不同的浏覽器使用的JavaScript引擎也不盡相同:
浏覽器 | 腳本引擎(開發語言) |
Chrome | Google V8 (c++) |
Opera | Google V8 (c++) |
Safari | JavaScript Core (nitro) |
FireFox | SpiderMonkey (c/c++) |
Edge | Chakra JavaScript Engine (c++) |
IE | Chakra JScript Engine (c++) |
6. UI後端 – UI Backend
用于繪制基本小部件,比如組合框和視窗。UI後端會提供與平台無關的通用接口。其底層會使用作業系統的使用者接口方法。
7. 資料存儲 – Data Storage
這是資料持久層。浏覽器需要儲存各種各樣的本地資料,比如cookies.浏覽器也支援其它多種存儲機制,比如localStorage,sessionStorage, AppCache, IndexedDB,WebSQL等。
那接下來我們來看看浏覽器的工作流程。
浏覽器工作流程
1. 處理位址欄輸入
- 如果已有打開的頁面,unload目前頁面。
- 解析位址欄輸入。以Chrome為例,會根據相應的規則,将位址欄輸入解析成搜尋請求或者URI請求。如果是搜尋,解析處搜尋關鍵字,然後與搜尋引擎位址組裝成一個URI請求;否則,浏覽器就會将位址與相關協定(HTTP,HTTPs等)組裝成一個URI請求。
- 在觸發DNS Lookup之前,浏覽器會首先确認緩存中是否存在域名的IP位址。浏覽器會依次檢查浏覽器緩存,作業系統緩存,路由緩存和ISP DNS緩存,如果在緩存中找到了域名的IP位址,那麼就可以直接使用改IP位址發起網絡請求;反之,DNS Lookup就會發生。
需要指出的是:
- 并非URI不同,域名就不同,事實上不同的URI可以有相同的域名,也就是所謂相同的源,這個可以參考Web請求的header的origin字段。
- 緩存中不光會有關于域名的資訊,也包括網址重定向資訊和離線網頁資訊等。
- 如果緩存中有網址重定向資訊,那麼就會向重定向的網址送出請求;如果緩存中有離線網頁資訊,那麼會直接打開離線網頁(本文不會過多在意細節)。
【概念】:
- 永久重定向(狀态碼301),即原域名已經永久修改成新域名,浏覽器會緩存永久重定向的DNS解析記錄。
- 臨時重定向(狀态碼302),浏覽器不會緩存目前域名的解析記錄。
2. DNS Lookup
DNS(Domain Name System)是域名系統, 可将域名解析成對應的IP位址,進而使用者可以通過域名在英特網上通路特定Web 伺服器提供的線上資訊或服務。
當浏覽器想要通路某個域名時,會首先檢查緩存,如果緩存已經儲存了對應的IP位址,那麼DNS Lookup就不會發生;反之,下圖的DNS Lookup流程會被觸發:
編輯
第一步: 浏覽器無法得知輸入域名的IP位址,是以向DNS Resolver發起一個 DNS遞歸查詢。這意味着DNS Resolver必須完成遞歸查詢(recursive query),并傳回給查詢發起者。對于大多數使用者來說,DNS Resolver是由Internet服務供應商(ISP)提供,或者使用開源DNS,比如Google DNS(8.8.8.8)和OpenDNS(208.67.222.222)。當然也可以是本地域名伺服器(Local Domain Server)。
第二步:DNS Resolver本身并沒有儲存域名和IP位址的對應資訊,是以向DNS根伺服器(Root server)發起疊代查詢(Iterative query),
第三步:DNS根伺服器中儲存了所有TLD Server的位址。DNS根伺服器将與域名example.com 比對的TLD Server(.com)的位址傳回給DNS Resolver。TLD server也就是頂級域名伺服器,可以是.com, .edu, .org, .net等。
第四步:DNS Resolver向.com TDL Server發起發起疊代查詢。
第五步:TDL Server将example.com的Name server的IP位址傳回給DNS Resolver。
第六步:DNS Resolver向example.com的Name server發起疊代查詢。
第七步:Name server将example.com的IP位址傳回給DNS Resolver。
第八步:DNS Resolver将example.com的IP位址傳回給浏覽器。
整個DNS查詢過程可以看作: 我們委托朋友(DNS Resolver)幫助我們完成查詢,而朋友通過遞歸查詢各級目錄,最終獲得域名的IP位址,并傳回給我們。其中,Root Server是根目錄,查詢根目錄擷取到次級目錄com的位置;然後通過次級目錄(也就是TDL Server),擷取到最終目錄項example.com的位置;最後從example.com(也就是Name server)擷取到IP位址。
3. 建立TCP連接配接
當IP位址被确認後,浏覽器會根據這個IP,通過TCP三向交握(TCP three-way handshake)的方式,和伺服器建立連接配接。
所謂TCP三向交握:
第一次: 浏覽器發送一個TCP 同步包(SYNchronize package)給伺服器,等待确認。
第二次: 伺服器收到SYN包,并傳回一個同步确認包(SYNchronize-ACKnowledgement package)。
第三次: 浏覽器收到伺服器的SYN-ACK,向伺服器發送一個确認包(ACKnowledgement package)。當伺服器收到ACK後,TCP的socket連接配接就建立了。
如果要建立基于HTTPS的安全連接配接,那麼TLS協商(TLS Negotiation)就會發生。浏覽器和伺服器需要更多次的握手。詳情見下圖:
編輯
4. 發送HTTP(S)請求
在TCP連接配接建立後,浏覽器就會向相應的HTTP(S)伺服器發送請求。
HTTP請求資訊由3部分組成:
(1)請求首行(Request Line)
請求的第一行是“方法URI協定/版本”例如:GET /index.html HTTP/1.1
“GET”代表請求方法(Method);
“/index.html”表示URI;
“HTTP/1.1”代表協定和協定的版本。
- 請求方法共有:CONNECT,DELETE,GET,HEAD,OPTIONS,POST,PUT,TRACE
(2)請求頭(Request Header)
請求頭包含許多有關的用戶端環境和請求正文的有用資訊。例如,請求頭可 以聲明浏覽器所用的語言,請求正文的長度等。例如:
Host: www.baidu.com
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
Accept: application/json, text/javascript, */*; q=0.01
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
sec-ch-ua-platform: "macOS"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://www.baidu.com/
Accept-Encoding: gzip, deflate, br
Accept-Language: en
Cookie: BIDUPSID=49D61A297C62F47AD5C98B8CEDF00A05; PSTM=1615091511; __yjs_duid=1_20f12cca852c3dff005bbde1750e113f1622365350748;FG=1; BD_UPN=123253; MCITY=-233%3A;
(3)請求正文(Request Body)
請求頭和請求正文之間是一個空行,這個行非常重要,它表示請求頭已經結束,接下來的是請求正文。請求正文中可以包含客戶送出的查詢字元串資訊:
username=test&password=test123
5. 伺服器傳回資料
當Web 伺服器收到HTTP請求後,如果請求的頁面在伺服器中不存在,那麼傳回404錯誤;如果請求的頁面已經被移至新的位置,那麼301/302 redirect會被觸發,那麼浏覽器需要重新以新的URI送出請求;如果伺服器存在問題,無法傳回資料,那麼500(internal server error)會被傳回;如果請求的頁面存在,且符合通路規則,那麼伺服器就會傳回請求的頁面。
實際的情況,比我們提到的更多也更加複雜,我們就不一一贅述。如果想檢視更多的HTTP狀态碼,可參考:https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
6. 浏覽接收Response資料并關閉連接配接
浏覽器接收到請求的資料後,TCL連接配接就會被關閉。關閉的過程就是四次揮手的過程:
第一次揮手:用戶端發送一個FIN=M,用來關閉用戶端到伺服器端的資料傳送,用戶端進入FIN_WAIT_1狀态。意思是說"我用戶端沒有資料要發給你了",但是如果你伺服器端還有資料沒有發送完成,則不必急着關閉連接配接,可以繼續發送資料。
第二次揮手:伺服器端收到FIN後,先發送ack=M+1,告訴用戶端,你的請求我收到了,但是我還沒準備好,請繼續你等我的消息。這個時候用戶端就進入FIN_WAIT_2 狀态,繼續等待伺服器端的FIN封包。
第三次揮手:當伺服器端确定資料已發送完成,則向用戶端發送FIN=N封包,告訴用戶端,好了,我這邊資料發完了,準備好關閉連接配接了。伺服器端進入LAST_ACK狀态。
第四次揮手:用戶端收到FIN=N封包後,就知道可以關閉連接配接了,但是他還是不相信網絡,怕伺服器端不知道要關閉,是以發送ack=N+1後進入TIME_WAIT狀态,如果Server端沒有收到ACK則可以重傳。伺服器端收到ACK後,就知道可以斷開連接配接了。用戶端等待了2MSL後依然沒有收到回複,則證明伺服器端已正常關閉,那好,我用戶端也可以關閉連接配接了。最終完成了四次握手。
可參考下圖去了解整個過程:
編輯
7. 浏覽器解析Response資料并渲染
浏覽器接受到response資料後,就會開始解析,如下圖所示(不同的浏覽器解析和渲染過程可能會略有不同,此處以Safari為例):
編輯
需要指出的是,一個網頁可能需要多個HTTP請求,比如,主體的HTML檔案是一個請求,而其相關的CSS,圖檔和JS檔案等也需要一一請求。浏覽器并不會等待所有的資料接收完成才開始解析,而隻保證所有的資料最終都會被解析。
整個過程可描述為:
- 解析HTML标記符号,開始生成DOM樹
- 解析CSS标記符号,開始生成CSSOM樹
- CSSOM樹生成後,JS引擎會解析JS腳本,完成DOM樹的建立
- 合并DOM樹和CSSOM樹,生成渲染樹
- 基于渲染樹和目前浏覽器面闆,計算頁面元素的大小和相對位置
- 将渲染樹裡的每個節點,結合Layout的計算結果,渲染引擎将渲染樹轉換成螢幕上實際的像素,即繪制内容。
【概念】:
- DOM是Document Object Model的縮寫,即文檔對象模型。是W3C推薦的一種處理可擴充标志語言的一種标準程式設計接口。浏覽器将機構化的文檔HTML/XML讀入,将文檔中的每個标簽模組化成一個節點(Node),每個節點有各自的屬性(名稱,類型,内容等等),節點之間有層級關系(父親,孩子,兄弟等),進而形成了樹形結構。DOM提供的程式設計接口,支援通路,修改,添加和删除DOM樹的節點和内容。
- CSSOM樹: 類此與DOM樹,浏覽器會便利CSS檔案中所有的規則,建立一棵樹,這棵樹包含多個節點,基本CSS選擇器,這些節點之間也有着層級關系,比如父親,孩子,兄弟等等
- Render樹: 将DOM樹和CSSOM合并後,就形成了Render樹。
- Layout: 當渲染樹生成後,就會開始layout。Layout負責計算渲染樹的每個節點在螢幕上的位置和次元。比如,不同的裝置上的浏覽器的寬度可能不同,那麼,基本螢幕寬度的顯示,Layout會得出不同的實際顯示寬度。
下圖是基于Navigation-Timing的流程圖,僅作進一步學習之用。詳情可以參考:Navigation Timing