【基礎】HTTP、TCP/IP 協議的原理及應用

前言

本文將持續記錄筆者在學習過程當中掌握的一些 HTTPTCP/IP 的原理,以及這些網絡通訊技術的一些應用場景,文章會保持更新,至關於對這塊知識的一個總結和概括。有不正確之處歡迎指出,及時改正~javascript

綱要

訪問網頁時發生了什麼

當用戶在瀏覽器地址欄輸入地址,敲下回車鍵,直到看到網頁界面,通常時間不過兩三秒左右。然而在這瞬時間,計算機實際上已經完成了很是複雜的操做。這段過程當中發生的事情,其實有很大一部分就與 HTTP TCP/IP 有關,咱們能夠簡要的歸納一下大概的流程。css

第一步,找服務器 IP

當用戶輸入一個網址並按下回車鍵的時候,瀏覽器獲得了一個域名。而在實際通訊過程當中,瀏覽器須要的是一個 IP 地址。爲了得到 IP 地址,瀏覽器會作以下操做,通常咱們把瀏覽器經過域名查找對應 IP 的行爲叫作 DNS 解析。html

  1. 先找瀏覽器的本地的緩存
  2. 再找電腦硬盤裏的 host 文件,有沒有記錄這個域名和 IP 的映射關係
  3. 實在沒找到,只好經過網絡鏈路去域名供應商那裏查詢

第二步,創建 TCP/IP 鏈接

  1. 瀏覽器獲取到了服務器對應 IP,就會向對應 IP 的服務器發送 TCP 鏈接請求。
  2. 服務器收到請求後迴應,雙方屢次確認後創建起 TCP 雙向鏈接

從客戶端發起鏈接請求一直到 TCP 鏈接創建,這個過程,叫作 三次握手前端

若是請求是 HTTPS 的,還須要在 TCP 鏈接上,再經過 SSLTLS 提供加密處理數據、驗證對方身份以及數據完整性,來保證數據傳輸的安全。java

第三步,請求資源

  1. TCP 鏈接建立完成,瀏覽器開始向服務端發送正式的 HTTP 請求的數據包。
  2. 服務端接受請求,對請求進行解析,通過數據操做後,返回客戶端須要的數據包。

第四步,瀏覽器渲染

瀏覽器獲取到須要的數據之後,對數據進行拼接、解析、執行,最終將完整的網頁繪製在頁面上。webpack

第五步,瀏覽器緩存

瀏覽器拿到服務端返回的數據後,會根據必定的策略進行數據的緩存,這樣在下一次請求一樣數據的時候,就可直接到緩存拿取,再也不請求服務器。nginx

上述流程能夠看做是一個應用在完整網絡通訊過程當中的實踐場景,其中帶出了不少網絡通訊的知識點,下面就以這條線爲索引,對其中涉及到的知識碎片進行闡述和說明。web

經典網絡五層模型

在每臺計算機設備上,都有這麼一套系統鏈路的關係,來保證網絡傳輸的正常進行,由於統一集成了這麼一套經典模型,因此本身使用的計算機也是能夠做爲一臺服務器來提供網絡服務的。面試

應用層:

應用層包含了咱們所說的 HTTP 協議,爲各個應用軟件提供了不少服務,常見的應用層服務有:HTTP 服務 、FTP 服務 、Email 服務等。應用層屏蔽了底層模型的相關細節,做爲應用支持,只提供給使用者一些必要的使用方式。數據庫

傳輸層

常見的傳輸層協議有 TCPUDP ,傳輸層做爲爲應用層的基礎,定義了「端到端(end to end)」之間數據間的傳輸方式,好比:兩臺設備如何創建鏈接?設備之間須要以何種規範進行數據傳輸?須要以什麼方式進行數據的分片、重組、拼接?這些都是傳輸層爲咱們定義好的。

網絡層

一般咱們常說的 IP 協議就位於這一層。網絡層爲數據在結點之間傳輸建立邏輯鏈路,當咱們在瀏覽器敲下域名,瀏覽器在網絡裏如何經過這個域名,找到對應的 IP 映射,這個查詢的邏輯關係和鏈路,是網絡層規範和定義的。

數據鏈路層

數據鏈路層在通訊實體間創建數據鏈路鏈接,物理設備鏈接完成之後,須要相應的軟件和驅動來鏈接和打通這些物理設備,建立電路的鏈接。

物理層

定義物理設備如何傳輸數據,常見的物理層有網線,光纜,網卡,聲卡等,物理層是一切軟件的基礎。

URI、URL 和 URN

對於 URL 咱們基本比較熟悉,然而對 URIURN 的瞭解可能比較少,URIURLURN 是識別、定位和命名互聯網上的資源的標準途徑。

當咱們在瀏覽器地址欄裏輸入域名的那一刻,其實已經和這三個概念牽扯上了聯繫。

URI

  • Uniform Resource Identifier,統一資源標識符,簡稱爲 URI
  • 每一個 Web 服務器都有一個 URI 標識符,它在世界範圍內惟一標識並定位信息資源,一個資源信息有了 URI 標識之後,在互聯網上就能經過一個固定的地址訪問到這個資源。
  • 它具備兩種形式,URN (統一資源名)、URL(統一資源定位符),也就是說 URLURN 是它的子集。

URL

