(轉) 當···時發生了什麼?

原文: https://github.com/skyline75489/what-happens-when-zh_CNcss

 

這個倉庫試圖回答一個古老的面試問題:當你在瀏覽器中輸入google.com而且按下回車以後發生了什麼?html

不過咱們再也不侷限於日常的回答,而是想辦法回答地儘量具體,不遺漏任何細節。node

這將是一個協做的過程,因此深刻挖掘吧,而且幫助咱們一塊兒完善它。仍然有大量的細節等待着你來添加,歡迎向咱們發送Pull Requset!linux

這些內容使用 Creative Commons Zero 協議發佈。git

目錄

  • 回車鍵按下
  • 產生中斷[非USB鍵盤]
  • (Windows)一個 WM_KEYDOWN 消息被髮往應用程序
  • (Mac OS X)一個 KeyDown NSEvent被髮往應用程序
  • (GNU/Linux)Xorg 服務器監聽鍵碼值
  • 解析URL
  • 輸入的是URL仍是搜索的關鍵字?
  • 檢查HSTS列表···
  • 轉換非ASCII的Unicode字符
  • DNS查詢···
  • ARP
  • 使用套接字
    • UDP 數據包
  • TLS 握手
    • TCP 數據包
  • HTTP 協議···
  • HTTP服務器請求處理
  • HTML 解析
  • CSS 解析
  • 頁面渲染
  • GPU 渲染
  • Window Server
  • 後期渲染與用戶引起的處理

回車鍵按下

爲了從頭開始,咱們選擇鍵盤上的回車鍵被按到最低處做爲起點。在這個時刻,一個專用於回車鍵的電流回路被直接或者經過電容器閉合了,使得少許的電流進入了鍵盤的邏輯電路系統。這個系統會掃描每一個鍵的狀態,對於按鍵開關的電位彈跳變化進行噪音消除(debounce),並將其轉化爲鍵盤碼值。在這裏,回車的碼值是13。鍵盤控制器在獲得碼值以後,將其編碼,用於以後的傳輸。如今這個傳輸過程幾乎都是經過通用串行總線(USB)或者藍牙(Bluetooth)來進行的,之前是經過PS/2或者ADB鏈接進行。github

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

