最經典的前端面試題之一,你能答出什麼幺蛾子?
本文的目標是以「輸入 URL 後發生了什麼」這個經典面試題為引子,寫一篇既能夠涵蓋面試中大部分網路試題,又能夠將「輸入 URL 後發生什麼」講得有深度的文章。以前寫過一篇類似的文章,但實在過於簡單。另外,HTTPS 逐漸普及,文章中沒有這部分過程也說不過去。不想修改原來的文章,就重新寫一篇吧。文中以我所在的項目「興趣部落」的官網 https://buluo.qq.com/index.html 為例子。
生成 HTTP 請求消息
解析完要訪問的目標伺服器是啥了,接下來瀏覽器就會用 HTTP 協議生成請求消息去 web伺服器請求資源,消息格式如下:
請求信息主要包括:
- 請求行:請求的方法(POST/GET/…)、URL、HTTP版本(1.1/2);
- 消息頭:請求的附加信息,以空行結束;
- 消息體:數據,比如 POST 請求時的表單數據。
對應的,響應消息也有 3 個部分組成:
- 狀態行:HTTP版本、狀態碼(200/304/404/…)、解釋狀態的響應短語;
- 消息頭
- 消息體:返回的數據。
用圖表示:
DNS
生成 HTTP 消息後,瀏覽器委託操作系統將消息發送給 web伺服器。而通過 web伺服器的名稱是沒法找到伺服器在哪的,好比知道一個人的名字沒法找到他家在哪一樣,網路中的地址是用 IP 地址表示的,所以要想跟伺服器通信,得先找到它的 IP 地址,使用 DNS(Domain Name System,域名服務系統) 伺服器可以將 web伺服器名稱轉換成 IP 地址。那這個過程是怎樣的呢?
操作系統有一個 Socket 庫,這個庫中的程序主要是讓應用程序調用操作系統的網路功能,而在這些功能中,瀏覽器需要調取操作系統的 DNS 解析功能。DNS 解析器生成一條表示「告訴我 https://buluo.qq.com/index.html 的 IP 地址」的消息,然後委託操作系統的協議棧發送 UDP 消息到 DNS 伺服器。那這條消息是如何發送到 DNS 伺服器又是如何將 IP 地址返回的呢?
首先介紹下操作系統中 DNS 解析器發送給 DNS 伺服器的消息內容,消息中包含 1)域名:http://buluo.qq.com;2)Class: IN,代表當前的網路是網際網路,DNS 設計之初還考慮了其他網路,雖然現在只有互聯網,但這個欄位還是保留了下來;3)記錄類型:A,表示域名對應的是 IP 地址,因為 DNS 還能解析其他地址,比如類型為 MX 時 DNS 伺服器會查詢郵件伺服器地址。DNS 伺服器中維護一張表,表的每一項包含上面三個欄位還有伺服器地址,當域名、Class、記錄類型全部匹配時,DNS 伺服器返回地址,在例子中會返回興趣部落首頁的 IP 地址。
但這個時候問題來了,世界上有不計其數的伺服器,將這些所有的伺服器信息都保存在一個 DNS 的表中肯定是不現實的,所以肯定有很多台 DNS 伺服器一起配合完成這個域名解析過程的,那具體過程是什麼樣的呢?
首先,DNS 伺服器中的所有信息都是按照域名來劃分層次的,這個層次是用 . 來分隔的,越靠右層次越高,比如 「buluo.qq.com」 中 「com」 層次最高,「qq」 次之,「buluo」 最後,其中每一層都被稱為「域」,比如 「com 域」下是 「qq」 域,再下是 「buluo」 域,域的層次劃分是為了更好地分配給不同國家、公司和組織等,典型的例子像南京市政府的官網:「www.nanjing.gov.cn」,「cn」 代表中國這個國家的域,「gov」 代表這個國家下的政府組織,「nanjing」 代表南京市政府。域有層次之分,那 DNS 伺服器呢?規定將管理下級域的 DNS 伺服器的 IP地址註冊到上級的 DNS 伺服器中,比如管理 「buluo.qq.com」 這個域的 DNS 伺服器的 IP地址需要註冊到 「qq.com」 域的 DNS 伺服器中,以此類推,一直到「根域」,就是 「cn」、「com」 這類域的上一層次,根域中就保存了 「cn」、「com」 等域名的 DNS 伺服器信息。此外,還需要將根域的 DNS 伺服器信息保存在所有的 DNS 伺服器中,這樣只要找到一台 DNS 伺服器就可以順藤摸瓜找到下層任何一個 DNS 伺服器。知道了域的層次劃分以及 DNS 伺服器的分布,下面就正式介紹如何尋找到相應的 DNS 伺服器並獲取 IP 地址。
首先,客戶端會訪問最近的一台 DNS 伺服器,但由於這台 DNS 伺服器上沒有 「buluo.qq.com」 這個域名的對應的信息,所以就向根域 DNS 伺服器發請求詢問,但根域中也沒有,但判定這個域名是屬於 「com」 域的,所以就返回其管理的 「com」 域的 DNS 伺服器的 IP 地址,意思是「雖然我不知道,但你可以去某某處問問,他應該知道」。然後 最近的那個 DNS 伺服器又向 「com」 域的 DNS 伺服器發請求,同理,也不知道,然後返回 「qq.com」 域的 DNS 伺服器,然後這台最近的 DNS 伺服器又向 「qq.com」 域 DNS 伺服器發請求,仍然沒有,直到最後,向 「buluo.qq.com」 這個域下的 DNS 伺服器發請求才拿到 IP 地址。接著,這台最近的 DNS 伺服器將獲得的 「buluo.qq.com」 的 IP 地址返回給客戶端,客戶端再拿著這個 IP 地址去請求資源。以上的過程用圖表示如下:
以上就是通過 DNS 服務獲取目標伺服器 IP 地址的過程,可以說是非常耗時,為了優化性能,DNS 伺服器會對中間的查詢結果做個緩存,為了保存緩存的實時性,每隔一段時間就會將緩存設為過期。
委託協議棧發送消息
現在客戶端拿到了目標伺服器的 IP 地址,下面就要與其連接並發送消息了,這個過程同樣不是瀏覽器做的,而是委託協議棧來完成的,具體過程是:
- 操作系統創建一個套接字,協議棧返回一個描述符,瀏覽器存儲起來,這個描述符是套接字的 ID,用於識別套接字,原因是同一個客戶端可能跟很多伺服器同時連接;
- 客戶端的套接字與服務端的套接字進行連接,連接成功後,協議棧將目標伺服器的 IP 地址和埠號保存在套接字中,下面就可以收發數據;
- 發送的數據是 HTTP 請求消息,發送的過程是:瀏覽器通過描述符查找到指定的套接字,並向套接字發送數據,數據便會通過網路傳輸到服務端的套接字,伺服器接收到消息後處理然後返迴響應消息;
- 消息返回後會被放入一塊內存緩衝區內,瀏覽器可以直接讀取這段消息。之後,操作系統斷開套接字連接,本地的套接字也會被刪除。
TCP 連接
在「委託協議棧發送消息」部分簡單地提了下客戶端和服務端利用套接字進行連接,那這個連接具體是什麼樣的呢?
首先什麼是套接字?套接字其實就是個放在內存的備忘錄,協議棧在發送數據時先看一眼備忘錄,了解這個數據是發到哪個埠,當數據發送出去後,這個備忘錄還得記錄什麼時間收到響應、什麼時候斷開等控制信息,協議棧需要根據這些信息來決定下一步做什麼。
客戶端和服務端的連接是通過套接字連接的,那「連接」又是什麼意思呢?連接實際上是客戶端和服務端互相交換控制信息的過程,控制信息主要包含兩種,一種是上面提到的套接字里要來幫助協議棧進行下一步操作的信息,另一種是客戶端和服務端通信時交換的控制信息,這種控制信息就是我們俗稱的 TCP 頭部。 那連接的過程是怎樣的呢?
這個連接過程就是我們平時經常聽到的三次握手。
- 首先客戶端創建 TCP 頭部,頭部包含目標伺服器的埠號等,同時將頭部的 SYN 設為 1,表示開始請求連接。TCP 頭部創建好了之後,TCP 模塊便將信息傳遞給 IP 模塊並委託它發送,然後信息經過網路到達伺服器的 IP 模塊再到 TCP 模塊,TCP 模塊則會根據 TCP 頭部的信息找到埠號對應的套接字,套接字則會寫入相應的信息,然後將狀態改為「正在連接」;
- 服務端的 TCP 模塊收到連接請求後就要回應,與客戶端一樣, 需要在 TCP 頭部設置發送方和接收方的埠號,以及將 SYN 設為 1,同時,返迴響應時還要將 ACK 設為 1,表示已經接收到相應的包。接著,將信息打包好,發送給客戶端;
- 客戶端收到消息後,發現 SYN 為 1,則表示連接成功,所以在套接字中寫入伺服器的埠號,同時將狀態改為連接完畢。為了告訴伺服器收到消息,客戶端也要將 ACK 設為 1,接著發送給服務端。
整個過程用圖表示如下:
HTTPS 的握手過程
上面的過程是最簡單的 HTTP 三次握手,但現在越來越多的網站使用了 HTTPS 協議,那與 HTTP 連接有什麼不同呢?
先介紹一下什麼是 HTTPS。HTTPS 正如其名字,HTTP 代表其並不是自己創建一個新的協議,而是建立在 HTTP 的基礎之上,S 代表其是安全的,如何保證安全?利用 SSL/TLS。SSL(Secure Sockets Layer,安全套接層)是網景設計的安全傳輸協議,經歷了 1.0、2.0 和 3.0 版本,但因為 1.0 有嚴重安全缺陷,所以從未公布。後來 IETF 將 SSL 標準化,稱為 TLS(Transport Layer Security, 傳輸層安全協議) ,TLS 1.0 與 SSL 3.0 差別很小。TLS 經歷了 1.0、1.1 到現在最新的 1.2。在 HTTPS 通信中具體使用哪一種還要看客戶端和服務端的支持程度。那 SSL/TLS 在網路模型中屬於哪一層呢?直接上圖:
在客戶端和服務端通過 HTTPS 連接的過程中,除了正常的 HTTP 連接中的事情,還有身份驗證和加密信息兩件事,下面看看具體過程(更詳細內容可以查看標準:RFC5246)。
- Client Hello:這次握手是客戶端向服務端發起加密通信請求,請求中包含以下關鍵信息:
- Version:客戶端支持的協議版本,比如 TLS 1.2;
- Random:第一個隨機數,作用在後面的握手步驟中介紹;
- Session ID:「空」表示這是一次新的連接,「不為空」表示維持前面的連接;
- Cipher Suites:密碼套件;
- Compression:客戶端支持的壓縮方法;
- Extensions:擴展。
- Server Hello:服務端收到客戶端消息後返迴響應,響應信息跟 ClientHello 類似,只不過每個欄位都是一個確定的值,是服務端根據客戶端傳過來的候選值的最終選擇結果,如果服務端沒有在候選值中找到合適的,那麼將會返回錯誤提示,需要提一下的是,這次的響應信息中包含第二個隨機數。
- Server Certificate:服務端緊接著向客戶端發送證書;
- Server Key Exchange Message:當上一條證書消息中的信息不全時,服務端會再次發送一些額外數據到客戶端;
- Certificate Request:如果服務端要求客戶端提供證書,會發出這樣一個請求;
- Server Hello Done:這條消息表示服務端這階段數據發送完畢,下面就是等待客戶端的響應;
- Client Certificate:如果服務端要求客戶端提供證書,那麼客戶端會返回自己的證書;
- Client Key Exchange Message:這一步非常關鍵,客戶端會生成 premaster secret(預主密鑰),為什麼叫 premaster secret?因為後面客戶端和服務端會根據 premaster secret 和前面過程中兩個隨機數共同生成一個 master secret(主密鑰,48位元組),後面通信的安全全靠這個 master secret。前兩個隨機數客戶端和服務端都知道了,這個步驟最主要的就是協商一個 premaster secret,這個過程叫做「密鑰交換」,這裡介紹兩個方法:
- RSA 密鑰交換:客戶端生成 46 位元組的隨機數,使用伺服器的公鑰加密,然後發送出去,伺服器便可以用私鑰解密。但這種方式不太安全,所以現在逐漸使用 DH 密鑰交換;
- Diffie-Hellman 密鑰交換:DH 的精髓就是正向計算簡單,反向計算困難,好比兩種顏色的顏料,混在一起你知道什麼顏色,但就給你一種顏色,你幾乎沒法說出其是由哪兩種顏色混合而來。具體生成 premaster secret 的方式可以看Diffie–Hellman key exchange,這裡簡單提一下,密鑰交換需要 6 個參數,其中 2 個叫「域參數」,由伺服器選取,交換過程中客戶端和伺服器各自生成 2 個參數,但是只相互發送 1 個,所以客戶端和伺服器各自知道 5 個參數,根據這 5 個參數,雙方計算得到一個同樣的 premaster secret。
- Certificate Verify:驗證客戶端的私鑰和之前發送的客戶端證書中的公鑰是對應的;
- Finished:客戶端的握手已經完成,消息內容加密,並且包含 verify_data 欄位,值是整個握手過程中所有消息的摘要,供服務端驗證消息完整性;
- Finished:表示服務端握手結束,同時也發送前面過程的消息的摘要。
用圖表示一下就是:
整個握手過程總結一下就是:
- 客戶端提出 HTTPS 連接請求;
- 伺服器表明身份,表示自己是李逵而不是李鬼;
- 客戶端生成一個用於以後通信的密鑰,並把密鑰也告訴了伺服器;
- 客戶端和伺服器結束握手。
以上就是握手的整個通信細節,但細心的同學可能會發現少了一個重要步驟,客戶端收到伺服器發來的證書時是如何判定對方就是自己想要找的伺服器呢?這時候就要驗證證書的有效性,證書就像現實中的身份證,可以確認某個網站的確是我要訪問的網站。那怎麼驗證證書的有效性呢?首先,數字證書和身份證一樣由權威機構簽發,不同的是身份證只能由政府簽發,而數字證書由 CA(Certification Authorities,數字證書認證機構)簽發,Mac 用戶可以通過「文件-應用程序-實用工具-鑰匙串訪問」來查看根 CA,根 CA 可以簽發其他 CA,所以一個網站的簽發者不是根 CA 也沒關係,只要這個 CA 的簽發者是根 CA 也行。了解了 CA,下面看一下證書包含什麼,先看圖:
證書中包含:網站的基本信息、網站的公鑰、CA 的名字等信息(詳細請看 X.509),然後 CA 根據這幾個內容生成摘要(digest),再對摘要用 CA 的私鑰加密,加密後的結果即數字簽名,最後將數字簽名也放入到證書中。那麼當系統收到一個證書後,先用公鑰解密,解得開說明對方是由權威 CA 簽發的,然後再根據證書的信息生成摘要,跟解密出來的摘要對比。
數據傳輸
建立連接之後,客戶端和服務端便可以開始進行數據傳輸。同樣,瀏覽器委託協議棧來幫忙收發消息,協議棧收到消息後不會立即發送出去,而是先放入到緩存區中,因為向協議棧發送的數據長度由瀏覽器控制,如果協議棧一收到數據就發送出去,那麼可能會發送大量小包,導致網路效率降低,所以協議棧一般會等數據量積累到一定程度再發送出去,那這個程度具體是啥樣?
首先,在乙太網中,一個包的MTU(Maximum Transmission Unit,最大傳輸單元)是 1500 位元組,除去 TCP、IP 頭部的 40位元組,MSS(Maximum Segment Size,最大分段大小)就是 1460 位元組,但因為加密需要,頭部可能會增加,相對的 MSS 就會減少。當緩存區內的數據接近 MSS 時再發送,可以避免發送小包。但是如果數據量本來就很小,或者應用程序發送數據的頻率很小,那協議棧就不得不等很長時間,所以協議棧內部還有一個定時器,一定時間之後就會將包發送出去。如果數據較小,那就幾個拼個車,放在一個包里發出去,如果數據很大,就要進行拆分。大概是下面這樣:
本地一切就緒之後,協議棧就會將消息發送出去,這時還沒完,客戶端還要確保伺服器收到了消息。我們一直都說 TCP 是面向連接的協議,因為它可以糾正丟包錯誤、連接失敗提示等等,使得傳輸更加可靠。那具體又是怎麼樣的呢?
首先 TCP 模塊在拆分數據時會先算好每一塊數據相當於從頭開始是第幾個位元組,然後將這個數字寫入到 TCP 頭部的「序號」欄位中,通過這個欄位,接收方就能知道包有沒有丟失,比如一個消息長度為 4380(1460 * 3),那麼這條消息就被拆分到三個數據塊中,三個數據塊的 TCP 頭部的「序號」依次是 0、1460 和 2920,所以接收方先收到一個序號為 0 的包,再收到一個序號為 2920 的包,但是沒收到序號為 1460 的包,說明這個包丟失了,現實中的序號為了安全不會從 0 開始,而是以一個隨機數作為初始值。如果確認沒有遺漏,那麼接收方會將到目前為止收到的數據長度加起來,寫入 TCP 的 「ACK 號」中發送給對方,注意 「ACK 號」與 ACK 標記位不是一回事,前者是數字,後者就是一個比特的標記位,但是 「ACK 號」只有在 ACK 標記位為 1 是才有效。
斷開連接
當數據發送完畢後,一方(可能是客戶端,可能是服務端)就會發起斷開連接過程。這個過程也是大家很熟悉的,即四次揮手。下面以客戶端發起斷開請求為例:
- 瀏覽器調用 Socket 庫關閉連接程序,客戶端的協議棧生成 TCP 頭部,將 FIN 標記位設為 1,告訴伺服器打算斷開連接,後面不會再發送數據,同時套接字也記錄斷開連接操作;
- 伺服器收到 FIN 為 1 的 TCP 頭部時,協議棧將套接字記錄為進入斷開操作狀態,同時向客戶端發送一個 ACK 號,告訴客戶端已經收到消息;
- 伺服器收到斷開連接信息時,可能還有數據沒有傳完,所以等待數據全部傳輸結束後,再發送一條 FIN 為 1 的信息,告訴對方也做了斷開連接的準備,但沒有斷開;
- 一段時間後,客戶端返回確認信號,到此,連接結束。
以上就是輸入 URL 後大概發生的一些事情,但是從面試角度看,仍然還有很多部分沒有涉及。後續還會繼續更新這篇文章,添加一些重要內容,這裡先挖個坑:
- 常見狀態碼解析;
- HTTP 緩存;
- 滑動窗口;
- 握手與揮手過程中的異常處理。
好,坑就挖這麼多,再多怕自己不想填,等填完再繼續挖。
推薦閱讀:
※HTML5的Websocket(理論篇 I)
※談談 HTTP 緩存
※APP精細化HTTP分析(二):響應性能分析與優化
※請正確使用http狀態碼,謝謝!