Uniform Resource Locator,統一資源定位符,簡稱 URL,下圖是一個完整的 URL 組成。

一個完整的 URL 從左到右包含以下部分:

  1. schema 標識了這個資源地址所基於的訪問協議,常見的好比:HTTPFTP
  2. user information 標識了用戶信息(若是這個資源須要用戶信息認證的話),不過通常如今的認證都不採用這種方式,一來輸入很是麻煩,二來不安全。
  3. host 標識了資源的域信息,能夠是域名,也能夠是 IP ,這塊的做用主要是找到資源所存放的物理服務器地址。
  4. port 端口號,一個物理服務器,經過開啓不一樣的端口,就同時能夠運行多個 web 服務器,資源文件會部署在某一個 web 服務器的某一個地方,而端口號就是用來定位資源存在的 web 服務器的。
  5. path 路徑,或者叫路由,一個 web 服務器下有許多目錄,通常 path 就是用來定位到資源文件所存放的目錄的。因爲如今不少的 web 應用很是龐大,這個路徑也不必定就是目錄地址,也多是 web 服務器指定的靜態資源文件的請求地址。
  6. query 查詢字符串,通常用於 GET 查詢,傳遞查詢參數。
  7. fragment 片斷,哈希,或者叫錨點,主要用於前端文檔的定位,或者是前端渲染時控制路由跳轉的手段。

這裏須要注意將 URL 與網址區別開來。 URL 不只僅包含了網頁的資源地址,還包含了組成網頁所需的圖片、視頻等超文本資源,以及 css js 等資源地址。 網址本質上是 IP 地址的一個更有辨別度的映射,在經過 DNS 解析以後,瀏覽器最早拿到的是 html 文檔的 URL 地址,根據瀏覽器對 Html 文檔的解析,繼續經過網頁內其餘資源文件的 URL 獲取對應的資源文件。

URN

Uniform Resource Name,統一資源名稱,簡稱 URN,它的用處簡單說就是永久定位資源,由於同一個資源可能會更換存儲位置,存儲位置一旦更換,再訪問原來的 url 確定是拿不到的,URN 就是解決這個問題的,無論資源位置怎麼移動,只要訪問同一個 URN 都能定位到。

TCP/IP 協議族

TCP 鏈接的創建,是網絡通訊成功不可或缺的一步,所以,TCP/IP 也成爲了一個十分重要的知識點。

TCP/IP 協議(傳輸控制協議/互聯網協議)不是簡單的一個協議,而是一組特別的協議,包括:TCP,IP,UDP,ARP等,這些被稱爲子協議。在這些協議中,最重要、最著名的就是 TCPIP。所以咱們習慣將整個協議族稱爲 TCP/IP

  • IP 協議
    • IP 協議使互聯網成爲一個容許鏈接不一樣類型的計算機和不一樣操做系統的網絡。
    • IP 地址是 IP 協議提供的一種統一的地址格式,它爲互聯網上的每個網絡和每一臺主機分配一個邏輯地址,至關於這臺機器的暫用名,別的機器能夠經過這個名字找到它,進而能互相創建起鏈接進行通訊和交流。
  • TCP 協議
    • TCP 協議是面向鏈接的全雙工協議,所以無論是客戶端仍是服務端都能在 TCP 鏈接通道下向對端接收和發送數據。
    • TCP 相比於 UDP 的優點在於它的傳輸穩定性,在數據傳輸以前必須通過三次握手創建鏈接;在數據傳輸過程當中必須保證數據有序完整地傳到對端。
    • TCP 相比於 UDP 的劣勢在於它的複雜度,鏈接創建、斷開都是比較大的性能開銷,並且數據傳輸過程當中一旦卡住,則必須等前面的數據發送完畢之後,後續數據才能繼續傳輸。
    • 每臺服務器可提供支持的 TCP 鏈接數量是有限的,因此這也使得 TCP 鏈接變成了稀缺資源,經不起浪費。
  • UDP 協議
    • UDP 協議是面向無鏈接的,不須要在傳輸數據前先創建鏈接,想發就發想傳就傳。
    • UDP 作的工做只是報文搬運,不負責有序且不丟失地傳遞到對端,所以容易出現丟包的狀況。
    • UDP 不只支持一對一的傳輸方式,還支持一對多、多對多、多對一的方式,也就是說 UPD 提供了單播、多播、廣播的功能。
    • UDP 相比於 TCP 的優點在於它的輕量、高效和靈活,在一些對於實時性應用要求較高的場景下須要使用到 UDP,好比直播、視頻會議、LOL等實時對戰遊戲。
    • UDP 相比於 TCP 的劣勢在於它的不可靠性和不穩定性。

TCP 鏈接

在客戶端發送正式的 HTTP 請求以前,須要先建立一個 TCP 鏈接,在建立的 TCP Connect 通道下,全部的 HTTP 請求和響應才能正常的發送和接受。