(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-Z0-9, - 或者 . 的字符
  • 這裏主機名是 google.com ,因此沒有非ASCII的字符,若是有的話,瀏覽器會對主機名部分使用Punycode 編碼

DNS查詢···

  • 瀏覽器檢查域名是否在緩存當中
  • 若是緩存中沒有,就去調用 gethostbynme 庫函數(操做系統不一樣函數也不一樣)進行查詢
  • gethostbyname 函數在試圖進行DNS解析以前首先檢查域名是否在本地Hosts裏,Hosts的位置 不一樣的操做系統有所不一樣
  • 若是 gethostbyname 沒有這個域名的緩存記錄,也沒有在 hosts 裏找到,它將會向DNS 服務器發送一條DNS查詢請求。DNS服務器是由網絡通訊棧提供的,一般是本地路由器或者ISP的緩存DNS服務器。
  • 查詢本地DNS服務器
  • 若是DNS服務器和咱們的主機在同一個子網內,系統將會查詢ARP緩存,以獲得DNS服務器的ARP入口。緩存沒有命中的話就要進行ARP廣播(見下面),緩存命中的話,咱們就獲得了DNS.server.ip.address = dns:mac:address
  • 若是DNS服務器和咱們的主機在不一樣的子網,與上面相似,不過咱們的目標變成了默認網關,獲得的信息是default.gateway.ip.address = gateway:mac:address

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封包已經準備好了,但是使用下面的方式進行傳輸:

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

大型企業和比較新的住宅一般使用光纖或直接以太網鏈接,這種狀況下信號一直是數字的,會被直接傳到下一個 網絡節點 進行處理。

最終封包會到達管理本地子網的路由器。在那裏出發,它會繼續通過自治區域的邊界路由器,其餘自治區域,最終到達目標服務器。一路上通過的這些路由器會從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版本,服務器選擇了哪一個加密和壓縮算法,以及服務器的公開證書,證書中包含了公鑰。
  • 客戶端驗證服務器端的證書是否有效,使用非對稱加密算法加密一個對稱密鑰,客戶端將這個加密後的祕鑰發送給服務器端,同時附帶了服務器的公鑰和一個加密信息用於驗證身份。
  • 服務器端使用本身的私鑰解密上面提到的對稱祕鑰,而後向客戶端返回一個使用本身私鑰加密的驗證消息
  • 客戶端確認服務器端身份,生成一個會話祕鑰,將這個祕鑰連同一個 finished 消息給服務器端
  • 服務器端也向客戶端發送一個使用協商好的會話祕鑰加密的 finished 消息
  • 從如今開始,接下來整個會話都使用會話祕鑰進行加密

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 。

在發送完這些請求和頭部以後,瀏覽器發送一個換行符,表示要發送的內容已經結束了。

服務器端返回一個響應碼,指示此次請求的狀態,響應的形式是這樣的:

200 OK
[response headers]

而後是一個換行,接下來有效載荷(payload),也就是 www.google.com 的HTML內容。服務器下面可能會關閉鏈接,若是客戶端請求保持鏈接的話,服務器端會保持鏈接打開,以供之後的請求重用。

若是瀏覽器發送的HTTP頭部包含了足夠多的信息(例如包含了 Etag 頭部,以致於服務器能夠判斷出,瀏覽器緩存的文件版本自從上次獲取以後沒有再更改過,服務器可能會返回這樣的響應:

304 Not Modified
[response 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與windows的IIS。 * HTTPD接收請求 * 服務器把請求拆分爲如下幾個參數:

  • HTTP請求方法(GET, POST, HEAD, PUT 和 DELETE )。在訪問Google這種狀況下,使用的是GET方法
  • 域名:google.com
  • 請求路徑/頁面:/ (咱們沒有請求google.com下的指定的頁面,所以 / 是默認的路徑)
  • 服務器驗證其上已經配置了google.com的虛擬主機
  • 服務器驗證google.com接受GET方法
  • 服務器驗證該用戶可使用GET方法(根據IP地址,身份信息等)
  • 服務器根據請求信息獲取相應的響應內容,這種狀況下因爲訪問路徑是 "/" ,會訪問index這個文件。(你能夠重寫這個規則,可是這個是最經常使用的)
  • 服務器會使用指定的處理程序分析處理這個文件,好比假設Google使用PHP
  • 服務器會使用PHP解析index文件,並捕獲輸出
  • 服務器會返回PHP的輸出結果給請求者

HTML 解析

瀏覽器渲染引擎從網絡層取得請求的文檔,通常狀況下文檔會分紅8kB大小的分塊傳輸。

HTML解析器的主要工做是對HTML文檔進行解析,生成解析樹。

解析樹是以DOM元素以及屬性爲節點的樹。DOM是文檔對象模型(Document Object Model)的縮寫,它是HTML文檔的對象表示,同時也是HTML元素面向外部(如Javascript)的接口。樹的根部是"Document"對象。整個DOM和HTML文檔幾乎是一對一的關係。

解析算法

HTML不能使用常見的自頂向下或自底向上方法來進行分析。主要緣由有如下幾點:

  • 語言自己的「寬容」特性
  • HTML自己多是殘缺的,對於常見的殘缺,瀏覽器須要有傳統的容錯機制來支持它們
  • 解析過程須要反覆。對於其餘語言來講,源碼不會在解析過程當中發生變化,可是對於HTML來講,動態代碼,例如腳本元素中包含的 document.write() 方法會在源碼中添加內容,也就是說,解析過程實際上會改變輸入的內容

因爲不能使用經常使用的解析技術,瀏覽器創造了專門用於解析HTML的解析器。解析算法在 HTML5 標準規範中有詳細介紹,算法主要包含了兩個階段:標記化(tokenization)和樹的構建。

解析結束以後

當瀏覽器把文檔標記爲「可交互的」,瀏覽器開始解析處於「推遲」模式的腳本,也就是那些須要在文檔解析完畢以後再執行的腳本。以後文檔的狀態會變爲「完成」,瀏覽器會進行「加載」事件。

你能夠在 HTML5 標準規範中看到標記化和構建樹的詳細算法。

瀏覽器容錯

在解析HTML網頁是永遠不會出現「語法錯誤」,瀏覽器會修復因此錯誤,而後繼續解析。

加載/預加載網頁的外部資源(CSS,圖像,Javascript 文件等)

執行同步 Javascript 代碼。

CSS 解析

  • 根據 CSS詞法和句法 分析CSS文件和 <style> 標籤
  • 每一個CSS文件都被解析成一個樣式表對象,這個對象裏包含了帶有選擇器的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主頁裏沒有。這些腳本能夠觸發網絡請求,也可能改變網頁的內容和佈局,產生又一輪渲染與繪製。

相關文章
相關標籤/搜索