瀏覽器訪問網頁流程
從我們輸入URL並按下回車鍵到看到網頁結果之間發生了什麼?換句話說,一張網頁,要經歷怎樣的過程,才能抵達使用者面前?下面來從一些細節上面嘗試一下探尋裡面的祕密。
前言:鍵盤與硬體中斷
說到輸入URL,當然是從手敲鍵盤開始。對於鍵盤,生活中用到的最常見的鍵盤有兩種:薄膜鍵盤、機械鍵盤。
- 薄膜鍵盤:由面板、上電路、隔離層、下電路構成。有外觀優美、壽命較長、成本低廉的特點,是最為流行的鍵盤種類。鍵盤中有一整張雙層膠膜,通過膠膜提供按鍵的回彈力,利用薄膜被按下時按鍵處碳心於線路的接觸來控制按鍵觸發。
- 機械鍵盤:由鍵帽、機械軸組成。鍵盤打擊感較強,常見於遊戲發燒友與打字愛好者。每一個按鍵都有一個獨立的機械觸點開關,利用柱型彈簧提供按鍵的回彈力,用金屬接觸觸點來控制按鍵的觸發。
鍵盤傳輸訊號到作業系統後便會觸發硬體中斷處理程式。硬體中斷是作業系統中提高系統效率、滿足實時需求的非常重要的訊號處理機制,它是一個非同步訊號,並提供相關中斷的登錄檔(IDT)與請求線(IRQ)。鍵盤被按壓時,將通過請求線將訊號輸入給作業系統,CPU在當前指令結束之後,根據登錄檔與訊號響應該中斷並利用段暫存器裝入中斷程式入口地址。具體可參看作業系統與彙編相關書籍。
當然本文主要不是介紹硬體與作業系統中的細節,前言只是想說明,從輸入URL到瀏覽器展現結果頁面之間有太多底層相關的知識,懷著一顆敬畏的心並且在有限的篇幅中是無法詳細闡述的,所以本文會將關注點放在一個稍高的角度上來看,從瀏覽器替我們傳送請求之後到看到頁面顯示完成這中間中發生了什麼事情
下圖是從使用者傳送一條請求開始到最終看到頁面的流程簡化圖,本文將以該請求為線索,在下面一步一步探索其中的細節。
瀏覽器解析URL
按下回車鍵之前
比如我按下一個“b”鍵,會出現很多待選URL給我,第一個便是百度。那麼其實是在瀏覽器接收到這個訊息之後,會觸發瀏覽器的自動完成機制,會在你之前訪問過的搜尋最匹配的相關URL,會根據特定的演算法,甚至涉及到機器學習來分析出你有可能想搜尋的關鍵字,顯示出來供使用者選擇。
按下回車鍵之後
依據上述鍵盤觸發原理,一個專用於回車鍵的電流回路通過不同的方式閉合了。然後觸發硬體中斷,隨之作業系統核心去處理對應中斷。省略其中的過程,最後交給了瀏覽器這樣一個“回車”訊號。那麼瀏覽器(本文涉及到的瀏覽器版本為Chrome 61)會進行以下但不僅限於以下炫酷(亂七八糟)的步驟:
- 解析URL:您輸入的是http還是https開頭的網路資源 / file開頭的檔案資源 / 待搜尋的關鍵字?然後瀏覽器進行對應的資源載入程序。
- HSTS:鑑於HTTPS遺留的安全隱患,大部分現代瀏覽器已經支援HSTS。對於瀏覽器來說,瀏覽器會檢測是否該網路資源存在於預設定的只使用HTTPS的網站列表,或者是否儲存過以前訪問過的只能使用HTTPS的網站記錄,如果是,瀏覽器將強行使用HTTPS方式訪問該網站。
DNS解析
不查DNS,讀取快取
- 瀏覽器中的快取:對於Chrome,快取檢視地址為:chrome://net-internals/#dns
- 本地hosts檔案:以Mac與Linux為例,hosts檔案所在路徑為:/etc/hosts。所以有一種翻牆的方式就是修改hosts檔案避免GFW對DNS解析的干擾,直接訪問真正IP地址,但已經不能完全生效因為GFW還有根據IP過濾的機制。
傳送DNS查詢請求
DNS的查詢方式是:按根域名->頂級域名->次級域名->主機名這樣的方式來查詢的,對於某個URL,如下所示
查詢步驟為:
- 查詢本地DNS伺服器。本地DNS伺服器地址為連線網路時路由器指定的DNS地址,一般為DHCP自動分配的路由器地址,儲存在/etc/resolv.conf,而路由器的DNS轉發器將請求轉發到上層ISP的DNS,所以此處本地DNS伺服器是區域網或者運營商的。
- 從”根域名伺服器”查到”頂級域名伺服器”的NS記錄和A記錄(IP地址)。世界上一共有十三組根域名伺服器,從A.ROOT-SERVERS.NET一直到M.ROOT-SERVERS.NET,由於已經將這些根域名伺服器的IP地址存放在本地DNS伺服器中。
- 從”頂級域名伺服器”查到”次級域名伺服器”的NS記錄和A記錄(IP地址)
- 從”次級域名伺服器”查出”主機名”的IP地址
以www.google.com為例,下面是在阿里雲實例上作的完整的DNS查詢過程:
- 由於本次測試是在阿里雲上的例項進行測試,所以首先從100.100.2.138這個阿里內網DNS伺服器查詢到所有根域名伺服器的對映關係。
- 訪問根域名伺服器(f.root-servers.net),拿到com頂級域名伺服器的NS記錄與IP地址。
- 訪問頂級域名伺服器(e.gtld-servers.net),拿到google.com次級域名伺服器的NS記錄與IP地址。
- 訪問次級域名伺服器(ns2.google.com),拿到www.google.com的IP地址
- 所以總的來說,DNS的解析是一個逐步縮小範圍的查詢過程。
建立HTTPS、TCP連線
確定傳送目標
拿到IP之後,還需要拿到那臺伺服器的MAC地址才行,在乙太網協議中規定,同一區域網中的一臺主機要和另一臺主機進行直接通訊,必須要知道目標主機的MAC地址。所以根據ARP(根據IP地址獲取實體地址的一個TCP/IP協議)獲取到MAC地址之後儲存到本地ARP快取之後與目標主機準備開始通訊。具體細節參見維基百科DHCH/ARP。
建立TCP連線
為什麼握手一定要是三次?
- 第一次與第二次握手完成意味著:A能傳送請求到B,並且B能解析A的請求
- 第二次與第三次握手完成意味著:A能解析B的請求,並且B能傳送請求到A
這樣就保證了A與B之間既能相互發送請求也能相互接收解析請求。同時避免了因為網路延遲產生的重複連線問題,比如A傳送一次連線請求但網路延遲導致這次請求是在A重發連線請求並完成與B通訊之後的,有三次握手的話,B返回的建立請求A就不會理睬了。
短連線與長連線?
上圖是一個短連線的過程演示,對於長連線,A與B完成一次讀寫之後,它們之間的連線並不會主動關閉,後續的讀寫操作會繼續使用這個連線。另外,由於長連線的實現比較困難,需要要求長連線在沒有資料通訊時,定時傳送資料包(心跳),以維持連線狀態,並且長連線對於伺服器的壓力也會很大,所以推送服務對於一般的開發者是非常難以實現的,這樣的話就出現了很多不同的大型廠商提供的訊息推送服務。
進行TLS加密過程
- Hello – 握手開始於客戶端傳送Hello訊息。包含服務端為了通過SSL連線到客戶端的所有資訊,包括客戶端支援的各種密碼套件和最大SSL版本。伺服器也返回一個Hello訊息,包含客戶端需要的類似資訊,包括到底使用哪一個加密演算法和SSL版本。
- 證書交換 – 現在連線建立起來了,伺服器必須證明他的身份。這個由SSL證書實現,像護照一樣。SSL證書包含各種資料,包含所有者名稱,相關屬性(域名),證書上的公鑰,數字簽名和關於證書有效期的資訊。客戶端檢查它是不是被CA驗證過的。注意伺服器被允許需求一個證書去證明客戶端的身份,但是這個只發生在敏感應用。
- 金鑰交換 – 先使用RSA非對稱公鑰加密演算法(客戶端生成一個對稱金鑰,然後用SSL證書裡帶的伺服器公鑰將改對稱金鑰加密。隨後傳送到服務端,服務端用伺服器私鑰解密,到此,握手階段完成。)或者DH交換演算法在客戶端與服務端雙方確定一將要使用的金鑰,這個金鑰是雙方都同意的一個簡單,對稱的金鑰,這個過程是基於非對稱加密方式和伺服器的公鑰/私鑰的。
- 加密通訊 – 在伺服器和客戶端加密實際資訊是用到對稱加密演算法,用哪個演算法在Hello階段已經確定。對稱加密演算法用對於加密和解密都很簡單的金鑰,這個金鑰是基於第三步在客戶端與服務端已經商議好的。與需要公鑰/私鑰的非對稱加密演算法相反。
服務端的處理
靜態快取、CDN
為了優化網站訪問速度並減少伺服器壓力,通常將html、js、css、檔案這樣的靜態檔案放在獨立的快取伺服器或者部署在類似Amazon CloudFront的CDN雲服務上,然後根據快取過期配置確定本次訪問是否會請求源伺服器來更新快取。
負載均衡
負載均衡具體實現有多種,有直接基於硬體的F5,有作業系統傳輸層(TCP)上的 LVS,也有在應用層(HTTP)實現的反向代理(也叫七層代理),下面簡單介紹一下最後者。
在請求傳送到真正處理請求的伺服器之前,還需要將請求路由到適合的伺服器上,一個請求被負載均衡器拿到之後,需要做一些處理,比如壓縮請求(在nginx中gzip壓縮格式是預設配置在nginx.conf內的,所以預設開啟,如果不對資料量要求特別精細的話,預設配置完全可以滿足基本需求)、接收請求(接收完畢後才發給Server,提高Server處理效率),然後根據預定的路由演算法,將此次請求傳送到某個後臺伺服器上。
其中需要提到的一點是反向代理,先理解一下正向代理的原理:正向代理是將自己要訪問的資源告訴Proxy,讓Proxy幫你拿到資料返回給你,Proxy服務於Client,常用於翻牆和跨許可權操作。反向代理也是將自己要訪問的資源告訴Proxy,讓Proxy幫你拿到資料返回給你,但是Proxy會將請求接受完畢之後傳送給某一合適的Server,這個時候Client是不知道是根據什麼規則並且也不知道最後是哪一個Server服務於它的且Proxy服務於Server,所以叫反向代理,常用於負載均衡、安全控制。
伺服器的處理
對於HTTPD(HTTP Daemon)在伺服器上部署,最常見的 HTTPD 有 Linux 上常用的 Apache 和 Nginx。對於Java平臺來說,Tomcat是Spring Boot也會預設選用的Servlet容器實現,以Tomcat為例,對於請求的處理如下:
- 請求到達Tomcat啟動時監聽的TCP埠。
- 解析請求中的各種資訊之後建立一個Request物件並填充那些有可能被所引用的Servlet使用的資訊,如引數,頭部、cookies、查詢字串等。
- 建立一個Response物件,所引用的Servlet使用它來給客戶端傳送響應。
- 呼叫Servlet的service方法,並傳入Request和Response物件。這裡Servlet會從Request物件取值,給Response寫值。
- 根據我們自己寫的Servlet程式或者框架攜帶的Servlet類做進一步的處理(業務處理、請求的進一步處理)
- 最後根據Servlet返回的Response生成相應的HTTP響應報文。
瀏覽器的渲染
瀏覽器的功能是從伺服器上取回你想要的資源,然後展示在瀏覽器視窗當中。資源通常是 HTML 檔案,也可能是 PDF,圖片,或者其他型別的內容。也可以顯示其他型別的外掛(瀏覽器擴充套件)。例如顯示PDF使用PDF瀏覽器外掛。資源的位置通過使用者提供的 URI(Uniform Resource Identifier) 來確定。
瀏覽器解釋和展示 HTML 檔案的方法,在 HTML 和 CSS 的標準中有詳細介紹。這些標準由 Web 標準組織 W3C(World Wide Web Consortium) 維護。
下面會以Chrome中使用的瀏覽器引擎Webkit為例,根據上圖來簡單介紹瀏覽器的渲染。具體解析、渲染會涉及到非常多的細節,請參考HTML5渲染規範和對應的頁面GPU渲染實現。
HTML解析
瀏覽器拿到具體的HTML文件之後,需要呼叫瀏覽器中使用的瀏覽器引擎中處理HTML的工具(HTML Parser)來將HTML文件解析成為DOM樹,將以便外部介面(JS)呼叫。
- 文件內容解析:將一大串字串解析為DOM之前需要從中分析出結構化的資訊讓HTML解析器可以很方便地提取資料進行其他操作,所以對於文件內容的解析是第一步。解析器有兩個處理過程——詞法分析(將字串切分成符合特定語法規範的符號)與語法分析(根據符合語法規範的符號構建對應該文件的語法樹)。
- HTML解析:根據HTML語法,將HTML標記到語法樹上構建成DOM(Document Object Model)。
CSS解析
- 根據CSS詞法和句法分析CSS檔案和
<style>
標籤包含的內容以及style屬性的值 - 每個CSS檔案都被解析成一個樣式表物件(StyleSheet object),這個物件裡包含了帶有選擇器的CSS規則,和對應CSS語法的物件
頁面渲染
解析完成後,瀏覽器引擎會通過DOM樹和CSS Rule樹來構造渲染樹。渲染樹的構建會產生Layout,Layout是定位座標和大小,是否換行,各種position, overflow, z-index屬性的集合,也就是對各個元素進行位置計算、樣式計算之後的結果。
接下來,根據渲染樹對頁面進行渲染(可以理解為“畫”元素)。
當然,將這個渲染的過程完成並顯示到螢幕上會涉及到顯示卡的繪製,視訊記憶體的修改,有興趣的讀者可以深入瞭解。本文只是將對於我們軟體開發者來說需要了解的部分進行總結,還有很多過程與機制沒有提到,github上的這個專案期待你的貢獻。
參考