【網(wǎng)絡(luò)通信與信息安全】之深入解析從輸入一個(gè)URL到頁(yè)面加載完成的過(guò)程
一、前言
從輸入一個(gè) URL,然后按下回車(chē)到顯示頁(yè)面,中間發(fā)生了什么?這是一道經(jīng)典的面試題,不光前端面試會(huì)問(wèn)到,后端面試也會(huì)被問(wèn)到。這道題沒(méi)有一個(gè)標(biāo)準(zhǔn)的答案,它涉及很多的知識(shí)點(diǎn),面試官會(huì)通過(guò)這道題了解你對(duì)哪一方面的知識(shí)比較擅長(zhǎng),然后繼續(xù)追問(wèn)看看你的掌握程度。當(dāng)然本篇博客的分析也只是我的一些個(gè)人理解,從前端的角度出發(fā),具體地分析從底層到高層、從硬件到軟件的原理,以及在同一個(gè)層次討論瀏覽器、操作系統(tǒng)、服務(wù)器是如何交互的。
其實(shí),這個(gè)問(wèn)題可以拆解成兩個(gè)過(guò)程:
用戶輸入 URL,客戶端(瀏覽器)拿到服務(wù)端的數(shù)據(jù);
瀏覽器獲取到數(shù)據(jù),呈現(xiàn)頁(yè)面(也就是瀏覽器工作過(guò)程)。
二、輸入網(wǎng)址到瀏覽器獲得資源的過(guò)程
① 按下 URL 地址首字母
以輸入“google.com”的地址為例,當(dāng)按下“g”鍵,瀏覽器接收到消息之后,會(huì)觸發(fā)自動(dòng)完成機(jī)制:
瀏覽器根據(jù)自己的算法,以及是否處于隱私瀏覽模式,會(huì)在瀏覽器的地址框下方給出輸入建議,大部分算法會(huì)優(yōu)先考慮根據(jù)你的搜索歷史和書(shū)簽等內(nèi)容給出建議。
你打算輸入 “google.com”,因此給出的建議并不匹配,但是輸入過(guò)程中仍然有大量的代碼在后臺(tái)運(yùn)行,每一次按鍵都會(huì)使得給出的建議更加準(zhǔn)確;
甚至有可能在你輸入之前,瀏覽器就將 “google.com” 建議給你。
② 按下回車(chē)鍵
為了從零開(kāi)始,我們選擇鍵盤(pán)上的回車(chē)鍵被按到最低處作為起點(diǎn),在這個(gè)時(shí)刻,一個(gè)專(zhuān)用于回車(chē)鍵的電流回路被直接地或者通過(guò)電容器間接地閉合,使得少量的電流進(jìn)入鍵盤(pán)的邏輯電路系統(tǒng),這個(gè)系統(tǒng)會(huì)掃描每個(gè)鍵的狀態(tài),對(duì)于按鍵開(kāi)關(guān)的電位彈跳變化進(jìn)行噪音消除(debounce),并將其轉(zhuǎn)化為鍵盤(pán)碼值。
在這里,回車(chē)的碼值是13,鍵盤(pán)控制器在得到碼值之后,將其編碼,用于之后的傳輸。現(xiàn)在這個(gè)傳輸過(guò)程幾乎都是通過(guò)通用串行總線(USB)或者藍(lán)牙(Bluetooth)來(lái)進(jìn)行的,以前是通過(guò) PS/2 或者 ADB 連接進(jìn)行。
USB 鍵盤(pán):
鍵盤(pán)的 USB 元件通過(guò)計(jì)算機(jī)上的 USB 接口與 USB 控制器相連接,USB 接口中的第一號(hào)針為它提供了 5V 的電壓;
鍵碼值存儲(chǔ)在鍵盤(pán)內(nèi)部電路的寄存器 endpoint 內(nèi);
USB 控制器大概每隔 10ms 便查詢一次 endpoint 以得到存儲(chǔ)的鍵碼值數(shù)據(jù),這個(gè)最短時(shí)間間隔由鍵盤(pán)提供
鍵值碼值通過(guò) USB 串行接口引擎被轉(zhuǎn)換成一個(gè)或者多個(gè)遵循低層 USB 協(xié)議的 USB 數(shù)據(jù)包;
這些數(shù)據(jù)包通過(guò) D+ 針或者 D- 針(中間的兩個(gè)針),以最高 1.5Mb/s 的速度從鍵盤(pán)傳輸至計(jì)算機(jī),速度限制是因?yàn)槿藱C(jī)交互設(shè)備總是被聲明成"低速設(shè)備"(USB 2.0 compliance);
這個(gè)串行信號(hào)在計(jì)算機(jī)的 USB 控制器處被解碼,然后被人機(jī)交互設(shè)備通用鍵盤(pán)驅(qū)動(dòng)進(jìn)行進(jìn)一步解釋?zhuān)蟀存I的碼值被傳輸?shù)讲僮飨到y(tǒng)的硬件抽象層。
虛擬鍵盤(pán)(觸屏設(shè)備):
在現(xiàn)代電容屏上,當(dāng)用戶把手指放在屏幕上時(shí),一小部分電流從傳導(dǎo)層的靜電域經(jīng)過(guò)手指?jìng)鲗?dǎo),形成了一個(gè)回路,使得屏幕上觸控的那一點(diǎn)電壓下降,屏幕控制器產(chǎn)生一個(gè)中斷,報(bào)告這次“點(diǎn)擊”的坐標(biāo);
然后移動(dòng)操作系統(tǒng)通知當(dāng)前活躍的應(yīng)用,有一個(gè)點(diǎn)擊事件發(fā)生在它的某個(gè) GUI 部件上了,現(xiàn)在這個(gè)部件是虛擬鍵盤(pán)的按鈕;
虛擬鍵盤(pán)引發(fā)一個(gè)軟中斷,返回給 OS 一個(gè)“按鍵按下”消息;
這個(gè)消息又返回來(lái)向當(dāng)前活躍的應(yīng)用通知一個(gè)“按鍵按下”事件。
鍵盤(pán)在它的中斷請(qǐng)求線(IRQ)上發(fā)送信號(hào),信號(hào)會(huì)被中斷控制器映射到一個(gè)中斷向量,實(shí)際上就是一個(gè)整型數(shù)。CPU 使用中斷描述符表(IDT)把中斷向量映射到對(duì)應(yīng)函數(shù),這些函數(shù)被稱(chēng)為中斷處理器,它們由操作系統(tǒng)內(nèi)核提供。當(dāng)一個(gè)中斷到達(dá)時(shí),CPU 根據(jù) IDT 和中斷向量索引到對(duì)應(yīng)的中斷處理器,然后操作系統(tǒng)內(nèi)核出場(chǎng)。
③ 一個(gè) WM_KEYDOWN 消息被發(fā)往應(yīng)用程序(Windows)
HID 把鍵盤(pán)按下的事件傳送給 KBDHID.sys 驅(qū)動(dòng),把 HID 的信號(hào)轉(zhuǎn)換成一個(gè)掃描碼(Scancode),這里回車(chē)的掃描碼是 VK_RETURN(0x0d)。 KBDHID.sys 驅(qū)動(dòng)和 KBDCLASS.sys (鍵盤(pán)類(lèi)驅(qū)動(dòng) keyboard class driver)進(jìn)行交互,這個(gè)驅(qū)動(dòng)負(fù)責(zé)安全地處理所有鍵盤(pán)和小鍵盤(pán)的輸入事件。之后它又去調(diào)用 Win32K.sys,在這之前有可能把消息傳遞給安裝的第三方鍵盤(pán)過(guò)濾器,這些都是發(fā)生在內(nèi)核模式。
Win32K.sys 通過(guò) GetForegroundWindow() API 函數(shù)找到當(dāng)前哪個(gè)窗口是活躍的,這個(gè) API 函數(shù)提供當(dāng)前瀏覽器的地址欄的句柄。Windows 系統(tǒng)的"message pump"機(jī)制調(diào)用 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam) 函數(shù), lParam 是一個(gè)用來(lái)指示這個(gè)按鍵的更多信息的掩碼,這些信息包括按鍵重復(fù)次數(shù)(這里是0),實(shí)際掃描碼(可能依賴(lài)于 OEM 廠商,不過(guò)通常不會(huì)是 VK_RETURN ),功能鍵(alt, shift, ctrl)是否被按下(在這里沒(méi)有),以及一些其他狀態(tài)。
Windows 的 SendMessage API 直接將消息添加到特定窗口句柄 hWnd 的消息隊(duì)列中,之后賦給 hWnd 的主要消息處理函數(shù) WindowProc 將會(huì)被調(diào)用,用于處理隊(duì)列中的消息。
當(dāng)前活躍的句柄 hWnd 實(shí)際上是一個(gè) edit control 控件,這種情況下,WindowProc 有一個(gè)用于處理 WM_KEYDOWN 消息的處理器,這段代碼會(huì)查看 SendMessage 傳入的第三個(gè)參數(shù) wParam,因?yàn)檫@個(gè)參數(shù)是 VK_RETURN,于是它知道用戶按下了回車(chē)鍵。
④ 一個(gè) KeyDown NSEvent 被發(fā)往應(yīng)用程序(Mac OS X)
中斷信號(hào)引發(fā)了 I/O Kit Kext 鍵盤(pán)驅(qū)動(dòng)的中斷處理事件,驅(qū)動(dòng)把信號(hào)翻譯成鍵碼值,然后傳給 OS X 的 WindowServer 進(jìn)程。
然后, WindowServer 將這個(gè)事件通過(guò) Mach 端口分發(fā)給合適的(活躍的,或者正在監(jiān)聽(tīng)的)應(yīng)用程序,這個(gè)信號(hào)會(huì)被放到應(yīng)用程序的消息隊(duì)列里。
隊(duì)列中的消息可以被擁有足夠高權(quán)限的線程使用 mach_ipc_dispatch 函數(shù)讀取到,這個(gè)過(guò)程通常是由 NSApplication 主事件循環(huán)產(chǎn)生并且處理的,通過(guò) NSEventType 為 KeyDown 的 NSEvent 。
⑤ Xorg 服務(wù)器監(jiān)聽(tīng)鍵碼值(GNU/Linux)
當(dāng)使用圖形化的 X Server 時(shí),X Server 會(huì)按照特定的規(guī)則把鍵碼值再一次映射,映射成掃描碼。
當(dāng)這個(gè)映射過(guò)程完成之后, X Server 把這個(gè)按鍵字符發(fā)送給窗口管理器(DWM、metacity、i3 等),窗口管理器再把字符發(fā)送給當(dāng)前窗口。
當(dāng)前窗口使用有關(guān)圖形 API 把文字打印在輸入框內(nèi)。
⑥ 解析 URL
瀏覽器通過(guò) URL 能夠知道下面的信息:
Protocol "http" 使用HTTP協(xié)議 Resource "/" 請(qǐng)求的資源是主頁(yè)(index)
1
2
3
4
那么輸入的是 URL 還是搜索的關(guān)鍵字?當(dāng)協(xié)議或主機(jī)名不合法時(shí),瀏覽器會(huì)將地址欄中輸入的文字傳給默認(rèn)的搜索引擎。大部分情況下,在把文字傳遞給搜索引擎的時(shí)候,URL 會(huì)帶有特定的一串字符,用來(lái)告訴搜索引擎這次搜索來(lái)自這個(gè)特定瀏覽器。
轉(zhuǎn)換非 ASCII 的 Unicode 字符:
瀏覽器檢查輸入是否含有不是 a-z, A-Z,0-9, - 或者 . 的字符;
這里主機(jī)名是 google.com ,所以沒(méi)有非 ASCII 的字符;如果有的話,瀏覽器會(huì)對(duì)主機(jī)名部分使用 Punycode 編碼。
⑦ 檢查 HSTS 列表
瀏覽器檢查自帶的“預(yù)加載 HSTS(HTTP 嚴(yán)格傳輸安全)”列表,這個(gè)列表里包含那些請(qǐng)求瀏覽器只使用 HTTPS 進(jìn)行連接的網(wǎng)站。
如果網(wǎng)站在這個(gè)列表里,瀏覽器會(huì)使用 HTTPS 而不是 HTTP 協(xié)議,否則,最初的請(qǐng)求會(huì)使用 HTTP 協(xié)議發(fā)送。
注意,一個(gè)網(wǎng)站哪怕不在 HSTS 列表里,也可以要求瀏覽器對(duì)自己使用 HSTS 政策進(jìn)行訪問(wèn)。瀏覽器向網(wǎng)站發(fā)出第一個(gè) HTTP 請(qǐng)求之后,網(wǎng)站會(huì)返回瀏覽器一個(gè)響應(yīng),請(qǐng)求瀏覽器只使用 HTTPS 發(fā)送請(qǐng)求。然而,就是這第一個(gè) HTTP 請(qǐng)求,卻可能會(huì)使用戶受到 添downgrade attack 的威脅,這也是為什么現(xiàn)代瀏覽器都預(yù)置了 HSTS 列表。
⑧ DNS 查詢
瀏覽器檢查域名是否在緩存當(dāng)中(要查看 Chrome 當(dāng)中的緩存, 打開(kāi) chrome://net-internals/#dns)。
如果緩存中沒(méi)有,就去調(diào)用 gethostbyname 庫(kù)函數(shù)(操作系統(tǒng)不同函數(shù)也不同)進(jìn)行查詢。
gethostbyname 函數(shù)在試圖進(jìn)行DNS解析之前首先檢查域名是否在本地 Hosts 里,Hosts 的位置 不同的操作系統(tǒng)有所不同。
如果 gethostbyname 沒(méi)有這個(gè)域名的緩存記錄,也沒(méi)有在 hosts 里找到,它將會(huì)向 DNS 服務(wù)器發(fā)送一條 DNS 查詢請(qǐng)求,DNS 服務(wù)器是由網(wǎng)絡(luò)通信棧提供的,通常是本地路由器或者 ISP 的緩存 DNS 服務(wù)器。
查詢本地 DNS 服務(wù)器:
如果 DNS 服務(wù)器和我們的主機(jī)在同一個(gè)子網(wǎng)內(nèi),系統(tǒng)會(huì)按照下面的 ARP 過(guò)程對(duì) DNS 服務(wù)器進(jìn)行 ARP查詢;
如果 DNS 服務(wù)器和我們的主機(jī)在不同的子網(wǎng),系統(tǒng)會(huì)按照下面的 ARP 過(guò)程對(duì)默認(rèn)網(wǎng)關(guān)進(jìn)行查詢。
⑨ ARP 過(guò)程
要想發(fā)送 ARP(地址解析協(xié)議)廣播,需要有一個(gè)目標(biāo) IP 地址,同時(shí)還需要知道用于發(fā)送 ARP 廣播的接口的 MAC 地址:
首先查詢 ARP 緩存,如果緩存命中,返回結(jié)果:目標(biāo) IP = MAC;
如果緩存沒(méi)有命中:
查看路由表,看看目標(biāo) IP 地址是不是在本地路由表中的某個(gè)子網(wǎng)內(nèi),如果是,使用跟那個(gè)子網(wǎng)相連的接口,否則使用與默認(rèn)網(wǎng)關(guān)相連的接口;
查詢選擇的網(wǎng)絡(luò)接口的 MAC 地址;
發(fā)送一個(gè)二層( OSI 模型中的數(shù)據(jù)鏈路層)ARP 請(qǐng)求:
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
1
2
3
4
根據(jù)連接主機(jī)和路由器的硬件類(lèi)型不同,可以分為以下幾種情況:
直連:如果和路由器是直接連接的,路由器會(huì)返回一個(gè) 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
1
2
3
4
集線器:如果連接到一個(gè)集線器,集線器會(huì)把 ARP 請(qǐng)求向所有其它端口廣播,如果路由器也“連接”在其中,它會(huì)返回一個(gè) ARP Reply 。
交換機(jī):
如果連接到了一個(gè)交換機(jī),交換機(jī)會(huì)檢查本地 CAM/MAC 表,查看哪個(gè)端口有要找的那個(gè) MAC 地址,如果沒(méi)有找到,交換機(jī)會(huì)向所有其它端口廣播這個(gè) ARP 請(qǐng)求。
如果交換機(jī)的 MAC/CAM 表中有對(duì)應(yīng)的條目,交換機(jī)會(huì)向想要查詢的 MAC 地址的那個(gè)端口發(fā)送 ARP 請(qǐng)求;
如果路由器也“連接”在其中,它會(huì)返回一個(gè) ARP Reply。
有 DNS 服務(wù)器或者默認(rèn)網(wǎng)關(guān)的 IP 地址,就可以繼續(xù) DNS 請(qǐng)求:
使用 53 端口向 DNS 服務(wù)器發(fā)送 UDP 請(qǐng)求包,如果響應(yīng)包太大,會(huì)使用 TCP 協(xié)議;
如果本地/ISP DNS 服務(wù)器沒(méi)有找到結(jié)果,它會(huì)發(fā)送一個(gè)遞歸查詢請(qǐng)求,一層一層向高層 DNS 服務(wù)器做查詢,直到查詢到起始授權(quán)機(jī)構(gòu),如果找到會(huì)把結(jié)果返回。
⑩ 使用套接字
當(dāng)瀏覽器得到目標(biāo)服務(wù)器的 IP 地址,以及 URL 中給出來(lái)端口號(hào)(http 協(xié)議默認(rèn)端口號(hào)是 80, https 默認(rèn)端口號(hào)是 443),它會(huì)調(diào)用系統(tǒng)庫(kù)函數(shù) socket ,請(qǐng)求一個(gè) TCP流套接字,對(duì)應(yīng)的參數(shù)是 AF_INET/AF_INET6 和 SOCK_STREAM 。
該請(qǐng)求首先被交給傳輸層,在傳輸層請(qǐng)求被封裝成 TCP segment,目標(biāo)端口會(huì)被加入頭部,源端口會(huì)在系統(tǒng)內(nèi)核的動(dòng)態(tài)端口范圍內(nèi)選取(Linux 下是 ip_local_port_range)。
TCP segment 被送往網(wǎng)絡(luò)層,網(wǎng)絡(luò)層會(huì)在其中再加入一個(gè) IP 頭部,里面包含目標(biāo)服務(wù)器的IP地址以及本機(jī)的 IP 地址,把它封裝成一個(gè) IP packet。
TCP packet 接下來(lái)會(huì)進(jìn)入鏈路層,鏈路層會(huì)在封包中加入 frame 頭部,里面包含本地內(nèi)置網(wǎng)卡的 MAC 地址以及網(wǎng)關(guān)(本地路由器)的 MAC 地址。像前面說(shuō)的一樣,如果內(nèi)核不知道網(wǎng)關(guān)的 MAC 地址,它必須進(jìn)行 ARP 廣播來(lái)查詢其地址。
到現(xiàn)在,TCP 封包已經(jīng)準(zhǔn)備好,可以使用下面的方式進(jìn)行傳輸:
以太網(wǎng)
WiFi
蜂窩數(shù)據(jù)網(wǎng)絡(luò)
對(duì)于大部分家庭網(wǎng)絡(luò)和小型企業(yè)網(wǎng)絡(luò)來(lái)說(shuō),封包會(huì)從本地計(jì)算機(jī)出發(fā),經(jīng)過(guò)本地網(wǎng)絡(luò),再通過(guò)調(diào)制解調(diào)器把數(shù)字信號(hào)轉(zhuǎn)換成模擬信號(hào),使其適于在電話線路,有線電視光纜和無(wú)線電話線路上傳輸。在傳輸線路的另一端,是另外一個(gè)調(diào)制解調(diào)器,它把模擬信號(hào)轉(zhuǎn)換回?cái)?shù)字信號(hào),交由下一個(gè)網(wǎng)絡(luò)節(jié)點(diǎn)處理。
最終封包會(huì)到達(dá)管理本地子網(wǎng)的路由器。在那里出發(fā),它會(huì)繼續(xù)經(jīng)過(guò)自治區(qū)域(autonomous system, 縮寫(xiě) AS)的邊界路由器,其他自治區(qū)域,最終到達(dá)目標(biāo)服務(wù)器。一路上經(jīng)過(guò)的這些路由器會(huì)從IP數(shù)據(jù)報(bào)頭部里提取出目標(biāo)地址,并將封包正確地路由到下一個(gè)目的地。IP數(shù)據(jù)報(bào)頭部 time to live (TTL) 域的值每經(jīng)過(guò)一個(gè)路由器就減1,如果封包的TTL變?yōu)?,或者路由器由于網(wǎng)絡(luò)擁堵等原因封包隊(duì)列滿了,那么這個(gè)包會(huì)被路由器丟棄。
上面的發(fā)送和接受過(guò)程在 TCP 連接期間會(huì)發(fā)生很多次:
客戶端選擇一個(gè)初始序列號(hào)(ISN),將設(shè)置 SYN 位的封包發(fā)送給服務(wù)器端,表明自己要建立連接并設(shè)置了初始序列號(hào);
服務(wù)器端接收到 SYN 包,如果它可以建立連接:
服務(wù)器端選擇它自己的初始序列號(hào);
服務(wù)器端設(shè)置 SYN 位,表明自己選擇一個(gè)初始序列號(hào);
服務(wù)器端把 (客戶端ISN + 1) 復(fù)制到 ACK 域,并且設(shè)置 ACK 位,表明自己接收到客戶端的第一個(gè)封包。
客戶端通過(guò)發(fā)送下面一個(gè)封包來(lái)確認(rèn)這次連接:
自己的序列號(hào) +1;
接收端 ACK+1
設(shè)置 ACK 位;
數(shù)據(jù)通過(guò)下面的方式傳輸:
當(dāng)一方發(fā)送了 N 個(gè) Bytes 的數(shù)據(jù)之后,將自己的 SEQ 序列號(hào)也增加 N;
另一方確認(rèn)接收到這個(gè)數(shù)據(jù)包(或者一系列數(shù)據(jù)包)之后,它發(fā)送一個(gè) ACK 包,ACK 的值設(shè)置為接收到的數(shù)據(jù)包的最后一個(gè)序列號(hào);
關(guān)閉連接時(shí):
要關(guān)閉連接的一方發(fā)送一個(gè) FIN 包;
另一方確認(rèn)這個(gè) FIN 包,并且發(fā)送自己的 FIN 包;
要關(guān)閉的一方使用 ACK 包來(lái)確認(rèn)接收到了 FIN。
? TLS 握手
客戶端發(fā)送一個(gè) ClientHello 消息到服務(wù)器端,消息中同時(shí)包含它的 Transport Layer Security (TLS) 版本,可用的加密算法和壓縮算法。
服務(wù)器端向客戶端返回一個(gè) ServerHello 消息,消息中包含服務(wù)器端的TLS版本,服務(wù)器所選擇的加密和壓縮算法,以及數(shù)字證書(shū)認(rèn)證機(jī)構(gòu)(Certificate Authority,縮寫(xiě) CA)簽發(fā)的服務(wù)器公開(kāi)證書(shū),證書(shū)中包含了公鑰。客戶端會(huì)使用這個(gè)公鑰加密接下來(lái)的握手過(guò)程,直到協(xié)商生成一個(gè)新的對(duì)稱(chēng)密鑰。
客戶端根據(jù)自己的信任 CA 列表,驗(yàn)證服務(wù)器端的證書(shū)是否可信,如果認(rèn)為可信,客戶端會(huì)生成一串偽隨機(jī)數(shù),使用服務(wù)器的公鑰加密它,這串隨機(jī)數(shù)會(huì)被用于生成新的對(duì)稱(chēng)密鑰。
服務(wù)器端使用自己的私鑰解密上面提到的隨機(jī)數(shù),然后使用這串隨機(jī)數(shù)生成自己的對(duì)稱(chēng)主密鑰。
客戶端發(fā)送一個(gè) Finished 消息給服務(wù)器端,使用對(duì)稱(chēng)密鑰加密這次通訊的一個(gè)散列值。
服務(wù)器端生成自己的 hash 值,然后解密客戶端發(fā)送來(lái)的信息,檢查這兩個(gè)值是否對(duì)應(yīng),如果對(duì)應(yīng),就向客戶端發(fā)送一個(gè) Finished 消息,也使用協(xié)商好的對(duì)稱(chēng)密鑰加密。
從現(xiàn)在開(kāi)始,接下來(lái)整個(gè) TLS 會(huì)話都使用對(duì)稱(chēng)秘鑰進(jìn)行加密,傳輸應(yīng)用層(HTTP)內(nèi)容。
? HTTP 協(xié)議
如果瀏覽器是 Google 出品的,它不會(huì)使用 HTTP 協(xié)議來(lái)獲取頁(yè)面信息,而是會(huì)與服務(wù)器端發(fā)送請(qǐng)求,商討使用 SPDY 協(xié)議。
如果瀏覽器使用 HTTP 協(xié)議而不支持 SPDY 協(xié)議,它會(huì)向服務(wù)器發(fā)送這樣的一個(gè)請(qǐng)求:
GET / HTTP/1.1 Host: google.com Connection: close [其他頭部]
1
2
3
4
“其它頭部”包含一系列的由冒號(hào)分割開(kāi)的鍵值對(duì),它們的格式符合 HTTP 協(xié)議標(biāo)準(zhǔn),它們之間由一個(gè)換行符分割開(kāi)來(lái)(假設(shè)瀏覽器沒(méi)有違反 HTTP 協(xié)議標(biāo)準(zhǔn)的 bug,同時(shí)假設(shè)瀏覽器使用 HTTP/1.1 協(xié)議,不然的話頭部可能不包含 Host 字段,同時(shí) GET 請(qǐng)求中的版本號(hào)會(huì)變成 HTTP/1.0 或者 HTTP/0.9)。
HTTP/1.1 定義“關(guān)閉連接”的選項(xiàng) “close”,發(fā)送者使用這個(gè)選項(xiàng)指示這次連接在響應(yīng)結(jié)束之后會(huì)斷開(kāi)。例如:
Connection:close
1
不支持持久連接的 HTTP/1.1 應(yīng)用必須在每條消息中都包含 “close” 選項(xiàng)。
在發(fā)送完這些請(qǐng)求和頭部之后,瀏覽器發(fā)送一個(gè)換行符,表示要發(fā)送的內(nèi)容已經(jīng)結(jié)束。
服務(wù)器端返回一個(gè)響應(yīng)碼,指示這次請(qǐng)求的狀態(tài),響應(yīng)的形式是這樣的:
200 OK [響應(yīng)頭部]
1
2
然后是一個(gè)換行,接下來(lái)有效載荷(payload),也就是 www.google.com 的HTML內(nèi)容,服務(wù)器下面可能會(huì)關(guān)閉連接,如果客戶端請(qǐng)求保持連接的話,服務(wù)器端會(huì)保持連接打開(kāi),以供之后的請(qǐng)求重用。
如果瀏覽器發(fā)送的HTTP頭部包含了足夠多的信息(例如包含了 Etag 頭部),以至于服務(wù)器可以判斷出,瀏覽器緩存的文件版本自從上次獲取之后沒(méi)有再更改過(guò),服務(wù)器可能會(huì)返回這樣的響應(yīng):
304 Not Modified [響應(yīng)頭部]
1
2
這個(gè)響應(yīng)沒(méi)有有效載荷,瀏覽器會(huì)從自己的緩存中取出想要的內(nèi)容。在解析完 HTML 之后,瀏覽器和客戶端會(huì)重復(fù)上面的過(guò)程,直到HTML頁(yè)面引入的所有資源(圖片,CSS,favicon.ico等等)全部都獲取完畢,區(qū)別只是頭部的 GET / HTTP/1.1 會(huì)變成 GET /$(相對(duì)www.google.com的URL) HTTP/1.1 。
如果 HTML 引入 www.google.com 域名之外的資源,瀏覽器會(huì)回到上面解析域名那一步,按照下面的步驟往下一步一步執(zhí)行,請(qǐng)求中的 Host 頭部會(huì)變成另外的域名。
? HTTP 服務(wù)器請(qǐng)求處理
HTTPD(HTTP Daemon)在服務(wù)器端處理請(qǐng)求/響應(yīng),最常見(jiàn)的 HTTPD 有 Linux 上常用的 Apache 和 nginx,以及 Windows 上的 IIS。
HTTPD 接收請(qǐng)求,服務(wù)器把請(qǐng)求拆分為以下幾個(gè)參數(shù):
HTTP 請(qǐng)求方法(GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, 或者 TRACE),直接在地址欄中輸入 URL 這種情況下,使用的是 GET 方法;
域名:google.com;
請(qǐng)求路徑/頁(yè)面:/ (沒(méi)有請(qǐng)求 google.com 下的指定的頁(yè)面,因此 / 是默認(rèn)的路徑)。
服務(wù)器驗(yàn)證其上已經(jīng)配置 google.com 的虛擬主機(jī)。
服務(wù)器驗(yàn)證 google.com 接受 GET 方法。
服務(wù)器驗(yàn)證該用戶可以使用 GET 方法(根據(jù) IP 地址,身份信息等)。
如果服務(wù)器安裝了 URL 重寫(xiě)模塊(例如 Apache 的 mod_rewrite 和 IIS 的 URL Rewrite),服務(wù)器會(huì)嘗試匹配重寫(xiě)規(guī)則,如果匹配上的話,服務(wù)器會(huì)按照規(guī)則重寫(xiě)這個(gè)請(qǐng)求。
服務(wù)器根據(jù)請(qǐng)求信息獲取相應(yīng)的響應(yīng)內(nèi)容,這種情況下由于訪問(wèn)路徑是 “/”,會(huì)訪問(wèn)首頁(yè)文件(可以重寫(xiě)這個(gè)規(guī)則,但是這個(gè)是最常用的)。
服務(wù)器會(huì)使用指定的處理程序分析處理這個(gè)文件,假如 Google 使用 PHP,服務(wù)器會(huì)使用 PHP 解析 index 文件,并捕獲輸出,把 PHP 的輸出結(jié)果返回給請(qǐng)求者。
? 瀏覽器
當(dāng)服務(wù)器提供資源之后(HTML,CSS,JS,圖片等),瀏覽器會(huì)執(zhí)行下面的操作:
解析:HTML,CSS,JS;
渲染:構(gòu)建 DOM 樹(shù) -> 渲染 -> 布局 -> 繪制。
瀏覽器的功能是從服務(wù)器上取回想要的資源,然后展示在瀏覽器窗口當(dāng)中,資源通常是 HTML 文件,也可能是 PDF、圖片或者其他類(lèi)型的內(nèi)容,資源的位置通過(guò)用戶提供的 URI(Uniform Resource Identifier) 來(lái)確定。
瀏覽器解釋和展示 HTML 文件的方法,這些標(biāo)準(zhǔn)由 Web 標(biāo)準(zhǔn)組織 W3C(World Wide Web Consortium) 維護(hù)。不同瀏覽器的用戶界面大都十分接近,有很多共同的 UI 元素:
一個(gè)地址欄
后退和前進(jìn)按鈕
書(shū)簽選項(xiàng)
刷新和停止按鈕
主頁(yè)按鈕
組成瀏覽器的組件有:
用戶界面包含地址欄,前進(jìn)后退按鈕,書(shū)簽菜單等,除了請(qǐng)求頁(yè)面之外所有你看到的內(nèi)容都是用戶界面的一部分;
瀏覽器引擎負(fù)責(zé)讓 UI 和渲染引擎協(xié)調(diào)工作;
渲染引擎渲染引擎負(fù)責(zé)展示請(qǐng)求內(nèi)容,如果請(qǐng)求的內(nèi)容是 HTML,渲染引擎會(huì)解析 HTML 和 CSS,然后將內(nèi)容展示在屏幕上;
網(wǎng)絡(luò)組件負(fù)責(zé)網(wǎng)絡(luò)調(diào)用,例如 HTTP 請(qǐng)求等,使用一個(gè)平臺(tái)無(wú)關(guān)接口,下層是針對(duì)不同平臺(tái)的具體實(shí)現(xiàn);
UI 后端用于繪制基本 UI 組件,如下拉列表框和窗口;UI 后端暴露一個(gè)統(tǒng)一的平臺(tái)無(wú)關(guān)的接口,下層使用操作系統(tǒng)的 UI 方法實(shí)現(xiàn);
Javascript 引擎用于解析和執(zhí)行 Javascript 代碼;
數(shù)據(jù)存儲(chǔ)組件是一個(gè)持久層,瀏覽器可能需要在本地存儲(chǔ)各種各樣的數(shù)據(jù),例如 Cookie 等;瀏覽器也需要支持諸如 localStorage,IndexedDB,WebSQL 和 FileSystem 之類(lèi)的存儲(chǔ)機(jī)制。
? HTML 解析
瀏覽器渲染引擎從網(wǎng)絡(luò)層取得請(qǐng)求的文檔,一般情況下文檔會(huì)分成 8 KB 大小的分塊傳輸。
HTML 解析器的主要工作是對(duì) HTML 文檔進(jìn)行解析,生成解析樹(shù),解析樹(shù)是以 DOM 元素以及屬性為節(jié)點(diǎn)的樹(shù)
DOM 是文檔對(duì)象模型(Document Object Model)的縮寫(xiě),它是 HTML 文檔的對(duì)象表示,同時(shí)也是 HTML 元素面向外部(如 Javascript)的接口。
樹(shù)的根部是"Document"對(duì)象,整個(gè) DOM 和 HTML 文檔幾乎是一對(duì)一的關(guān)系。
HTML 不能使用常見(jiàn)的自頂向下或自底向上方法來(lái)進(jìn)行分析,主要原因有以下幾點(diǎn):
語(yǔ)言本身的“寬容”特性;
HTML 本身可能是殘缺的,對(duì)于常見(jiàn)的殘缺,瀏覽器需要有傳統(tǒng)的容錯(cuò)機(jī)制來(lái)支持它們;
解析過(guò)程需要反復(fù),對(duì)于其它語(yǔ)言來(lái)說(shuō),源碼不會(huì)在解析過(guò)程中發(fā)生變化,但是對(duì)于 HTML 來(lái)說(shuō),動(dòng)態(tài)代碼,例如腳本元素中包含的 document.write() 方法會(huì)在源碼中添加內(nèi)容,也就是說(shuō),解析過(guò)程實(shí)際上會(huì)改變輸入的內(nèi)容。
由于不能使用常用的解析技術(shù),瀏覽器創(chuàng)造了專(zhuān)門(mén)用于解析 HTML 的解析器。解析算法主要包含了兩個(gè)階段:標(biāo)記化(tokenization)和樹(shù)的構(gòu)建。
解析結(jié)束之后:
瀏覽器開(kāi)始加載網(wǎng)頁(yè)的外部資源(CSS,圖像,Javascript 文件等);
此時(shí)瀏覽器把文檔標(biāo)記為可交互的(interactive),瀏覽器開(kāi)始解析處于“推遲(deferred)”模式的腳本,也就是那些需要在文檔解析完畢之后再執(zhí)行的腳本,之后文檔的狀態(tài)會(huì)變?yōu)椤巴瓿桑╟omplete)”,瀏覽器會(huì)觸發(fā)“加載(load)”事件。
注意解析 HTML 網(wǎng)頁(yè)時(shí)永遠(yuǎn)不會(huì)出現(xiàn)“無(wú)效語(yǔ)法(Invalid Syntax)”錯(cuò)誤,瀏覽器會(huì)修復(fù)所有錯(cuò)誤內(nèi)容,然后繼續(xù)解析。
? CSS 解析
根據(jù) CSS詞法和句法 分析 CSS 文件和