在不一樣的 HTTP 協議版本里,這個 TCP 鏈接通道的建立和持續機制也有所不一樣。

  • HTTP1.0 中,每一次 HTTP 請求都會建立一個 TCP 鏈接,在請求發送完成,服務器響應之後,這個 TCP 鏈接就自動斷開了。
  • HTTP1.1 中,能夠經過手動設置 Connection: keep-alive 請求頭來創建 TCP 的持久鏈接,多個 HTTP 請求能夠共用一個 TCP 鏈接。可是 TCP 鏈接存在線頭阻塞,即若干個請求排隊等待發送,一旦有某請求超時等,後續請求只能被阻塞。
  • HTTP2 中,採用了信道複用,使 TCP 鏈接支持併發請求,即多個請求可同時在一個鏈接上並行執行。某個請求任務耗時嚴重,不會影響到其它鏈接的正常執行嗎,這樣一來,大部分請求可使用一個 TCP 鏈接,而不用建立新的 TCP 鏈接通道,既節省了三次握手的開銷,又節約了服務端維護 TCP 端口的成本。

TCP 的三次握手和四次揮手

三次握手

提示:關於 ACKFINSYN 狀態碼的含義

  1. ACK 用於確認,表示通知對方,我已經收到你發來的信息了。
  2. FIN 用於結束,表示告知對方,我這邊已經結束,數據所有發送完畢,沒有後續輸出,請求終止鏈接。
  3. SYN 用於同步和創建鏈接,表示告知對方,我這邊請求同步創建鏈接。
  1. 第一次握手:由客戶端向服務端發送鏈接請求 SYN 報文,該報文段中包含自身的數據通信初始序號,請求發送後,客戶端便進入 SYN-SENT 狀態。
  2. 第二次握手:服務端收到鏈接請求報文段後,若是贊成鏈接,則會發送一個包含了 ACKSYN 報文信息的應答,該應答中也會包含自身的數據通信初始序號(在斷開鏈接的「四次揮手」時,ACKSYN 這兩個報文是做爲兩次應答,獨立開來發送的,所以會有四次揮手),服務端發送完成後便進入 SYN-RECEIVED 狀態。
  3. 第三次握手:當客戶端收到鏈接贊成的應答後,還要向服務端發送一個確認報文。客戶端發完這個報文段後便進入 ESTABLISHED 狀態,服務端收到這個應答後也進入 ESTABLISHED 狀態,此時鏈接創建成功。

面試時可能會問的一個問題就是,明明兩次握手就能肯定的鏈接,爲何須要三次握手? 由於因爲不少不可控制的因素,例如網絡緣由,可能會形成第一次請求隔了好久纔到達服務端,這個時候客戶端已經等待響應等了好久,以前發起的請求已超時,已經被客戶端廢棄掉再也不繼續守着監聽了。 然而服務端過了好久,收到了廢棄的延遲請求,發起迴應的同時又開啓了一個新的 TCP 鏈接端口,在那裏呆等客戶端。 而服務端能維護的 TCP 鏈接是有限的,這種閒置的無用連接會形成服務端的資源浪費。 所以在服務端發送了 SYNACK 響應後,須要收到客戶端接的再次確認,雙方鏈接才能正式創建起來。三次握手就是爲了規避這種因爲網絡延遲而致使服務器額外開銷的問題。

四次揮手

和創建 TCP 鏈接相似,斷開 TCP 鏈接也一樣須要客戶端於服務端的雙向交流,由於整個斷開動做須要雙端共發送 4 個數據包才能完成,因此簡稱爲「四次揮手」。

  1. 第一次揮手:客戶端認爲本身這邊的數據已經所有發送完畢了,因而發送一個 FIN 用來關閉客戶端到服務端的數據傳輸,發送完成之後,客戶端進入 FIN_WAIT_1 狀態。
  2. 第二次揮手:服務端收到客戶端發送回來的 FIN 之後,會告訴應用層要釋放 TCP 連接,而且發送一個 ACK 給客戶端,代表已經收到客戶端的釋放請求了,不會再接受客戶端發來的數據,自此,服務端進入 CLOSE_WAIT 的狀態。
  3. 第三次揮手:服務端若是此時還有未發送完的數據能夠繼續發送,發送完畢後,服務端也會發送一個釋放鏈接的 FIN 請求用來關閉服務端到客戶端的數據傳送,而後服務端進入 LAST_ACK 狀態。
  4. 第四次揮手:客戶端接收到服務端的 FIN 請求後,發送最後一個 ACK 給服務端,接着進入 TIME_WAIT_2 狀態,該狀態會持續 2MSL(最大段生存期,指報文段在網絡中生存的時間,超時會被拋棄) 時間,若該時間段內沒有服務端的重發請求的話,客戶端就進入 CLOSED 狀態.服務端在收到應答消息後,也會進入 CLOSED 狀態,至此完成四次揮手的過程,雙方正式斷開鏈接。

上面的內容可能仍是有些不夠直觀,因此我還準備了一段人話來描述整個過程:

  1. 客戶端:喂,我好了。
  2. 服務端:噢,你好了是吧,我知道了,我還沒好,你等一哈。
  3. 服務端:OK,如今我也好了。
  4. 客戶端:收到,此次玩的很開心,咱們下次再約。

可能有些面試中會問,爲何創建鏈接有三次握手,而斷開鏈接卻有四次? 這是由於在創建鏈接過程當中,服務端在收到客戶但創建鏈接請求的 SYN 報文後,會把 ACKSYN 放在一個報文裏發送給客戶端。 而關閉鏈接時,服務端收到客戶端的 FIN 報文,只是表示客戶端再也不發送數據了,可是還能接收數據,並且這會兒服務端可能還有數據沒有發送完,不能立刻發送 FIN 報文,只能先發送 ACK 報文,先響應客戶端,在確認本身這邊全部數據發送完畢之後,纔會發送 FIN。 因此,在斷開鏈接時,服務器的 ACKFIN 通常都會單獨發送,這就致使了斷開鏈接比請求鏈接多了一次發送操做。

