歡迎光臨
每天分享高質量文章

當你在瀏覽器中輸入Google.com並且按下回車之後發生了什麼?

本文試圖回答一個古老的面試問題:當你在瀏覽器中輸入Google.com並且按下回車之後發生了什麼?

不過我們不再侷限於平常的回答,而是想辦法回答地盡可能具體,不遺漏任何細節。

這將是一個協作的過程,所以深入挖掘吧,並且幫助我們一起完善它。仍然有大量的細節等待著你來新增,歡迎向我們傳送Pull Requset!

這些內容使用 Creative Commons Zero 協議釋出。

回車鍵按下

為了從頭開始,我們選擇鍵盤上的回車鍵被按到最低處作為起點。在這個時刻,一個專用於回車鍵的電流迴路被直接或者透過電容器閉合了,使得少量的電流進入了鍵盤的邏輯電路系統。這個系統會掃描每個鍵的狀態,對於按鍵開關的電位彈跳變化進行噪音消除(debounce),並將其轉化為鍵盤碼值。在這裡,回車的碼值是13。鍵盤控制器在得到碼值之後,將其編碼,用於之後的傳輸。現在這個傳輸過程幾乎都是透過通用序列匯流排(USB)或者藍芽(Bluetooth)來進行的,以前是透過PS/2或者ADB連線進行。

USB鍵盤:

  • 鍵盤的USB元件透過計算機上的USB介面與USB控制器相連線,USB介面中的第一號針為它提供了5V的電壓
  • 鍵碼值儲存在鍵盤內部電路一個叫做”endpoint”的暫存器內
  • USB控制器大概每隔10ms便查詢一次”endpoint”以得到儲存的鍵碼值資料,這個最短時間間隔由鍵盤提供
  • 鍵值碼值透過USB序列介面引擎被轉換成一個或者多個遵循低層USB協議的USB資料包
  • 這些資料包透過D+針或者D-針(中間的兩個針),以最高1.5Mb/s的速度從鍵盤傳輸至計算機。速度限制是因為人機互動裝置總是被宣告成”低速裝置”(USB 2.0 compliance)
  • 這個序列訊號在計算機的USB控制器處被解碼,然後被人機互動裝置通用鍵盤驅動進行進一步解釋。之後按鍵的碼值被傳輸到作業系統的硬體抽象層

虛擬鍵盤(觸屏裝置):

  • 在現代電容屏上,當使用者把手指放在螢幕上時,一小部分電流從傳導層的靜電域經過手指傳導,形成了一個迴路,使得螢幕上觸控的那一點電壓下降,螢幕控制器產生一個中斷,報告這次“點選”的坐標
  • 然後移動作業系統通知當前活躍的應用,有一個點選事件發生在它的某個GUI部件上了,現在這個部件是虛擬鍵盤的按鈕
  • 虛擬鍵盤引發一個軟中斷,傳回給OS一個“按鍵按下”訊息
  • 這個訊息又傳回來向當前活躍的應用通知一個“按鍵按下”事件

產生中斷[非USB鍵盤]

鍵盤在它的中斷請求線(IRQ)上傳送訊號,訊號會被中斷控制器對映到一個中斷向量,實際上就是一個整型數 。CPU使用中斷描述符表(IDT)把中斷向量對映到對應函式,這些函式被稱為中斷處理器,它們由作業系統核心提供。當一個中斷到達時,CPU根據IDT和中斷向量索引到對應的中端處理器,然後作業系統核心出場了。

(Windows)一個 WM_KEYDOWN 訊息被髮往應用程式

HID把鍵盤按下的事件傳送給 KBDHID.sys 驅動,把HID的訊號轉換成一個掃描碼(Scancode),這裡回車的掃描碼是 VK_RETURN(0x0d)。 KBDHID.sys 驅動和 KBDCLASS.sys (鍵盤類驅動,keyboard class driver)進行互動,這個驅動負責安全地處理所有鍵盤和小鍵盤的輸入事件。之後它又去呼叫 Win32K.sys ,在這之前有可能把訊息傳遞給安裝的第三方鍵盤過濾器。這些都是發生在核心樣式。

Win32K.sys 透過 GetForegroundWindow() API函式找到當前哪個視窗是活躍的。這個API函式提供了當前瀏覽器的位址列的控制代碼。Windows系統的”message pump”機制呼叫 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam) 函式, lParam 是一個用來指示這個按鍵的更多資訊的掩碼,這些資訊包括按鍵重覆次數(這裡是0),實際掃描碼(可能依賴於OEM廠商,不過通常不會是 VK_RETURN ),功能鍵(alt, shift, ctrl)是否被按下(在這裡沒有),以及一些其他狀態。

