本文地址:https://github.com/skyline75489/what-happens-when-zh_CN
為了方便訪問,轉載至此。
按下"g"鍵
當你按下“g”鍵,瀏覽器接收到這個消息之后,會觸發自動完成機制。瀏覽器根據自己的算法,以及你是否處於隱私瀏覽模式,會在瀏覽器的地址框下方給出輸入建議。大部分算法會優先考慮根據你的搜索歷史和書簽等內容給出建議。持續輸入過程中仍然有大量的代碼在后台運行,你的每一次按鍵都會使得給出的建議更加准確。甚至有可能在你輸入之前,瀏覽器就將 "google.com" 建議給你。
解析URL
-
瀏覽器通過 URL 能夠知道下面的信息:
-
-
Protocol
"http" - 使用HTTP協議
-
-
-
Resource
"/" - 請求的資源是主頁(index)
-
-
輸入的是 URL 還是搜索的關鍵字?
當協議或主機名不合法時,瀏覽器會將地址欄中輸入的文字傳給默認的搜索引擎。大部分情況下,在把文字傳遞給搜索引擎的時候,URL會帶有特定的一串字符,用來告訴搜索引擎這次搜索來自這個特定瀏覽器。
轉換非 ASCII 的 Unicode 字符
- 瀏覽器檢查輸入是否含有不是
a-z
,A-Z
,0-9
,-
或者.
的字符 - 這里主機名是
google.com
,所以沒有非ASCII的字符;如果有的話,瀏覽器會對主機名部分使用 Punycode 編碼
檢查 HSTS 列表
- 瀏覽器檢查自帶的“預加載 HSTS(HTTP嚴格傳輸安全)”列表,這個列表里包含了那些請求瀏覽器只使用HTTPS進行連接的網站
- 如果網站在這個列表里,瀏覽器會使用 HTTPS 而不是 HTTP 協議,否則,最初的請求會使用HTTP協議發送
- 注意,一個網站哪怕不在 HSTS 列表里,也可以要求瀏覽器對自己使用 HSTS 政策進行訪問。瀏覽器向網站發出第一個 HTTP 請求之后,網站會返回瀏覽器一個響應,請求瀏覽器只使用 HTTPS 發送請求。然而,就是這第一個 HTTP 請求,卻可能會使用戶受到 downgrade attack 的威脅,這也是為什么現代瀏覽器都預置了 HSTS 列表。
DNS 查詢
- 瀏覽器檢查域名是否在緩存當中(要查看 Chrome 當中的緩存, 打開 chrome://net-internals/#dns)。
- 如果緩存中沒有,就去調用
gethostbyname
庫函數(操作系統不同函數也不同)進行查詢。 gethostbyname
函數在試圖進行DNS解析之前首先檢查域名是否在本地 Hosts 里,Hosts 的位置 不同的操作系統有所不同- 如果
gethostbyname
沒有這個域名的緩存記錄,也沒有在hosts
里找到,它將會向 DNS 服務器發送一條 DNS 查詢請求。DNS 服務器是由網絡通信棧提供的,通常是本地路由器或者 ISP 的緩存 DNS 服務器。 - 查詢本地 DNS 服務器
- 如果 DNS 服務器和我們的主機在同一個子網內,系統會按照下面的 ARP 過程對 DNS 服務器進行 ARP查詢
- 如果 DNS 服務器和我們的主機在不同的子網,系統會按照下面的 ARP 過程對默認網關進行查詢
ARP 過程
要想發送 ARP(地址解析協議)廣播,我們需要有一個目標 IP 地址,同時還需要知道用於發送 ARP 廣播的接口的 MAC 地址。
- 首先查詢 ARP 緩存,如果緩存命中,我們返回結果:目標 IP = MAC
如果緩存沒有命中:
- 查看路由表,看看目標 IP 地址是不是在本地路由表中的某個子網內。是的話,使用跟那個子網相連的接口,否則使用與默認網關相連的接口。
- 查詢選擇的網絡接口的 MAC 地址
- 我們發送一個二層( OSI 模型 中的數據鏈路層)ARP 請求:
ARP Request
:
Sender MAC: interface:mac:address:here Sender IP: interface.ip.goes.here Target MAC: FF:FF:FF:FF:FF:FF (Broadcast) Target IP: target.ip.goes.here
根據連接主機和路由器的硬件類型不同,可以分為以下幾種情況:
直連:
- 如果我們和路由器是直接連接的,路由器會返回一個
ARP Reply
(見下面)。
集線器:
- 如果我們連接到一個集線器,集線器會把 ARP 請求向所有其它端口廣播,如果路由器也“連接”在其中,它會返回一個
ARP Reply
。
交換機:
- 如果我們連接到了一個交換機,交換機會檢查本地 CAM/MAC 表,看看哪個端口有我們要找的那個 MAC 地址,如果沒有找到,交換機會向所有其它端口廣播這個 ARP 請求。
- 如果交換機的 MAC/CAM 表中有對應的條目,交換機會向有我們想要查詢的 MAC 地址的那個端口發送 ARP 請求
- 如果路由器也“連接”在其中,它會返回一個
ARP Reply
ARP Reply
:
Sender MAC: target:mac:address:here Sender IP: target.ip.goes.here Target MAC: interface:mac:address:here Target IP: interface.ip.goes.here
現在我們有了 DNS 服務器或者默認網關的 IP 地址,我們可以繼續 DNS 請求了:
- 使用 53 端口向 DNS 服務器發送 UDP 請求包,如果響應包太大,會使用 TCP 協議
- 如果本地/ISP DNS 服務器沒有找到結果,它會發送一個遞歸查詢請求,一層一層向高層 DNS 服務器做查詢,直到查詢到起始授權機構,如果找到會把結果返回
使用套接字
當瀏覽器得到了目標服務器的 IP 地址,以及 URL 中給出來端口號(http 協議默認端口號是 80, https 默認端口號是 443),它會調用系統庫函數 socket
,請求一個 TCP流套接字,對應的參數是 AF_INET/AF_INET6
和 SOCK_STREAM
。
- 這個請求首先被交給傳輸層,在傳輸層請求被封裝成 TCP segment。目標端口會被加入頭部,源端口會在系統內核的動態端口范圍內選取(Linux下是ip_local_port_range)
- TCP segment 被送往網絡層,網絡層會在其中再加入一個 IP 頭部,里面包含了目標服務器的IP地址以及本機的IP地址,把它封裝成一個IP packet。
- 這個 TCP packet 接下來會進入鏈路層,鏈路層會在封包中加入 frame 頭部,里面包含了本地內置網卡的MAC地址以及網關(本地路由器)的 MAC 地址。像前面說的一樣,如果內核不知道網關的 MAC 地址,它必須進行 ARP 廣播來查詢其地址。
到了現在,TCP 封包已經准備好了,可以使用下面的方式進行傳輸:
- 以太網
- WiFi
- 蜂窩數據網絡
對於大部分家庭網絡和小型企業網絡來說,封包會從本地計算機出發,經過本地網絡,再通過調制解調器把數字信號轉換成模擬信號,使其適於在電話線路,有線電視光纜和無線電話線路上傳輸。在傳輸線路的另一端,是另外一個調制解調器,它把模擬信號轉換回數字信號,交由下一個 網絡節點 處理。節點的目標地址和源地址將在后面討論。
大型企業和比較新的住宅通常使用光纖或直接以太網連接,這種情況下信號一直是數字的,會被直接傳到下一個 網絡節點 進行處理。
最終封包會到達管理本地子網的路由器。在那里出發,它會繼續經過自治區域(autonomous system, 縮寫 AS)的邊界路由器,其他自治區域,最終到達目標服務器。一路上經過的這些路由器會從IP數據報頭部里提取出目標地址,並將封包正確地路由到下一個目的地。IP數據報頭部 time to live (TTL) 域的值每經過一個路由器就減1,如果封包的TTL變為0,或者路由器由於網絡擁堵等原因封包隊列滿了,那么這個包會被路由器丟棄。
上面的發送和接受過程在 TCP 連接期間會發生很多次:
- 客戶端選擇一個初始序列號(ISN),將設置了 SYN 位的封包發送給服務器端,表明自己要建立連接並設置了初始序列號
-
- 服務器端接收到 SYN 包,如果它可以建立連接:
-
- 服務器端選擇它自己的初始序列號
- 服務器端設置 SYN 位,表明自己選擇了一個初始序列號
- 服務器端把 (客戶端ISN + 1) 復制到 ACK 域,並且設置 ACK 位,表明自己接收到了客戶端的第一個封包
-
- 客戶端通過發送下面一個封包來確認這次連接:
-
- 自己的序列號+1
- 接收端 ACK+1
- 設置 ACK 位
-
- 數據通過下面的方式傳輸:
-
- 當一方發送了N個 Bytes 的數據之后,將自己的 SEQ 序列號也增加N
- 另一方確認接收到這個數據包(或者一系列數據包)之后,它發送一個 ACK 包,ACK 的值設置為接收到的數據包的最后一個序列號
-
- 關閉連接時:
-
- 要關閉連接的一方發送一個 FIN 包
- 另一方確認這個 FIN 包,並且發送自己的 FIN 包
- 要關閉的一方使用 ACK 包來確認接收到了 FIN
TLS 握手
- 客戶端發送一個
ClientHello
消息到服務器端,消息中同時包含了它的 Transport Layer Security (TLS) 版本,可用的加密算法和壓縮算法。 - 服務器端向客戶端返回一個
ServerHello
消息,消息中包含了服務器端的TLS版本,服務器所選擇的加密和壓縮算法,以及數字證書認證機構(Certificate Authority,縮寫 CA)簽發的服務器公開證書,證書中包含了公鑰。客戶端會使用這個公鑰加密接下來的握手過程,直到協商生成一個新的對稱密鑰 - 客戶端根據自己的信任CA列表,驗證服務器端的證書是否可信。如果認為可信,客戶端會生成一串偽隨機數,使用服務器的公鑰加密它。這串隨機數會被用於生成新的對稱密鑰
- 服務器端使用自己的私鑰解密上面提到的隨機數,然后使用這串隨機數生成自己的對稱主密鑰
- 客戶端發送一個
Finished
消息給服務器端,使用對稱密鑰加密這次通訊的一個散列值 - 服務器端生成自己的 hash 值,然后解密客戶端發送來的信息,檢查這兩個值是否對應。如果對應,就向客戶端發送一個
Finished
消息,也使用協商好的對稱密鑰加密 - 從現在開始,接下來整個 TLS 會話都使用對稱秘鑰進行加密,傳輸應用層(HTTP)內容
HTTP 協議
如果瀏覽器是 Google 出品的,它不會使用 HTTP 協議來獲取頁面信息,而是會與服務器端發送請求,商討使用 SPDY 協議。
如果瀏覽器使用 HTTP 協議而不支持 SPDY 協議,它會向服務器發送這樣的一個請求:
GET / HTTP/1.1 Host: google.com Connection: close [其他頭部]
“其他頭部”包含了一系列的由冒號分割開的鍵值對,它們的格式符合HTTP協議標准,它們之間由一個換行符分割開來。(這里我們假設瀏覽器沒有違反HTTP協議標准的bug,同時假設瀏覽器使用 HTTP/1.1
協議,不然的話頭部可能不包含 Host
字段,同時 GET
請求中的版本號會變成 HTTP/1.0
或者 HTTP/0.9
。)
HTTP/1.1 定義了“關閉連接”的選項 "close",發送者使用這個選項指示這次連接在響應結束之后會斷開。例如:
Connection:close
不支持持久連接的 HTTP/1.1 應用必須在每條消息中都包含 "close" 選項。
在發送完這些請求和頭部之后,瀏覽器發送一個換行符,表示要發送的內容已經結束了。
服務器端返回一個響應碼,指示這次請求的狀態,響應的形式是這樣的:
200 OK [響應頭部]
然后是一個換行,接下來有效載荷(payload),也就是 www.google.com
的HTML內容。服務器下面可能會關閉連接,如果客戶端請求保持連接的話,服務器端會保持連接打開,以供之后的請求重用。
如果瀏覽器發送的HTTP頭部包含了足夠多的信息(例如包含了 Etag 頭部),以至於服務器可以判斷出,瀏覽器緩存的文件版本自從上次獲取之后沒有再更改過,服務器可能會返回這樣的響應:
304 Not Modified [響應頭部]
這個響應沒有有效載荷,瀏覽器會從自己的緩存中取出想要的內容。
在解析完 HTML 之后,瀏覽器和客戶端會重復上面的過程,直到HTML頁面引入的所有資源(圖片,CSS,favicon.ico等等)全部都獲取完畢,區別只是頭部的 GET / HTTP/1.1
會變成 GET /$(相對www.google.com的URL) HTTP/1.1
。
如果HTML引入了 www.google.com
域名之外的資源,瀏覽器會回到上面解析域名那一步,按照下面的步驟往下一步一步執行,請求中的 Host
頭部會變成另外的域名。
HTTP 服務器請求處理
HTTPD(HTTP Daemon)在服務器端處理請求/響應。最常見的 HTTPD 有 Linux 上常用的 Apache 和 nginx,以及 Windows 上的 IIS。
- HTTPD 接收請求
-
- 服務器把請求拆分為以下幾個參數:
-
- HTTP 請求方法(
GET
,POST
,HEAD
,PUT
,DELETE
,CONNECT
,OPTIONS
, 或者TRACE
)。直接在地址欄中輸入 URL 這種情況下,使用的是 GET 方法 - 域名:google.com
- 請求路徑/頁面:/ (我們沒有請求google.com下的指定的頁面,因此 / 是默認的路徑)
- HTTP 請求方法(
- 服務器驗證其上已經配置了 google.com 的虛擬主機
- 服務器驗證 google.com 接受 GET 方法
- 服務器驗證該用戶可以使用 GET 方法(根據 IP 地址,身份信息等)
- 如果服務器安裝了 URL 重寫模塊(例如 Apache 的 mod_rewrite 和 IIS 的 URL Rewrite),服務器會嘗試匹配重寫規則,如果匹配上的話,服務器會按照規則重寫這個請求
- 服務器根據請求信息獲取相應的響應內容,這種情況下由於訪問路徑是 "/" ,會訪問首頁文件(你可以重寫這個規則,但是這個是最常用的)。
- 服務器會使用指定的處理程序分析處理這個文件,假如 Google 使用 PHP,服務器會使用 PHP 解析 index 文件,並捕獲輸出,把 PHP 的輸出結果返回給請求者
瀏覽器背后的故事
當服務器提供了資源之后(HTML,CSS,JS,圖片等),瀏覽器會執行下面的操作:
- 解析 —— HTML,CSS,JS
- 渲染 —— 構建 DOM 樹 -> 渲染 -> 布局 -> 繪制
瀏覽器
瀏覽器的功能是從服務器上取回你想要的資源,然后展示在瀏覽器窗口當中。資源通常是 HTML 文件,也可能是 PDF,圖片,或者其他類型的內容。資源的位置通過用戶提供的 URI(Uniform Resource Identifier) 來確定。
瀏覽器解釋和展示 HTML 文件的方法,在 HTML 和 CSS 的標准中有詳細介紹。這些標准由 Web 標准組織 W3C(World Wide Web Consortium) 維護。
不同瀏覽器的用戶界面大都十分接近,有很多共同的 UI 元素:
- 一個地址欄
- 后退和前進按鈕
- 書簽選項
- 刷新和停止按鈕
- 主頁按鈕
瀏覽器高層架構
組成瀏覽器的組件有:
- 用戶界面 用戶界面包含了地址欄,前進后退按鈕,書簽菜單等等,除了請求頁面之外所有你看到的內容都是用戶界面的一部分
- 瀏覽器引擎 瀏覽器引擎負責讓 UI 和渲染引擎協調工作
- 渲染引擎 渲染引擎負責展示請求內容。如果請求的內容是 HTML,渲染引擎會解析 HTML 和 CSS,然后將內容展示在屏幕上
- 網絡組件 網絡組件負責網絡調用,例如 HTTP 請求等,使用一個平台無關接口,下層是針對不同平台的具體實現
- UI后端 UI 后端用於繪制基本 UI 組件,例如下拉列表框和窗口。UI 后端暴露一個統一的平台無關的接口,下層使用操作系統的 UI 方法實現
- Javascript 引擎 Javascript 引擎用於解析和執行 Javascript 代碼
- 數據存儲 數據存儲組件是一個持久層。瀏覽器可能需要在本地存儲各種各樣的數據,例如 Cookie 等。瀏覽器也需要支持諸如 localStorage,IndexedDB,WebSQL 和 FileSystem 之類的存儲機制
HTML 解析
瀏覽器渲染引擎從網絡層取得請求的文檔,一般情況下文檔會分成8kB大小的分塊傳輸。
HTML 解析器的主要工作是對 HTML 文檔進行解析,生成解析樹。
解析樹是以 DOM 元素以及屬性為節點的樹。DOM是文檔對象模型(Document Object Model)的縮寫,它是 HTML 文檔的對象表示,同時也是 HTML 元素面向外部(如Javascript)的接口。樹的根部是"Document"對象。整個 DOM 和 HTML 文檔幾乎是一對一的關系。
解析算法
HTML不能使用常見的自頂向下或自底向上方法來進行分析。主要原因有以下幾點:
- 語言本身的“寬容”特性
- HTML 本身可能是殘缺的,對於常見的殘缺,瀏覽器需要有傳統的容錯機制來支持它們
- 解析過程需要反復。對於其他語言來說,源碼不會在解析過程中發生變化,但是對於 HTML 來說,動態代碼,例如腳本元素中包含的 document.write() 方法會在源碼中添加內容,也就是說,解析過程實際上會改變輸入的內容
由於不能使用常用的解析技術,瀏覽器創造了專門用於解析 HTML 的解析器。解析算法在 HTML5 標准規范中有詳細介紹,算法主要包含了兩個階段:標記化(tokenization)和樹的構建。
解析結束之后
瀏覽器開始加載網頁的外部資源(CSS,圖像,Javascript 文件等)。
此時瀏覽器把文檔標記為可交互的(interactive),瀏覽器開始解析處於“推遲(deferred)”模式的腳本,也就是那些需要在文檔解析完畢之后再執行的腳本。之后文檔的狀態會變為“完成(complete)”,瀏覽器會觸發“加載(load)”事件。
注意解析 HTML 網頁時永遠不會出現“無效語法(Invalid Syntax)”錯誤,瀏覽器會修復所有錯誤內容,然后繼續解析。
CSS 解析
- 根據 CSS詞法和句法 分析CSS文件和
<style>
標簽包含的內容以及 style 屬性的值 - 每個CSS文件都被解析成一個樣式表對象(
StyleSheet object
),這個對象里包含了帶有選擇器的CSS規則,和對應CSS語法的對象 - CSS解析器可能是自頂向下的,也可能是使用解析器生成器生成的自底向上的解析器
頁面渲染
- 通過遍歷DOM節點樹創建一個“Frame 樹”或“渲染樹”,並計算每個節點的各個CSS樣式值
- 通過累加子節點的寬度,該節點的水平內邊距(padding)、邊框(border)和外邊距(margin),自底向上的計算"Frame 樹"中每個節點的首選(preferred)寬度
- 通過自頂向下的給每個節點的子節點分配可行寬度,計算每個節點的實際寬度
- 通過應用文字折行、累加子節點的高度和此節點的內邊距(padding)、邊框(border)和外邊距(margin),自底向上的計算每個節點的高度
- 使用上面的計算結果構建每個節點的坐標
- 當存在元素使用
floated
,位置有absolutely
或relatively
屬性的時候,會有更多復雜的計算,詳見http://dev.w3.org/csswg/css2/ 和 http://www.w3.org/Style/CSS/current-work - 創建layer(層)來表示頁面中的哪些部分可以成組的被繪制,而不用被重新柵格化處理。每個幀對象都被分配給一個層
- 頁面上的每個層都被分配了紋理(?)
- 每個層的幀對象都會被遍歷,計算機執行繪圖命令繪制各個層,此過程可能由CPU執行柵格化處理,或者直接通過D2D/SkiaGL在GPU上繪制
- 上面所有步驟都可能利用到最近一次頁面渲染時計算出來的各個值,這樣可以減少不少計算量
- 計算出各個層的最終位置,一組命令由 Direct3D/OpenGL發出,GPU命令緩沖區清空,命令傳至GPU並異步渲染,幀被送到Window Server。
GPU 渲染
- 在渲染過程中,圖形處理層可能使用通用用途的
CPU
,也可能使用圖形處理器GPU
- 當使用
GPU
用於圖形渲染時,圖形驅動軟件會把任務分成多個部分,這樣可以充分利用GPU
強大的並行計算能力,用於在渲染過程中進行大量的浮點計算。
Window Server
后期渲染與用戶引發的處理
渲染結束后,瀏覽器根據某些時間機制運行JavaScript代碼(比如Google Doodle動畫)或與用戶交互(在搜索欄輸入關鍵字獲得搜索建議)。類似Flash和Java的插件也會運行,盡管Google主頁里沒有。這些腳本可以觸發網絡請求,也可能改變網頁的內容和布局,產生又一輪渲染與繪制。