HTTP 定義

一旦端對端成功創建起了 TCP 鏈接,下一步就要開始發送正式的 HTTP 請求了。流淌在 TCP Connect 通道里的 HTTP 只負責傳輸數據包,並無鏈接的概念,所以 HTTP 也被叫作「無狀態協議」。

HTTP 協議是 Hyper Text Transfer Protocol(超文本傳輸協議)的縮寫,它一般運行在 TCP 之上,經過瀏覽器和服務器進行數據交互,進行超文本(文本、圖片、視頻等)傳輸的規定。也就是說,HTTP 協議規定了超文本傳輸所要遵照的規則。

  1. HTTP 協議是無狀態的。這意味着客戶端和服務端之間沒法知曉當前對方的狀態信息,HTTP 請求自己是不帶有任何狀態存儲的。但實際狀況下,客戶端和服務端必然須要狀態的認證和交互,因此就引入了 Cookie, 用於存儲當前瀏覽器的一些狀態信息,每次經過獨立的 HTTP 請求進行收發,從而解決這個問題。
  2. HTTP 請求互相獨立HTTP 互相之間都是一個獨立的個體請求,在客戶端請求網頁時多數狀況下並非一次請求就能成功的,服務端首先是響應 HTML 頁面,而後瀏覽器收到響應以後發現頁面還引用了其餘的資源,例如,CSS,JS文件,圖片等等,還會自動發送 HTTP 請求獲取這些須要的資源。
  3. HTTP 協議基於 TCP 協議HTTP 協議目的是規定客戶端和服務端數據傳輸的格式和數據交互行爲,並不負責數據傳輸的細節,底層是基於 TCP 實現的。如今使用的版本當中是默認持久鏈接的,也就是屢次 HTTP 請求使用一個 TCP 鏈接。

注意:HTTP 請求和 TCP 鏈接是不同的,HTTP 是在 TCP 鏈接創建的基礎上而發起的傳輸請求,在同一個 TCP 鏈接通道下,能夠發送多個 HTTP 請求,舉個例子的話就是高速公路和車子的關係。

HTTP 發展歷史

HTTP 0.9 版本

  • 只有一個 GET 命令。
  • 沒有請求頭和響應頭來描述傳輸相關的數據信息。
  • 服務器發送完數據後,直接關閉 TCP 鏈接,不支持 TCP 持久化鏈接。

HTTP 1.0 版本

  • 增長了不少命令,HEADPOSTPUTDELETE 等。
  • 增設了 status code 狀態碼和 header 請求頭和響應頭。
  • 增長了多字符集支持、多部分發送、權限、緩存等。
  • 可經過開啓 Connection: keep-alive 來指定使用 TCP 長鏈接

HTTP 1.1 (目前廣泛使用)

  • 默認支持持久鏈接
  • 默認支持長鏈接(PersistentConnection),即默認開啓 Connection: keep-alive
  • 支持請求的流水線(Pipelining)處理,即在一個 TCP 鏈接上能夠傳送多個 HTTP 請求和響應。
  • 增長了 host 請求頭字段,經過對 host 解析,就可以容許在同一臺物理服務器上運行多個軟件服務,極大提升了服務器的使用率。目前的 nginx 反向代理就是根據 HTTP 請求頭中的 host 來分辨不一樣的請求,從而將這些請求代理到同一臺服務器不一樣的軟件服務上。

HTTP 2.0

  • HTTP1.x 的解析是基於文本,存在解析上的缺陷;而 HTTP2.0 直接使用二進制的解析方式來替代 HTTP 1.X 的字符串解析,更爲高效和健壯。
  • HTTP2.0 全部數據以「幀」的方式進行傳輸,所以同一個鏈接中發送的多個請求再也不須要按照順序進行返回處理,能夠達到並行的數據傳輸。
  • HTTP2.0 壓縮頭信息進行傳輸數據量的優化。HTTP1.x 的請求頭帶有大量信息,並且每次都要重複發送,HTTP2.0 使用 encoder 來減小須要傳輸的請求頭大小,通信雙方各自緩存一份 header fields 表,既避免了重複的傳輸,又減少了傳輸信息的大小。
  • HTTP2.0 新增了 server push(服務端推送) 的概念,服務端能夠主動發起一些數據推送。好比,服務端在接收到瀏覽器發來的 HTML 請求的同時,能夠主動推送相關的資源文件(js/css)給客戶端,並行發送,提升網頁的傳輸和渲染效率。
  • 目前若是要使用 HTTP2 須要首先使用 HTTPS 在這基礎上,才能使用 HTTP2

HTTP 2.0 相比於 HTTP 1 最直觀的圖片加載性能提高,能夠看 HTTP 2 性能提高的官方演示

HTTPS