Windows的 SendMessage API直接將訊息新增到特定視窗控制代碼 hWnd 的訊息佇列中,之後賦給 hWnd 的主要訊息處理函式 WindowProc 將會被呼叫,用於處理佇列中的訊息。

當前活躍的控制代碼 hWnd 實際上是一個edit control控制元件,這種情況下,WindowProc 有一個用於處理WM_KEYDOWN 訊息的處理器,這段程式碼會檢視 SendMessage 傳入的第三個引數 wParam ,因為這個引數是 VK_RETURN ,於是它知道使用者按下了回車鍵。

(Mac OS X)一個 KeyDown NSEvent被髮往應用程式

中斷訊號引發了I/O Kit Kext鍵盤驅動的中斷處理事件,驅動把訊號翻譯成鍵碼值,然後傳給OS X的WindowServer 行程。然後, WindowServer 將這個事件透過Mach埠分發給合適的(活躍的,或者正在監聽的)應用程式,這個訊號會被放到應用程式的訊息佇列裡。佇列中的訊息可以被擁有足夠高許可權的執行緒使用 mach_ipc_dispatch 函式讀取到。這個過程通常是由 NSApplication 主事件迴圈產生並且處理的,透過 NSEventType 為 KeyDown 的 NSEvent 。

(GNU/Linux)Xorg 伺服器監聽鍵碼值

當使用圖形化的 X Server 時,X Server會按照特定的規則把鍵碼值再一次對映,對映成掃描碼。當這個對映過程完成之後, X Server 把這個按鍵字元傳送給視窗管理器(DWM,metacity, i3等等),視窗管理器再把字元傳送給當前視窗。當前視窗使用有關圖形API把文字列印在輸入框內。

解析URL

  • 瀏覽器透過URL能夠知道下麵的資訊:
    • Protocol ”http”
    • 使用HTTP協議
    • Resource ”/”
    • 請求的資源是主頁(index)

輸入的是URL還是搜尋的關鍵字?

當協議或主機名不合法時,瀏覽器會將位址列中輸入的文字傳給預設的搜尋引擎。大部分情況下,在把文字傳遞給搜尋引擎的時候,URL會帶有特定的一串字元,用來告訴搜尋引擎這次搜尋來自這個特定瀏覽器。

檢查HSTS串列···

  • 瀏覽器檢查自帶的“預載入HSTS(HTTP嚴格傳輸安全)”串列,這個串列裡包含了那些請求瀏覽器只使用HTTPS進行連線的網站
  • 如果網站在這個串列裡,瀏覽器會使用HTTPS而不是HTTP協議,否則,最初的請求會使用HTTP協議傳送
  • 註意,一個網站哪怕不在HSTS串列裡,也可以要求瀏覽器對自己使用HSTS政策進行訪問。瀏覽器向網站發出第一個HTTP請求之後,網站會傳回瀏覽器一個響應,請求瀏覽器只使用HTTPS傳送請求。然而,就是這第一個HTTP請求,卻可能會使使用者收到 downgrade attack 的威脅,這也是為什麼現代瀏覽器都預置了HSTS串列。

轉換非ASCII的Unicode字元

  • 瀏覽器檢查輸入是否含有不是 a-z, A-Z,0-9, – 或者 . 的字元
  • 這裡主機名是 google.com ,所以沒有非ASCII的字元,如果有的話,瀏覽器會對主機名部分使用Punycode 編碼

DNS查詢···

  • 瀏覽器檢查域名是否在快取當中
  • 如果快取中沒有,就去呼叫 gethostbynme 庫函式(作業系統不同函式也不同)進行查詢
  • 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地址
  • 我們傳送一個二層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 和SOCK_STREAM 。

  • 這個請求首先被交給傳輸層,在傳輸層請求被封裝成TCP segment。標的埠會會被加入頭部,源埠會在系統內核的動態埠範圍內選取(Linux下是ip_local_port_range)
  • TCP segment被送往網路層,網路層會在其中再加入一個IP頭部,裡麵包含了標的伺服器的IP地址以及本機的IP地址,把它封裝成一個TCP packet。
  • 這個TCP packet接下來會進入鏈路層,鏈路層會在封包中加入frame頭部,裡麵包含了本地內建網絡卡的MAC地址以及閘道器(本地路由器)的MAC地址。像前面說的一樣,如果核心不知道閘道器的MAC地址,它必須進行ARP廣播來查詢其地址。