咱們常常會在有些網頁上看到懸浮的彈窗或者廣告,有的時候甚至會在本身編寫的上線網頁上也看到這些垃圾廣告,然而開發者明明沒有寫過這些東西,但是這種垃圾信息是怎麼上去的呢? 究其根本緣由就在於各類代理服務,當咱們從客戶端發起一個 HTTP 請求,並非直接就能傳遞到目標服務器的,期間會通過層層的代理服務,咱們經常使用的 nginx ,以及在 DNS 解析過程當中要通過的寬帶運營商,都是一種代理服務。 因爲 HTTP 時使用明文字符串來傳遞數據的,那麼這些數據就能很輕易地被中間服務讀取甚至篡改,那麼中間服務拿到了原始的 HTML 數據,想插入點小廣告進去天然不是難事。

HTTPS 是爲了解決 HTTP 明文傳輸而出現的安全問題而出現的一種解決機制 ———— 對 HTTP 請求中的信息進行加密以後傳輸,從而有效地防止了中間代理服務截獲或篡改信息的問題。

HTTPS 其實就是一個安全增強版的 HTTP 1.1 ,有幾點須要注意的是:

  1. HTTPS 協議須要到 CA 申請證書,通常免費證書不多,須要交費
  2. HTTP 協議運行在 TCP 之上,全部傳輸的內容都是明文,HTTPS 運行在 SSL/TLS 之上,SSL/TLS 運行在 TCP 之上,全部傳輸的內容都通過加密的。
  3. HTTPHTTPS 使用的是徹底不一樣的鏈接方式,用的端口也不同,前者是 80,後者是 443
  4. HTTPS 能夠有效的防止運營商劫持,解決了防劫持的一個大問題。

HTTP 的報文組成

HTTP 是以請求和響應的形式存在的,因爲發起方主動發起一個 HTTP 請求,而後由響應方迴應,雙方按照必定的報文格式進行數據的互傳,一個完整的 HTTP 報文一般由 首行首部主體 構成。

首行

首行並不屬於 Http Headers ,它包含了:

  1. HTTP MethodGETPOSTPUTDELETE 等 ),不一樣的 HTTP Method 有不一樣的語意。

    HTTP Method 對應予以
    GET 通常用於獲取服務器資源
    POST 通常用於傳輸實體主體
    PUT 通常用於傳輸文件
    DELETE 用於刪除文件
    HEAD 用於獲取報文首部,不返回報文主體
    OPTIONS 用於預檢請求中,詢問請求URI資源支持的方法

    HTTP Method 只是 HTTP 協議推崇的一種規範,就像 ESLint,你能夠選擇遵循,也能夠選擇不遵循,它們所做的事情實質上沒有差異,只是語義化更明確。

  2. URL請求資源的地址,這個地址只會包含請求的路由地址。

  3. 協議的版本HTTP 1.0 / HTTP 1.1 / HTTP 2

  4. HTTP 返回狀態碼(響應報文首行包含)

    HTTP 定義了40個標準狀態代碼,可用於傳遞客戶端請求的結果,狀態代碼分爲如下五類,關於各個分段下的返回狀態碼信息能夠參考 HTTP 響應碼

這邊須要注意的一點是,一個好的 HTTP 應用服務應該是有完善的 HTTP status code 的返回信息的,即訪問者單從 HTTP status > code 上就能得知當前 HTTP 請求的狀態信息。 而目前咱們大部分的開發模式下的 HTTP 返回碼,只有 200500。服務端的同窗會先把 200 返回過來,而後再告訴你出了什麼 「沒登陸」 / 「沒認證」 / 「沒權限」 這一類的問題。 業界也有一句戲言:又不是不能用,其實這種開發方式是不正確的,無論從代碼的維護性仍是我的自身發展角度,咱們都須要> 儘可能避免這種問題。

HTTP 頭信息

HTTP 頭信息,即 HTTP Header,首行換行後的信息都是 HTTP HeaderHTTP header 裏通常存放了客戶端和服務端之間交互的非業務信息,例如:本次請求的數據類型、請求日期、字符集支持、自定義頭部字段、一些通訊憑證和緩存支持等。 HTTP Header 完整字段列表:傳送門

主體

主體,即 HTTP bodyHTTP Header 信息和主體信息間以一個空行 + 一個換行來區分。HTTP body 裏通常會放置請求的一些具體業務信息

HTTP 數據協商

在 HTTP 協議中,數據協商是這樣一種機制,客戶端經過請求頭告知服務端本次請求但願獲取的數據格式、展示形式、以及數據的壓縮方式等。常見的數據協商例如,文檔使用的天然語言,圖片的格式,或者內容編碼形式。 服務端能夠對請求頭中攜帶的數據協商字段進行解析,而後在返回客戶端數據的時候,也會用相對字段來通知客戶端:本次返回的數據格式、壓縮方式等信息。這樣瀏覽器就可使用特定的解析方式,來對這些資源進行解析、處理和渲染。

下面簡單列舉一些經常使用的數據協商字段,完整的數據協商信息傳送門

  • Accept 請求頭字段,指按期望得到的數據類型
  • Accept-Encoding 請求頭字段,指按期望得到的數據須要以什麼樣的編碼方式進行傳輸,經常使用於限制服務端對數據的壓縮方式,常見的 JS 文件包大小優化的 GZIP 壓縮,就使用了這個方法
  • Accept-Language 請求頭字段,指按期望得到的數據語言類型:中文、英語、仍是其餘語言,這個頭信息字段,通常是瀏覽器自動加上的
  • User-Agent 請求頭字段,指定本次請求的瀏覽器信息,服務端可根據此信息選擇不一樣兼容性的頁面返回給用戶,或者是作用戶使用瀏覽器信息、操做系統等數據的統計
  • Content-Type 響應頭字段,請求頭裏的 Accept 字段可能會指定好幾種能夠接受的數據格式,服務端最終會返回一種數據格式給客戶端
  • Content-Encoding 響應頭字段,對應 Accept-Encoding
  • Content-Language 響應頭字段,對應 Accept-Language

HTTP 長鏈接

每個 HTTP 請求都須要在 TCP 鏈接通道里才能完成發送和接受。在 HTTP 協議的早期版本里,每一條 HTTP 請求發送以前,都會建立一條新的 TCP 鏈接通道,在這個請求完成之後,該條 TCP 通道就會自動關閉。 這樣帶來的問題就是,單條 TCP 鏈接沒有辦法複用,形成很大的新能浪費。好在這一問題隨着 HTTP 協議的逐步完善已經獲得解決。

HTTP 1.0 中引入的 Connection 頭字段,容許對其設置 Keep-Alive 或者是 Close 來決定是否須要複用 TCP 鏈接,仍是說在一次請求完成以後直接關閉。而在 HTTP 1.1 中默認雙端都會默認開啓這個字段,即默認支持 HTTP 的長鏈接。

須要注意的是:Connection: Keep-Alive 須要雙端同時開啓才能啓動 HTTP 長鏈接,若是任何一段手動設置 ConnectionClose,長鏈接都沒法位置,由於 TCP 鏈接的創建和持久保持是一個雙端交互的過程。

那麼咱們在本地如何看到 TCP 的鏈接 ID 呢,能夠打開 Chrome 的調試工具來查看:

圖上能夠看到有不一樣的 Connection ID,這就表明着本次請求其實是開啓了一個新的 TCP 鏈接,最下面的請求的 Connection ID 都是相同的,表明着多個 HTTP 請求複用了同一個 TCP 鏈接。

Chrome 瀏覽器所可以支持的最大併發 TCP 鏈接數是 6個,而且在 HTTP 2.0 如下的 HTTP 版本中,請求是阻塞的。也就是說,一旦六個鏈接開滿,前面的請求未完成,那麼後續請求就會被阻塞,直到前面的請求返回,後續才能繼續發送。

HTTP 緩存

雖然 HTTP 緩存不是必須的,但重用緩存的資源一般是必要的。然而常見的 HTTP 緩存只能存儲 GET 響應,對於其餘類型的響應則無能爲力。緩存的關鍵主要包括 request method 和目標 URI(通常只有 GET 請求才會被緩存)。

緩存讀取策略

前端環境下的文件緩存,分爲幾個不一樣的位置。當咱們打開 Chrome 控制檯,查看 Network 下每條請求記錄的 size 選項,會發現很是豐富的來源信息。

對於前端瀏覽器環境來講,緩存讀取位置是由前後順序的,順序分別是(由上到下尋找,找到即返回;找不到則繼續)

  • Service Worker
  • Memory Cache
  • Disk Cache
  • 網絡請求

Service Worker

Service Worker 的緩存與瀏覽器其餘內建的緩存機制不一樣,它可讓咱們自由控制緩存哪些文件、如何匹配緩存、如何讀取緩存,而且緩存是持續性的。

  • 瀏覽器優先查找。
  • 持久存儲。
  • 能夠更加靈活地控制存儲的內容,能夠選擇緩存哪些文件、定義緩存文件的路由匹配規則等。
  • 能夠從 Chrome 的 F12 中,Application -> Cache Storage 查看。

Memory Cache

  • memory cache 是內存中的緩存存儲。
  • 讀取速度快。
  • 存儲空間較小。
  • 存儲時間短,當瀏覽器的 tab 頁被關閉,內存資源即被釋放。
  • 若是明確指定了 Cache-Controlno-store,瀏覽器則不會使用 memory-cache

Disk Cache

  • Disk Cache 是硬盤中的緩存存儲。
  • 讀取速度慢於 Memory Cache ,快於網絡請求。
  • 存儲空間較大。
  • 持久存儲。
  • Disk Cache 嚴格依照 HTTP 頭信息中的字段來判斷資源是否可緩存、是否要認證等。
  • 常常聽到的「強制緩存」,「對比緩存」,以及 Cache-Control 等,歸於此類。

網絡請求

若是一個請求的資源文件均未命中上述緩存策略,那麼就會發起網絡請求。瀏覽器拿到資源後,會把這個新資源加入緩存。

Cache-Control

HTTP/1.1定義的 Cache-Control 頭用來區分對緩存機制的支持狀況, 請求頭和響應頭都支持這個屬性。經過它提供的不一樣的值來定義緩存策略。須要注意的是,數據變化頻率很快的場景並不適合開啓 Cache-Control