到了現在,TCP封包已經準備好了,可是使用下麵的方式進行傳輸:

  • 乙太網
  • WiFi
  • 蜂窩資料網路

對於大部分家庭網路和小型企業網路來說,封包會從本地計算機出發,經過本地網路,再透過數據機把數字訊號轉換成模擬訊號,使其適於在電話線路,有線電視光纜和無線電話線路上傳輸。在傳輸線路的另一端,是另外一個數據機,它把模擬訊號轉換回數字訊號,交由下一個 網路節點 處理。節點的標的地址和源地址將在後面討論。

大型企業和比較新的住宅通常使用光纖或直接乙太網連線,這種情況下訊號一直是數字的,會被直接傳到下一個 網路節點 進行處理。

最終封包會到達管理本地子網的路由器。在那裡出發,它會繼續經過自治區域的邊界路由器,其他自治區域,最終到達標的伺服器。一路上經過的這些路由器會從IP資料報頭部裡提取出標的地址,並將封包正確地路由到下一個目的地。IP資料報頭部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

UDP 資料包

TLS 握手

  • 客戶端傳送一個 Client hello 訊息到伺服器端,訊息中同時包含了它的TLS版本,可用的加密演演算法和壓縮演演算法。
  • 伺服器端向客戶端傳回一個 Server hello 訊息,訊息中包含了伺服器端的TLS版本,伺服器選擇了哪個加密和壓縮演演算法,以及伺服器的公開證書,證書中包含了公鑰。客戶端會使用這個公鑰加密接下來的握手過程,直到協商生成一個新的對稱金鑰
  • 客戶端根據自己的信任CA串列,驗證伺服器端的證書是否有效。如果有效,客戶端會生成一串偽隨機數,使用伺服器的公鑰加密它。這串隨機數會被用於生成新的對稱金鑰
  • 伺服器端使用自己的私鑰解密上面提到的隨機數,然後使用這串隨機數生成自己的對稱主金鑰
  • 客戶端傳送一個 Finished 訊息給伺服器端,使用對稱金鑰加密這次通訊的一個雜湊值
  • 伺服器端生成自己的 hash 值,然後解密客戶端傳送來的資訊,檢查這兩個值是否對應。如果對應,就向客戶端傳送一個 Finished 訊息,也使用協商好的對稱金鑰加密
  • 從現在開始,接下來整個 TLS 會話都使用對稱秘鑰進行加密,傳輸應用層(HTTP)內容

TCP 資料包

HTTP 協議···

如果瀏覽器是Google出品的,它不會使用HTTP協議來獲取頁面資訊,而是會與伺服器端傳送請求,商討使用SPDY協議。

如果瀏覽器使用HTTP協議,它會向伺服器傳送這樣的一個請求:

GET / HTTP/1.1

Host: google.com

[其他頭部]

“其他頭部”包含了一系列的由冒號分割開的鍵值對,它們的格式符合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

[response essay-headers]

然後是一個換行,接下來有效載荷(payload),也就是 www.google.com 的HTML內容。伺服器下麵可能會關閉連線,如果客戶端請求保持連線的話,伺服器端會保持連線開啟,以供以後的請求重用。

如果瀏覽器傳送的HTTP頭部包含了足夠多的資訊(例如包含了 Etag 頭部,以至於伺服器可以判斷出,瀏覽器快取的檔案版本自從上次獲取之後沒有再更改過,伺服器可能會傳回這樣的響應:

304 Not Modified

[response essay-headers]

這個響應沒有有效載荷,瀏覽器會從自己的快取中取出想要的內容。

在解析完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 )。在訪問Google這種情況下,使用的是GET方法
    • 域名:google.com
    • 請求路徑/頁面:/ (我們沒有請求google.com下的指定的頁面,因此 / 是預設的路徑)
  • 伺服器驗證其上已經配置了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 檔案等)。

此時瀏覽器把檔案標記為“可互動的”,瀏覽器開始解析處於“推遲”樣式的指令碼,也就是那些需要在檔案解析完畢之後再執行的指令碼。之後檔案的狀態會變為“完成”,瀏覽器會進行“載入”事件。

註意解析 HTML 網頁時永遠不會出現“語法錯誤”,瀏覽器會修複所有錯誤,然後繼續解析。

執行同步 Javascript 程式碼。

CSS 解析

  • 根據 CSS詞法和句法 分析CSS檔案和
贊(0)

分享創造快樂