指令 做用
public 公共緩存:表示該響應能夠被任何中間人(好比中間代理、CDN等)緩存。
private 私有緩存:表示該響應是專用於某單個用戶的,中間人不能緩存此響應,該響應只能應用於瀏覽器私有緩存中。
max-age (單位/秒)設置緩存的過時時間,過時須要從新請求,不然就讀取本地緩存,並不實際發送請求
s-maxage (單位/秒)覆蓋 max-age,做用同樣,只在代理服務器中生效
max-stale (單位/秒)表示即便緩存過時,也使用這個過時緩存
no-store 禁止進行緩存
no-transform 不得對資源進行轉換或壓縮等操做,Content-Encoding、Content-Range、Content-Type 等 HTTP 頭不能由代理修改(有時候資源比較大的狀況下,代理服務器可能會自行作壓縮處理,這個指令就是爲了防止這種狀況)。
no-cache 強制確認緩存:即每次使用本地緩存以前,須要請求服務器,查看緩存是否失效,若未過時(注:實際就是返回304),則緩存才使用本地緩存副本。
must-revalidate 緩存驗證確認:意味着緩存在考慮使用一個陳舊的資源時,必須先驗證它的狀態,已過時的緩存將不被使用
proxy-revalidate 與 must-revalidate 做用相同,但它僅適用於共享緩存(例如代理),並被私有緩存忽略。

緩存校驗

在瀏覽器使用緩存的過程當中,爲了配合 Cache-Control 中 no-cache ,咱們還須要一個機制來驗證緩存是否有效。好比服務器的資源更新了,客戶端須要及時刷新緩存;又或者客戶端的資源過了有效期,但服務器上的資源仍是舊的,此時並不須要從新發送。 緩存校驗就是用來解決這些問題的,在http 1.1 中,咱們主要關注下 Last-ModifiedETag 這兩個字段。

Last-Modified

顧名思義,就是資源的最新一次修改時間。當客戶端訪問服務端的資源,服務端會將這個 Last-Modified 值返回給客戶端,客戶端收到以後,下次發送請求就會將服務端返回回來的 Last-Modified 值裝在 If-Modified-Since 或者 If-Unmodified-Since 裏,發送給服務端進行緩存校驗。

這樣服務器就能夠經過讀取 If-Modified-Since (較經常使用)或 If-UnModified-Since 的值,和本地的 Last-Modified 值作對比校驗。若是校驗發現這兩個值是同樣的,就表明本次請求的資源文件沒有被修改過,那麼服務器就會告訴瀏覽器,資源有效,能夠繼續使用,不然就須要使用最新的資源。

來看一下下面的兩張圖:

當請求服務端的 script.js 的腳本資源時,能夠看到服務端返回了 Last-Modified,裏面記錄了該資源最後一次的修改時間

當客戶端下次再次發起請求,會攜帶上這個過時時間給服務端進行驗證

來看下服務端的部分代碼:

const http = require('http');
const fs = require('fs');
http.createServer((request, response) => {
   const ifModifiedSince = request.headers['If-Modified-Since'];
   const lastModified = 'Web Aug 19 2019 19:01:15 GMT+0800 (China Standard Time)';
    
   if (request.url === '/') {
       const html = fs.readFileSync('test.html', 'utf-8');
       
       response.writeHead(200, {
           'Content-Type': 'text/html'
       });
       response.end(html);
   }
   if (request.url === '/script.js') {
       const js = fs.readFileSync('script.js', 'utf-8');
       let status = 200;
       // 若是讀取到的 If-Modified-Since 和 lastModified 相同,則設置頭部 304 表示可以使用緩存
       if (ifModifiedSince === lastModified) {
           status = 304;
           response.end('');
       }
       response.writeHead(status, {
           'Content-Type': 'text/javascript',
           'Cache-Control': 'no-cache,max-age=2000',
           'Last-Modified': lastModified
       });
       response.end(js);
   }
});

複製代碼

ETag

Etag 的做用本質上和 Last-Modified 差異不大。相比於 Last-Modified 使用最後修改日期來比較資源是否失效的緩存校驗策略,ETag 則是經過數據簽名來作一個更加嚴格的緩存驗證。

所謂數據簽名,其實就是經過對資源內容進行一個惟一的簽名標記,一旦資源內容改變,那麼簽名必將改變,服務端就以此簽名做爲暗號,來標記緩存的有效性。典型的作法是針對資源內容進行一個 hash 計算,相似於 webpack 打包線上資源所加的 hash 標識

Last-Modified 對應 If-Modified-Since 相同,ETag 也會對應 If-Match 或者 If-None-MatchIf-None-Match 比較經常使用),若是先後的簽名相同,則不須要返回新的資源內容。

緩存校驗的合理使用

Last-ModifiedETag 只是給服務端提供了一個控制緩存有效期的手段,並無任何強制緩存的做用,最終決定是否使用緩存、仍是使用新的資源文件,仍是須要靠服務端指定對應的 http code 來決定。 對於保存在服務器上的文件,都有最後修改日期的屬性,當使用 Last-Modified 能夠利用這個有效的屬性進行數據緩存驗證;或者在數據庫存入一個 updatetime 字段來標識具體的修改日期,從而判斷緩存是否有效。 具體如何構建一個可以合理使用緩存的服務器,就比較涉及後端知識了,這裏不作具體描述。

瀏覽器的同源策略

瀏覽器的同源限制:當瀏覽器訪問 URL 地址的協議(schema)/ 端口(port)/ 域名(host),三者中有任何一個與當前的 URL 片斷信息不匹配的時候,便存在跨域問題。

當前地址 請求地址 請求是否成功
www.juejin.com:80 www.juejin.com:80 跨域(協議不一樣)
www.juejin.com:80 www.juejin.cn:80 跨域(域名不一樣)
www.juejin.com:80 www.juejin.com:90 跨域(端口不一樣)

對於跨域的幾點須要明確:

  1. 跨域,是瀏覽器提供的一種保護手段,服務端是不存在跨域這一說的。這也就是爲何如今先後端分離的開發模式下,前端比較依賴 webpack-dev-server 啓動代理服務來中轉和代理後臺接口的緣由,由於兩個服務器之間相互通訊是沒有跨域障礙的。
  2. 跨域,是對於 XMLHttpRequest 來講的,瀏覽器獲取不一樣源服務器下的靜態資源,是沒有跨域限制的,這也是 JSONP 跨域請求得以實現的本質。
  3. 不一樣於 XMLHttpRequest 的是,經過 src 屬性加載的腳本資源,瀏覽器限制了 Javascript 的權限,使其不能讀寫、返回內容
  4. 對於瀏覽器來講,除了 DOM 、Cookie、XMLHttpRequest 會收到同源策略限制之外,一些常見的插件,好比 Flash、Java Applet 、Silverlight、Google Gears 等也都有本身的控制策略。

當瀏覽器向不一樣域的服務器發送請求時,請求是真能發出去,對方服務端也是真能接收到請求,而且真能給你的瀏覽器響應,瀏覽器也真能接收到有效數據。 可是,若是在跨域的狀況下、服務端返回數據的響應頭裏的 Access-Control-Allow-Origin 字段,沒有把當前域名列進白名單,那麼瀏覽器會把服務端返回的數據給藏起來,不告訴你,而後給你拋個 Access-Control-Allow-Origin 的錯誤。

至於爲何資源文件不受同源策略限制呢?能夠試想一下,若是資源文件也被限制跨域,那麼如今大量使用的 CDN 緩存策略基本就沒辦法用了。並且如今不少網站的資源文件,都會放到雲服務器的 OSS 上,OSS 資源對應的 url 地址確定是不一樣域的,那這些資源也不能使用了。

Access-Control-Allow-Origin

Access-Control-Allow-Origin 標識了服務器容許的跨域白名單,它有如下幾種設置方法:

  1. 直接設置 * 通配符,簡單粗暴,可是這麼作等於把服務器的全部接口資源對外徹底暴露,是不安全的。
  2. 設置制定域,好比 Access-Control-Allow-Origin: https://www.baidu.com ,這樣只會容許指定域的請求進行跨域訪問。
  3. 由後端動態設置。Access-Control-Allow-Origin 限制只能寫一個白名單,可是當咱們有多個域都須要跨域請求怎麼呢?這個時候,這時能夠由服務端本身維護一套白名單列表,在請求進來的時候對請求的源 host 進行白名單比對,若是在白名單中,就將這個 Access-Control-Allow-Origin 動態設置上去,而後返回響應。

CORS 的預請求

若是咱們像上面同樣,只設置的 Access-Control-Allow-Origin 白名單,是否就能夠徹底暢通無阻地進行跨域了呢?並非。 就算對端開啓了域名白名單認證,然鵝有一些操做仍然是須要進一步認證的,這種進一步的認證操做,就是 CORS 預請求

預請求觸發過程

瀏覽器預請求的觸發條件,是判斷本次請求是否屬於一個簡單請求。 若是本次請求屬於一個複雜請求,那麼在發送正式的跨域請求以前,瀏覽器會先準備一個名爲 OPTIONSHTTP Method ,做爲預請求發送。 在服務器經過預請求後,下面瀏覽器纔會發生正式的數據請求。整個請求過程實際上是發生了兩次請求:一個預檢請求,以及後續的實際數據請求。

簡單請求

  1. 請求方式只能是 GET POST HEAD
  2. 請求頭字段只容許:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
  3. Content-Type 的值僅限於:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  4. XMLHttpRequestUpload 對象均沒有註冊任何事件監聽器(瞭解就好)。
  5. 請求中沒有使用 ReadableStream 對象(瞭解就好)。

複雜請求

除了簡單請求裏定義的,都是複雜請求,通通須要預請求。

預請求的驗證

那麼怎樣使預檢請求成功認證呢?仍是須要服務端繼續幫忙設置請求頭的白名單:

  1. Access-Control-Allow-Headers,設置容許的額外請求頭字段。
  2. Access-Control-Allow-Methods,設置容許的額外請求方法。
  3. Access-Control-Max-Age (單位/秒),指定了預請求的結果可以被緩存多久,在這個時間範圍內,再次發送跨域請求不會被預檢。

更多、更具體的跨域限制策略能夠點擊這裏查看更多

HTTP 性能優化方案

  1. 合理使用 HTTP 的緩存策略,避免同一資源屢次請求服務端而致使的額外性能開銷
  2. 儘可能使用 HTTP 長鏈接,避免每次重建 TCP 鏈接帶來的時間損耗
  3. 儘可能使用 HTTPS 來保證網絡傳輸的安全性。
  4. 可使用 HTTP2 來大幅提升數據傳輸的效率,使用 server push 開啓 HTTP2 的服務端推送功能
  5. 客戶端開啓 Accept-Encoding 壓縮方式的支持,服務端傳輸壓縮後的文件,減小傳輸數據的大小
相關文章
相關標籤/搜索