51 張圖助你徹底掌握 HTTP!

ErnestEvan發表於2021-01-01

前言

如果說 TCP/IP 協議是網際網路通訊的根基,那麼 HTTP 就是其中當之無愧的王者,小到日常生活中的遊戲,新聞,大到雙十一秒殺等都能看到它的身影,據 NetCraft 統計,目前全球至少有 16 億個網站、2 億多個獨立域名,而這個龐大網路世界的底層運轉機制就是 HTTP,可以毫不誇張的說,無 HTTP 不通訊!

畫外音: TCP/IP 協議群如下, IP 不是 IP 地址,是 Internet Protocol 的簡稱

HTTP 應用如此廣泛,我們確實必要好好學習下它,不僅有助於我們理解和解釋工作中的強制重新整理,防盜鏈等現象和原理,還讓我們在設計開源中介軟體時會有所啟發,比如在設計 MQ, Dubbo 這些元件時,第一要務就是要設計協議,在其中你或多或少能看到 HTTP 協議的影子,學習了 HTTP 能讓你在設計中介軟體等元件協議時,提供很好的思路

本文將會全面剖析 HTTP 的設計理念,助你徹底掌握 HTTP,相信看完以下問題你會手到擒來!

  1. 什麼是 HTTP,它有什麼特點,為什麼說 HTTP 是萬能的
  2. 為什麼說反爬是個偽命題
  3. 簡要介紹一下 HTTP 0.9, 1.1, 2.0, 3.0 的特點
  4. 曾經在我司有一位同事在大群裡拋了一個問題:在瀏覽器的位址列輸入圖片 url,想預覽一下圖片,結果卻變成了下載圖片,你能替他解釋一下其中的原因嗎
  5. DNS 協議瞭解多少,什麼是 DNS 負載均衡
  6. HTTP 1.1 唯一一個要求請求頭必傳的欄位是哪個,它有什麼作用
  7. no-cache 真的是不快取的意思?你對 HTTP 的快取瞭解多少
  8. 為什麼重新整理了瀏覽器器卻抓不到請求,而強制重新整理卻可以抓到,強制重新整理到底做了什麼事
  9. 301 和 302 的區別是啥
  10. 各種協議與 HTTP 的關係
  11. 老生常談很多人的誤解:GET 和 POST 的區別是啥

本文將會從以下幾點來展開闡述 HTTP

什麼是 HTTP

HTTP 全稱 HyperText Transfer Protocol「超文字傳輸協議」,拆成三個部分來看,即「超文字」,「傳輸」,「協議」

超文字:即「超越了普通文字的文字」,即音視訊,圖片,檔案的混合體,大家常見的網頁很多就內嵌了 img, video 這些標籤解析展現而成的圖片,視訊等,除了這些超文字內容外,最關鍵的是超文字中含有超連結,超連結意味著網頁等檔案內容的超文字上可以點選連結到其他頁面上,網際網路就是通過這樣的超連結構成的。 超連結

傳輸: 傳輸意味著至少有兩個參與者,比如 A,B,這意味著 HTTP 協議是個雙向協議,一般是將「超文字」按照約定的協議以二進位制資料包的形式從 A 傳到 B 或 B 傳到 A, A <===> B,我們把發起請求的叫請求方,接到請求後返回資料的那一方稱為應答方,但需要注意的是傳輸也不限於兩個參與者,允許中間有中轉或者接力,只要參與者間遵循約定的協議即可傳輸。 如圖示:傳輸可以有多個參與者,只要遵循相應的協議即可

協議:HTTP 是一個協議,啥是協議?在日常生活中協議並不少見,比如我們租房時簽訂的租房協議,入職後和企業簽訂的勞動合同協議,「協」意味著至少有兩人蔘與,「議」意味著雙方要就某項條款達成一致,比如租房協議規定月付 xx 元,勞動合同協議規定月工資 xx 元,協議即對通訊雙方的約束,雙方按照約定傳輸資料才能進行明白對方的意思,否則便是雞同鴨講。

經過以上解釋,我們可以給 HTTP 下一個比較準確的定義了:

HTTP 是一個在計算機世界裡專門在兩點之間傳輸文字、圖片、音訊、視訊等超文字資料的約定和規範。

與 HTTP 相關的各種協議

HTTP 雖然在當今網際網路通訊中佔據統計地位,但要讓它生效還必須依賴其他協議或規範的支援

1. URI 和 URL

首先既然我們要在兩點之間傳輸超文字,那這個超文字該怎麼表示?超文字即資源,網際網路上資源這麼多,如何唯一標記網際網路上的資源。

使用 URI(Uniform Resource Identifier) 即統一資源識別符號就可以唯一定位網際網路上的資源。URI 大家比較少聽過,大家更熟悉的可能是 URL(Uniform Resource Locator),即統一資源定位符,URL 其實是 URI 的一種子集,區別就是URI 定義資源,而 URL 不單定義這個資源,還定義瞭如何找到這個資源。

URL 主要由四個部分組成:協議、主機、埠、路徑

協議:即通訊雙方指定的傳輸協議,應用最廣的當然是本文要介紹的 http 協議,除此之外還有 ftp, mailto,file 等協議。

主機名:存放資源的伺服器主機名或 IP 地址,當前有時候伺服器由於安全原因還需要對使用者進行認證,需要提供使用者名稱和密碼,此時需要在 hostname前加 username:password。

:整數,可選,省略時使用方案的預設埠,各種傳輸協議都有預設的埠,如 HTTP 預設用的 80 埠,HTTPS 用的 443 埠,傳輸層協議正是利用這些埠號識別本機中正在進行通訊的應用程式,並準確地將資料傳輸。

路徑:即資源在主機上的存放路徑,一般表示主機的目錄或檔案地址

parameter:用於指定特殊引數的可選項。

query:查詢字串,可選,用於給動態網頁或介面傳遞引數,可有多個引數,用“&”符號隔開,每個引數的名和值用“=”符號隔開。

fragment:瀏覽器專用,用於指定網路資源中的片斷,指定後開啟網頁可直接定位到 fragment 對應的位置。

例子如下

2. TCP/IP 協議

在上述 URL 地址中,如果你指定了「www.example.com 」 這樣的主機名,最終會被 DNS 解析成 IP 地址然後才開始通訊,為啥主機名最終要被解析成 IP 地址才能通訊呢,因為 HTTP 協議使用的是 TCP/IP 協議棧,協議棧就是這樣規定的,一起來看看 TCP/IP 協議棧各層的功能。

TCP/IP協議棧
TCP/IP協議棧

TCP/IP 協議棧總共有四層

  1. link layer: 連結層,負責在乙太網,WIFI 這樣的底層網路上傳送原始資料包,工作在網路卡這一層,使用 MAC 地址來標記網路上的裝置,所以也叫 MAC 層
  2. Internet layer: 網路層,IP 協議即處於這一層,提供路由和定址的功能,使兩終端系統能夠互連且決定最佳路徑,並具有一定的擁塞控制和流量控制的能力。相當於傳送郵件時需要地址一般重要
  3. transport layer: 傳輸層,該層的協議為應用程式提供端到端的通訊服務,這層主要有 TCP,UDP 兩個協議,TCP 提供面向連線的資料流支援、可靠性、流量控制、多路複用等服務,UDP不提供複雜的控制機制,利用 IP 提供面向無連線的簡單訊息傳輸
  4. application layer: 即應用層,前面三層已經為網路通訊打下了堅實的基礎,這層可發揮的空間就大很多了,應用層協議可以想象為不同的服務,每個應用層協議都是為了解決某一類應用問題而生的,每一個服務需要用不同的協議,規定應用程式在通訊時所遵循的協議。

我們可以把前面三層認為是高速公路的基礎設施,至於要傳什麼貨物,高速公路是否要關閉等則由應用層決定。

利用 TCP/IP 協議族進行網路通訊時,會通過分層順序與對方進行通訊。每個分層中,都會對所傳送的資料附加一個首部,在這個首部中包含了該層必要的資訊,如傳送的目標地址以及協議相關資訊。

接收方收到資料後,同樣的,每一層也會解析其首部欄位,直到應用層收到相應的資料。

圖片來自文末參考連結
圖片來自文末參考連結

通過這樣分層的方式,每個層各司其職,只要管好自己的工作即可,可擴充套件性很好,比如對於 HTTP 來說,它底層可以用 TCP,也可以用 UDP 來傳輸,哪天如果再出現了更牛逼的協議,也可以替換之,不影響上下層,這就是計算機中比較有名的分層理論:沒有什麼是分層解決不了的,如果有那就再分一層。

IP 包的首部中定義了32 位的源 IP 地址和目的 IP 地址,如下圖所示址

所以應用層在請求傳輸資料時必須事先要知道對方的 IP 地址,然後才能開始傳輸。

2. DNS 協議

由上一節可知請求時需要事先知道對方的 IP 地址,但 IP 地址是由「161.117.232.65」這樣的數字組成的,正常人根本記不住,想想看,如果我要上個百度,還要先知道它的 IP 地址,那豈不是要瘋掉,那怎麼辦,聯絡生活場景,想想看,如果我們要打某人電話,記不住他的電話號碼,是不是要先到電話本找某個人的名字,然後再打,電話本起的作用就是把姓名翻譯成電話號碼。

同樣的,正常人只會記住 baidu.com 這樣的網址,那就需要有類似電話本這樣的翻譯器把網址轉成 IP 地址,DNS(域名伺服器)就是幹這個事的

上面只是 DNS 的簡化版本,實際上 DNS 解析無法一步到位,比較複雜,要理解 DNS 的工作機制,首先我們要看懂域名的層級結構,類似 www.baidu.com 這樣的網址也叫域名,是一個有層次的結構, 最右邊的被稱為頂級域名,然後是二級域名,層級關係向左降低,最左邊的是主機名,通常用來表示主機的用途,比如 「www」 表示提供全球資訊網服務

當然這也不是絕對的,起名的關鍵只是方便我們記憶而已。

域名有層次之分,DNS 也是有層次之分,DNS 核心系統是一個三層的樹狀,分散式結構,基本對應域名結構。

  1. 根域名伺服器(Root DNS Server): 返回「com」,「cn」,「net」等頂級域名伺服器的 IP 地址,
  2. 頂級域名伺服器(Top-level DNS Server):管理各自域名下的權威域名伺服器,比如 com 頂級域名伺服器可以返回 apple.com 域名伺服器的 IP 地址;
  3. 權威域名伺服器(Authoritative DNS Server):管理自己域名下主機的 IP 地址,比如 apple.com 權威域名伺服器可以返回 www.apple.com 的 IP 地址。
DNS 核心系統層級結構
DNS 核心系統層級結構

根域名伺服器是關鍵,必須是眾所周知的,找到了它,下面的各級域名伺服器才能找到,否則域名解析就無從談起了。既然知道了 DNS 的層次之分,那麼不難猜出請求 www.apple.com 的 DNS 解析如下

  1. 首先訪問根域名伺服器,獲取「com」頂級域名伺服器的地址
  2. 請求「com」頂級域名伺服器,返回「apple.com」域名伺服器的地址
  3. 然後返回「apple.com」域名伺服器,返回 www.apple.com 的地址

以上三層解析我們稱為 DNS 核心解析系統,那麼大家想想,全世界的 PC,app 等裝置多如牛毛,如果每發一次請求都要按上面的 DNS 解析來獲取 IP,那估計 DNS 解析系統就要炸了,如何緩解這種壓力呢,答案是用快取,事實上很多大公司,或網路運營商都會自建自己的 DNS 伺服器,作為使用者查詢的代理,代替使用者請求核心 DNS 系統,這樣如果查到的話可以快取查詢記錄,再次收到請求的號如果有快取結果或者快取未過期,則直接返回原來的快取結果,大家可能聽過 Google 的 8.8.8.8 DNS 解析伺服器,這種就是 Google 自建的,我們一般稱這種自建的為「非權威域名伺服器」。

配置 DNS Server
配置 DNS Server

除了非權威域名伺服器,還有瀏覽器快取,作業系統快取(大家熟知的 /etc/hosts 就是作業系統 DSN 快取的一種)

這樣的話如果請求 www.example.com,dns 的完整解析流程如下:

1、 瀏覽器中輸入 www.example.com 後,會先檢視 瀏覽器的 DNS 快取是否過期,未過期直接取快取的,已過期會繼續請求作業系統的快取(/etc/hosts 檔案等),還未找到,進入步驟 2

2、 請求本地地址配置的 DNS resolver(非權威域名伺服器),一般由使用者的 Internet 服務提供商 (ISP) 進行管理,例如有線 Internet 服務提供商、DSL 寬頻提供商或公司網,MAC 的同學可以開啟網路配置中的 DNS Servers 來看下預設 ISP 提供的域名伺服器(如果想用其他的非權威域名伺服器,填入即可,這樣就會覆蓋 ISP 提供的預設地址)

3、 DNS resolver 將 www.example.com 的請求轉發到 DNS 根名稱伺服器, 根伺服器返回「.com」頂級域名伺服器地址

4、 DNS resolver 再次轉發 www.example.com 的請求,這次轉發到步驟三獲取到的 .com 域的一個 TLD 名稱伺服器。.com 域的名稱伺服器使用與 example.com 域相關的四個 Amazon Route 53 名稱伺服器的名稱來響應該請求。

5、Amazon Route 53 名稱伺服器在 example.com 託管區域中查詢 www.example.com 記錄,獲得相關值,例如,Web 伺服器的 IP 地址 (192.0.2.44),並將 IP 地址返回至 DNS 解析程式。

6、DNS resolver 最終獲得使用者需要的 IP 地址。解析程式將此值返回至 Web 瀏覽器。DNS 解析程式還會將 example.com 的 IP 地址快取 (儲存) 您指定的時長,以便它能夠在下次有人瀏覽 example.com 時更快地作出響應。

我們可以用 dig 工具來驗證一下上面的請求流程

可以看到請求流程確實與我們的流程圖一致!另外我們注意到 ip 地址返回了四個,這樣的話 client 可以隨機選擇其中一個請求,這就是我們常說的 DNS 負載均衡,可有效快取 server 壓力。

HTTP 報文格式

接下來我們再介紹下 HTTP 報文格式,通訊雙方要能正常通訊,就必須遵循協議才能理解對方的資訊,協議規定了 HTTP 請求和響應報文的格式。

請求和響應報文都由「起始行」,「頭部」,「空行」,「實體」 四個部分組成,只不過起始行稍有不同。

請求報文格式

先來看下請求報文的格式

示例如下:

請求方法比較常見的有以下幾類

1、 GET: 請求 URL 指定的資源,指定的資源經伺服器端解析後返回響應內容,GET 方法具有冪等性,即無論請求多次,都只會返回資源,而不會額外建立或改變資源, GET 請求只傳請求頭,不傳請求體。

2、 HEAD: 語義上與 GET 類似,但 HEAD 的響應只有請求頭,沒有請求體

3、 POST: 主要用來建立,修改,上傳資源,不具有冪等性,一般將要請求的資源附在請求體上傳輸

4、 PUT: 修改資源,基本上不用,因為 POST 也具有修改的語義,所以基本上線上大多用 POST 來代替。

5、 OPTIONS: 列出可對資源實行的方法,這個方法很少見,但在跨域中會用到,也比較重要,後文結合 Cookie 談到安全問題時我們會再提

協議版本指定了客戶端當前支援的 HTTP 版本,HTTP 目前通用的有 1.1, 2.0 三個版本,如果請求方指定了 1.1,應答方收到之後也會使用 HTTP 1.1 協議進行回覆。

響應報文格式

響應行的報文格式如下

示例如下

響應報文主要有如下五類狀態碼:

  • 1××:提示資訊,表示目前是協議處理的中間狀態,還需要後續的操作;
  • 2××:成功,報文已經收到並被正確處理;
  • 3××:重定向,資源位置發生變動,需要客戶端重新傳送請求;
  • 4××:客戶端錯誤,請求報文有誤,伺服器無法處理;
  • 5××:伺服器錯誤,伺服器在處理請求時內部發生了錯誤。

常用的狀態碼及其釋義如下(更詳細的可以參考文末參考連結)

狀態碼 狀態碼的原因短語 釋義
100 Continue 這個臨時響應表明,迄今為止的所有內容都是可行的,客戶端應該繼續請求,如果已經完成,則忽略它。
200 OK 請求獲取/建立資源成功
201 Created 該請求已成功,並因此建立了一個新的資源,通過用到 POST 請求中
206 Partial Content 伺服器已經成功處理了部分 GET 請求。類似於 FlashGet 或者迅雷這類的 HTTP 下載工具都是使用此類響應實現斷點續傳或者將一個大文件分解為多個下載段同時下載。該請求必須包含 Range 頭資訊來指示客戶端希望得到的內容範圍,並且可能包含 If-Range 來作為請求條件。
301 Moved Permanently 永久重定向,說明請求的資源已經被移動到了由 Location 頭部指定的url上,是固定的不會再改變
302 Found 臨時重定向,重定向狀態碼錶明請求的資源被暫時的移動到了由Location 頭部指定的 URL 上
400 Bad Request 1、語義有誤,當前請求無法被伺服器理解。除非進行修改,否則客戶端不應該重複提交這個請求。2、請求引數有誤
401 Unauthorized 當前請求需要使用者驗證。該響應必須包含一個適用於被請求資源的 WWW-Authenticate 資訊頭用以詢問使用者資訊。
403 Forbidden 伺服器已經理解請求,但是拒絕執行它,可以認為使用者沒有許可權
404 Not Found 請求失敗,請求所希望得到的資源未被在伺服器上發現
405 Method Not Allowed 請求行中指定的請求方法不能被用於請求相應的資源。該響應必須返回一個Allow 頭資訊用以表示出當前資源能夠接受的請求方法的列表
500 Internal Server Error 伺服器遇到意外的情況並阻止其執行請求。
502 Bad Gateway 此錯誤響應表明伺服器作為閘道器需要得到一個處理這個請求的響應,但是得到一個錯誤的響應。

瞭解這些狀態碼,我們定位排查問題就能成竹在胸,比如看到 5xx,就知道是 server 的邏輯有問題,看到 400 就知道是客戶端的請求引數有問題,另一端就不用傻傻去排查了。

請求和響應頭

請求和響應頭部報文的 header 格式基本都是一樣的,都是 key-value 的形式,key 和 value 都是用 「: 」分隔,此外 HTTP 頭欄位非常靈活,除了使用標準的 Host,Connection 等頭欄位外,也可以任意新增自定義頭,這就給 HTTP 協議帶來了無限的擴充套件可能!

常用頭欄位

HTTP 協議規定了非常多的頭欄位,可以實現各種各樣的功能,但基本上可以分為以下四類

  1. 通用欄位:在請求頭和響應頭裡都可以出現;
  2. 請求欄位:僅能出現在請求頭裡,進一步說明請求資訊或者額外的附加條件;
  3. 響應欄位:僅能出現在響應頭裡,補充說明響應報文的資訊;
  4. 實體欄位:它實際上屬於通用欄位,但專門描述 body 的額外資訊。

對 HTTP 報文的解析和處理其實本質上就是對頭欄位的處理,HTTP 的連線管理,快取控制,內容協商等都是通過頭欄位來處理的,理解了頭欄位,基本上也就理解了 HTTP,所以理解頭欄位非常重要。接下來我們就來看看這些頭部欄位的具體含義

1、通用欄位

首部欄位名 說明
Cache-Control 控制快取的行為
Connection 逐跳首部、連線的管理
Date 建立報文的日期時間
Pragma 報文指令
Trailer 報文末端的首部一覽
Transfer-Encoding 指定報文主體的傳輸編碼方式
Upgrade 升級為其他協議
Via 代理伺服器的相關資訊
Warning 錯誤通知

2、請求首部欄位

首部欄位名 說明
Accept 使用者代理可處理的媒體型別
Accept-Charset 優先的字符集
Accept-Encoding 優先的內容編碼
Accept-Language 優先的語言(自然語言)
Authorization Web 認證資訊
Expect 期待伺服器的特定行為
From 使用者的電子郵箱地址
Host 請求資源所在伺服器
If-Match 比較實體標記(ETag)
If-Modified-Since 比較資源的更新時間
If-None-Match 比較實體標記(與 If-Match 相反)
If-Range 資源未更新時傳送實體 Byte 的範圍請求
If-Unmodified-Since 比較資源的更新時間(與 If-Modified-Since 相反)
Max-Forwards 最大傳輸逐跳數
Proxy-Authorization 代理伺服器要求客戶端的認證資訊
Range 實體的位元組範圍請求
Referer 對請求中 URI 的原始獲取方 傳輸編碼的優先順序
TE 傳輸編碼的優先順序
User-Agent HTTP 客戶端程式的資訊

3、響應首部欄位

首部欄位名 說明
Accept-Ranges 是否接受位元組範圍請求
Age 推算資源建立經過時間
ETag 資源的匹配資訊
Location 令客戶端重定向至指定 URI
Retry-After 對再次發起請求的時機要求
Server HTTP 伺服器的安裝資訊
Vary 代理伺服器快取的管理資訊
WWW-Authenticate 伺服器對客戶端的認證資訊
If-Match 比較實體標記(ETag)
If-Modified-Since 比較資源的更新時間
If-None-Match 比較實體標記(與 If-Match 相反)
If-Range 資源未更新時傳送實體 Byte 的範圍請求
If-Unmodified-Since 比較資源的更新時間(與 If-Modified-Since 相反)
Max-Forwards 最大傳輸逐跳數
Proxy-Authorization 代理伺服器要求客戶端的認證資訊
Range 實體的位元組範圍請求
Referer 對請求中 URI 的原始獲取方 傳輸編碼的優先順序
TE 傳輸編碼的優先順序
User-Agent HTTP 客戶端程式的資訊

4、實體首部欄位

首部欄位名 說明
Allow 資源可支援的 HTTP 方法
Content-Encoding 實體主體適用的編碼方式
Content-Language 實體主體的自然語言
Content-Length 實體主體的大小(單位 :位元組)
Content-Location 替代對應資源的 URI
Content-MD5 實體主體的報文摘要
Content-Range 實體主體的位置範圍
Content-Type 實體主體的媒體型別
Expires 實體主體過期的日期時間

這麼多欄位該怎麼記呢,死記硬背肯定不行,從功能上理解會更易懂,HTTP 主要為我們提供瞭如下功能

一、內容協商

內容協商機制是指客戶端和伺服器端就響應的資源內容進行交涉,然後提供給客戶端最為適合的資源。內容協商會以響應資源的語言、字符集、編碼方式等作為判斷的基準,舉一個常見的例子,大家在 Chrome 上設定不同的語言,主頁也就展示不同的內容

內容協商示例
內容協商示例

請求報文的「Accept」,「Accept-Charset」,「Accept-Encoding」,「Accept-Language」,「Content-Language」,即為內容協商的判定標準。

舉個例子

上圖表示的含義如下

  1. 客戶端:請給我型別為 text/*,語言為 en,編碼型別最好為 br(如果沒有 gzip 也可接受)的資源。
  2. 伺服器:好的,我找到了編碼型別為 br(Content-Encoding: br),內容為 en(Content-Language: en)的資源,它的 url 為 URLe(Content-Location: /URLe),你拿到後再去請求下就有了。

內容協議請求頭和對應的響應頭對應關係如下:

二、快取管理

快取管理也是 HTTP 協議非常重要的內容,這部分也是務必要掌握的。

對於資源來說,由於有些挺長時間內都不會更新,所以沒必要每次請求都向 server 發起網路請求,如果第一次請求後能儲存在本地,下次請求直接在本地取,那無疑會快得多,對伺服器的壓力也會減少。

涉及到快取的請求頭為 Cache-Control。這個快取指令是單向的,也就是說請求中設定的指令,不一定包含在響應中,請求中如果沒有傳 Cache-Control, server 也可以返回 Cache-Control。

如圖示:客戶端發起請求後,伺服器返回 Cache-Control: max-age=30,代表資源在客戶端可以快取 30 秒,30 秒內客戶端的請求可以直接從快取獲取,超過 30 秒後需要向伺服器發起網路請求。

max-age 是 HTTP 快取控制最常用的屬性,表示資源儲存的最長時間,需要注意的是,時間的計算起點是響應報文的建立時刻(即 Date 欄位,也就是離開伺服器的時刻),超過後客戶端需要重新發起請求。

除此之外,還有其它屬性值如下:

no-cache: 這個是很容易造成誤解的欄位,它的含義並不是不允許快取,而是可以快取,但在使用之前必須要去伺服器驗證是否過期,是否有最新的版本;

no-store: 這才是真正的不允許快取,比如秒殺頁面這樣變化非常頻繁的頁面就不適合快取

no-cache
no-cache

must-revalidate:一旦資源過期(比如已經超過max-age),在成功向原始伺服器驗證之前,快取不能用該資源響應後續請求。

三者的區別如下:

Cache-Control 只能重新整理資料,但不能很好地利用快取,又因為快取會失效,使用前還必須要去伺服器驗證是否是最新版,存在一定的效能穩定,所以 HTTP 又引入了條件快取。

條件請求以 If 開頭,有「If-Match」,「If-Modified-Since」,「If-None-Match」,「If-Range」,「If-Unmodified-Since」五個頭欄位,我們最常用的是「if-Modified-Since」和「If-None-Match」這兩個頭欄位,所以重點介紹一下。

if-Modified-Since:指的是檔案最後修改時間,伺服器只在所請求的資源在給定的日期時間之後對內容進行過修改的情況下才會將資源返回,如果請求的資源從那時起未經修改,那麼返回一個不帶有訊息主體的 304 響應,需要第一次請求提供「Last-modified」,第二次請求就可以在 「if-Modified-Since」首部帶上此值了。

If-None-Match:條件請求首部,對於 GETHEAD 請求方法來說,當且僅當伺服器上沒有任何資源的 ETag 屬性值與這個首部中列出的相匹配的時候,伺服器端會才返回所請求的資源,響應碼為 200,

注意到上圖中有個 ETag 返回,它是實體標籤(Entity Tag)的縮寫,是資源的唯一標識,主要解決修改時間無法準確區分檔案變化的問題,比如檔案在一秒內修改了多次,由於修改時間是秒級的,用 if-Modified-Since 就會誤認為資源沒有變化,而每次檔案修改了都會修改 ETag,也就是說 ETag 可以精確識別資源的變動, 所以如果對資源變化很敏感覺的話,應該用 If-None-Match。

注:ETag 有強,弱之分,強 ETag 要求資源在位元組級別必須完全相符,弱 ETag 在值前有 「W/」標記,只要求資源在語義上沒啥變化,比如加了幾個空格等等。

需要注意的是不管是 if-Modified-Since 還是 If-None-Match,這兩者只會在資源過期(即存活時間超 max-age)後才會觸發,但如果在開發環境下,快取可能會影響我們聯調,我們希望每次請求都從 server 拿,而不是快取裡,該怎麼辦?這種情況下就要用到重新整理或者強制重新整理了。如果是重新整理,請求頭裡會加上一個 Cache-Control: max-age=0,代表需要最新的資源,瀏覽器看到了後就不會使用本地資源,會向 server 請求,如果是強制重新整理,請求頭會加上Cache-Control: no-cache,也會向 server 傳送請求,通常重新整理和強制重新整理效果一下。

三、實體首部

由於實體部分可以傳文字,音視訊,檔案等,所以一般要指定實體型別,內容大小,編碼型別,實體採用的語言(英文,法語)等,這樣應答方才會理解其內容。

先來看最重要的 Content-Type,通常有以下幾種資料型別

這些資料型別被稱為 MIME 型別,指示資源所屬型別,請求方如果要上傳資源(一般是 POST 請求),可以在用 Content-Type 指定資源所屬型別,如果請求方想要獲取資源(GET 請求),可以用 Accept 請求頭指定想要獲取什麼資源,這樣 server 找到匹配的資源後就可以在 Content-Type 中指定返回的資源型別,瀏覽器等客戶端看到後就可以據此解析處理了

如圖示:客戶端使用 Accept: image/ 告訴伺服器,我想請求 png,jpeg,svg 等屬於 image 型別的資源,服務端返回圖片的同時用 Content-Type: image/png 告訴客戶端資源型別為 png。*

通過 Content-Type 指定資源的方式瀏覽器等客戶端就可以據此作出相關解析處理了,再看下開頭的問題,為啥請求圖片本來希望是能直接在瀏覽器上展示的,實際上卻直接下載了呢,抓包看下它的 Content-Type

可以看到返回的 Content-Type 是 application/octet-stream,這個型別是應用程式檔案的預設值,意思是未知的應用程式檔案,瀏覽器一會不會自動執行或詢問執行,會直接下載。那如果希望圖片能自動展示在瀏覽器而非下載呢,server 指定一下 Content-Type 的具體型別如 image/png 這樣的形式即可,瀏覽器識別到了具體型別就會自動解析展示出來。

其他請求方,應答方的實體首部對應關係在上文內容協商部分也給出了,再貼一下圖:

四、連線管理

連線管理也是 HTTP 非常重要的功能,只不過因為使用者無感知,所以被很多人忽略了,實際它是幕後英雄,對提升傳輸效能起了巨大的作用,也促進 HTTP 不斷不斷改版衍進的重要原因之一,我們可以從 HTTP 的各個版本來看下連線管理的功能改進。

首先我們知道雙方要建立可靠連線要經過 TCP 的三次握手,然後才能開始傳輸 HTTP 的報文,報文傳輸之後要經過四次揮手斷開連線

HTTP 0.9,1.0 時期,傳送完 HTTP 報文後, 連線立馬關閉,這種連線被稱為短連結

可以看到短連結效率非常低下,大量時間浪費在無意義的三次握手和四次揮手上。

於是 HTTP 1.1 對此進行了改進,每次報文傳送後不立即關閉,可複用,我們稱這樣的連結為長連結,對比下圖的長短連結可以看到,長連結由於減少了大量無意義的三次握手,四次揮手,效率大大提升了!

我們可以在請求頭裡明確要求使用長連結,指定 Connection: keep-alive 即可,在 HTTP 1.1 就算不指定也是預設開啟的。如果伺服器支援長連結,不管客戶端是否顯式要求長連結,它都會在返回頭裡帶上 Connection: keep-alive,這樣接下來雙方就會使用長連線來收發報文,客戶端如果想顯式關閉關閉,只需要指定 Connection: Close 頭欄位即可。

長連線讓傳輸效率大大提升,但新的問題又來了,因為 HTTP 規定報文必須一發一收,如果在要連線上發多個 HTTP 報文,多個報文會被累積到佇列裡依次處理(不能並行處理)只要隊首的請求被阻塞了,後續 HTTP 的傳送就受到影響,這就是有名的隊頭阻塞

隊頭阻塞
隊頭阻塞

雖然 HTTP 1.1 提出了管線化機制,一次可以傳送多個請求,但依然要等前一個請求的響應返回後才能處理下一個請求,所以這種機制聊勝於無。

我們再仔細想想,為啥會有隊頭阻塞這樣的問題,本質上其實是因為我們沒法區分每一個請求,再回顧一下我們上文的分層模型

以上是每一層發出的包,每個資料連結層的包(準確地說,鏈路層的包應該叫幀)規定的 IP 資料包的大小是有限制的,一般把這個大小限制稱為最大傳輸單元(MTU, Maximum Transmission Unit), TCP 資料包的大小也是有限制的,我們稱之為 MSS(Maximum Transmission Unit)

也就是說對於每一個最終傳送的乙太網包能傳輸的應用層資料是有限的,如果上層的應用層要發的資料大小超過了乙太網包的大小,就需要對其進行拆分,分成幾個乙太網包再傳輸。

接收方拿到每個包的應用層資料再組裝成應用層資料,然後一個請求才算接收完成,響應也是類似的原理。

這也是實體首部欄位 Content-Length 存在的意義,接收方通過 Content-Length 就可以判斷幾個請求報文組合後大小是否達到這個設定值,如果是說明報文接收完畢,就可以對請求進行解析了,如果少於這個值,說明還需要接收請求包直到達到這個設定的值。

**畫外音,Content-Length 指的是實體訊息首部,也就是在 POST,PUT 等方法中傳輸實體資料時才會出現,GET 請求不會出現 **

在底層,每個請求是複用同一個連線的,也就是說每個包傳送都是序列的。

在 HTTP 1.1 中,沒法區分每個資料包所屬的請求,所以它規定每個請求只能序列處理,每個請求通過 Content-Length 判斷接收完每個請求的資料包並處理後,才能再處理下一個請求,這樣的話如果某個請求處理太慢就會影響後續請求的處理。

那麼 HTTP 2.0 又是如何處理隊頭阻塞的呢,接下來我們就來揭開一下 HTTP 2.0 的面紗。

HTTP 2.0 概覽

HTTP 2.0 在效能上實現了很大的飛躍,更難得的是它在改進的同時保持了語義的不變,與 HTTP 1.1 的語義完全相同!比如請求方法、URI、狀態碼、頭欄位等概念都保留不變,這樣就消除了再學習的成本,在我們的日常軟體升級中,向下相容非常重要,也是促進產品大規模使用的一個前提,不然你一升級,各種介面之類的全換了,誰還敢升。 HTTP 2.0 只在語法上做了重要改進,完全變更了 HTTP 報文的傳輸格式

在語法上主要實現了以下改造

1、頭部壓縮

HTTP 1.1 考慮了 body 的壓縮,但沒有考慮 header 的壓縮, 經常出現傳了頭部上百,上千位元組,但 Body 卻只有幾十位元組的情況,浪費了頻寬,而且我們知道從 1.1 開始預設是長連線,幾百上千個請求都用的這個連線,而請求的頭部很多都是重複的,造成了頻寬的極大浪費!想象一下面的這個請求,為了傳輸區區 「name=michale 」這幾個位元組,卻要傳輸如此巨量的頭部,浪費的頻寬確實驚人。

那麼 HTTP 2.0 是如何解決的呢?它開發了專門的 「HPACK」演算法,在客戶端和伺服器兩端建立字典,用索引號表示重複的字串,還採用哈夫曼編碼來壓縮數字和整數,可以達到最高達 90% 的壓縮率

這裡簡單解釋下,頭部壓縮需要在支援 HTTP 2.0 的客戶端和伺服器之間:

  1. 維護一份靜態的字典(Static table),包含常見的頭部名稱,以及特別常見的頭部名稱與值的組合。這樣的話如果請求響應命中了靜態字典,直接發索引號即可
  2. 維護一份相同的動態字典(Dynamic table),可以動態地新增字典,這樣的話如果客戶端首次請求由於「User-Agent: xxx」,「host:xxx」,「Cookie」這些的動態鍵值對沒有命中靜態字典,還是會傳給伺服器,但伺服器收到後會基於傳過來的鍵值對建立動態字典條目,如上圖的「User-Agent: xxx」對應數字 62,「host:xxx」對應數字 63,這樣雙方都建立動態條目後,之後就可以用只傳 62,63 這樣的索引號來通訊了!顯而易見,傳輸資料急遽降低,極大地提升了傳輸效率!需要注意的是動態字典是每個連線自己維護的,也就是對於每個連線而言,首次都必須傳送動態鍵值對
  3. 支援基於靜態哈夫曼碼錶的哈夫曼編碼(Huffman Coding):對於靜態、動態字典中不存在的內容,可以使用哈夫曼編碼來減小體積。HTTP/2 使用了一份靜態哈夫曼碼錶(詳見),也需要內建在客戶端和服務端之中。

2、二進位制格式

HTTP 1.1 是純文字形式,而 2.0 是完全的二進位制形式,它把 TCP 協議的部分特性挪到了應用層,把原來的 Header+Body 訊息打散為了數個小版的二進位制"幀"(Frame),“HEADERS”幀存放頭資料、“DATA”幀存放實體資料

HTTP 2.0 二進位制幀
HTTP 2.0 二進位制幀

這些二進位制幀只認 0,1,基於這種考慮 http 2.0 的協議解析決定採用二進位制格式,使用二進位制的形式雖然對人不友好,但大大方便了計算機的解析,原來使用純文字容易出現多義性,如大小寫,空白字元等,程式在處理時必須用複雜的狀態機,效率低,還麻煩。而使用二進位制的話可以嚴格規定欄位大小、順序、標誌位等格式,“對就是對,錯就是錯”,解析起來沒有歧義,實現簡單,而且體積小、速度快。

3. 流

HTTP 2 定義了「流」(stream)的的概念,它是二進位制幀的雙向傳輸序列,同一個訊息往返的資料幀 (header 幀和 data 幀)會分配一個唯一的流 ID,這樣我們就能區分每一個請求。在這個虛擬的流裡,資料幀按先後次序傳輸,到達應答方後,將這些資料幀按它們的先後順序組裝起來,最後解析 HTTP 1.1 的請求頭和實體。

同一時間,請求方可以在流裡發請求,應答方也可以也流裡發響應,對比 HTTP 1.1 一個連線一次只能處理一次請求-應答,吞吐量大大提升

如圖示,一個連線裡多個流可以同時收發請求-應答資料幀,每個流中資料包按序傳輸組裝

所有的流都是在同一個連線中流動的,這也是 HTTP 2.0 經典的多路複用( Multiplexing),另外由於每個流都是獨立的,所以誰先處理好請求,誰就可以先將響應通過連線傳送給對方,也就解決了隊頭阻塞的問題。

如圖示,在 HTTP 2 中,兩個請求同時傳送,可以同時接收,而在 HTTP 1.1 中必須等上一個請求響應後才能處理下一個請求

HTTP 2 的隊頭阻塞

HTTP 2 引入的流,幀等語法層面的改造確實讓其傳輸效率有了質的飛躍,但是它依然存在著隊頭阻塞,這是咋回事?

其實主要是因為 HTTP 2 的分幀主要是在應用層處理的,而分幀最終還是要傳給下層的 TCP 層經由它封裝後再進行傳輸,每個連線最終還是順序傳輸這些包,

如圖示:流只是我們虛擬出來的概念,最終在連線層面還是順序傳的

TCP 是可靠連線,為了保證這些包能順序傳給對方,會進行丟包重傳機制,如果傳了三個包,後兩個包傳成功,但第一個包傳失敗了,TCP 協議棧會把已收到的包暫存到快取區中,停下等待第一個包的重傳成功,這樣的話在網路不佳的情況下只要一個包阻塞了,由於重傳機制,後面的包就被阻塞了,上層應用由於拿不到包也只能乾瞪眼了。

由於這是 TCP 協議層面的機制,無法改造,所以 HTTP 2 的隊頭阻塞是不可避免的。HTTP 3 對此進行了改進,將 TCP 換成了 UDP 來進行傳輸,由於 UDP 是無序的,不需要斷建連,包之間沒有依賴關係,所以從根本上解決了“隊頭阻塞”, 當然由於 UDP 本身的這些特性不足以支撐可靠的通訊,所以 Google 在 UDP 的基礎上也加了 TCP 的連線管理,擁塞視窗,流量控制等機制,這套協議我們稱之為 QUIC 協議。

HTTP 1,2,3 三者對比
HTTP 1,2,3 三者對比

可以看到不管是 HTTP 2 還是 3 它們底層都支援用 TLS,保留了 HTTPS 安全的特性,這也可以理解,在網際網路發展如此迅猛的今天,各大企業也越來越重視通訊安全。

總結:HTTP 的特點

說了這麼多 HTTP,接下來我們簡單總結一下 HTTP 的特點

1、靈活可擴充套件

這可以說是 HTTP 最重要的特點,也是 HTTP 能大行其道並碾壓其他協議稱霸於世的根本原因!它只規定了報文的基本格式,用空格分隔單詞,用換行分隔欄位,「header+body」等基本語義,但在語法層面並不做限制,它並沒有強制規定 header 裡應該傳什麼,也沒有限制它底層應該用什麼傳輸,這也為 HTTPS 新增 SSL/TLS 層來加密傳輸,HTTP2 使用幀,流來進行多路複用,HTTP 3 使用 UDP 徹底解決解決隊頭阻塞問題提供了可能!後續如果又出現了牛逼的協議,底層也隨時可以替換

2、可靠傳輸

不管底層是 TCP 還是 QUIC(底層使用 UDP),它們的傳輸都是可靠的,都能保證應用層請求響應的可靠傳輸,這一點很重要,不然傳輸過程中缺胳膊少腿,應用層就無法解析了。

3、應用層協議

HTTP 是一個協議,很多人把 HTTP 和 TCP 混在了一起, 就像前文所述,TCP 相當於高速公路,為我們提供了可靠的傳輸通道,HTTP 規定貨物的表現形式(header + 空行+ body),貨物是否可從中間站運回(快取機制)高速公路是否應該關閉(連線控制),至於貨物如何可靠傳輸到目的地,那是 TCP 的事,與 HTTP 無關,這一點也是不少人經常搞混的。

4、請求應答模式

HTTP 需要請求方發起請示,然後應答方對此作出響應,應答方不會無緣無故地發響應給請求方,另外請求和應答方的角色是可以互換的,比如 HTTP 2 中 server 是可以主動 push 給 client 的,這種情況下 server 即為請求方,cilent 即為應答方

5、無狀態

HTTP 的每個請求-應答都是無關的,即每次的收發報文都是完全獨立,沒有任何聯絡的,伺服器收到每個請求響應後,不會記錄這個請求的任何資訊,有人說不對啊,為啥我新增多次購物車,購物車列表還能保留我之前加過的商品呢?這就要簡單地瞭解一下 Session 和 Cookie 了,Session 可以認為是 Server 用來追蹤每個使用者行為的一個會話,server 會給每個使用者分配一個這個 Session 的 session,通過 Cookie 這個頭欄位返回給 client,之後 client 每次請求都會在 Cookie 裡帶上這個 sessionId,server 拿到 sessionId 之後就知道是哪個使用者發起的了

回答開篇問題

大部分問題已經隱藏在本文的知識點講述中了,還有一些問題,我們一起來看下

1、為什麼說反爬是個偽命題

因為不管是正常的客戶端請求,還是爬蟲請求,都要遵循 HTTP 協議,爬蟲發的 HTTP 報文與正常使用者請求沒有本質區別,伺服器無法區分,伺服器只能通過一些 trick,如短時間內發現某個 ip 的請求特別頻繁認定其為爬蟲,直接拒絕服務,或者通過驗證碼的方式等提高爬蟲的難度,但無法徹底杜絕,當然爬蟲也有應對之道,它可以在請求的時候不停地更換自己的 ip 以達到欺騙 server 的目的,也可以破解驗證碼,爬蟲和反爬也是在相愛相殺中不斷提高破解與被破解的手段了。

2、301 與 302 有啥區別 這一點其實我在之前的高效能短鏈設計有提過,在短鏈設計中,重定向是一個必須要考慮的點

如圖示,輸入 A 網址後,會重定向到 B 網址,就需要考慮是用 301 還是 302,兩者的區別如下

  • 301:代表永久重定向,也就是說第一次請求拿到長連結後,下次瀏覽器再去請求短鏈的話,不會向短網址伺服器請求了,而是直接從瀏覽器的快取裡拿,這樣在 server 層面就無法獲取到短網址的點選數了,如果這個連結剛好是某個活動的連結,也就無法分析此活動的效果。所以我們一般不採用 301。
  • 302:代表臨時重定向,也就是說每次去請求短鏈都會去請求短網址伺服器(除非響應中用 Cache-Control 或 Expired 暗示瀏覽器快取),這樣就便於 server 統計點選數,所以雖然用 302 會給 server 增加一點壓力,但在資料異常重要的今天,這點程式碼是值得的,所以推薦使用 302!

3、HTTP 1.1 唯一一個要求請求頭必傳的欄位是哪個,它有什麼作用

是 Host, HTTP 1.1 允許一臺伺服器搭建多個 Web 站點,也就是說一臺伺服器可以託管多個域名對應的網站,這樣的話必須指定 Host,到達伺服器後才能找到對應的網址向其請求。

4. 老生常談很多人的誤解:GET 和 POST 的區別是啥

這個問題很多人都有誤解,最常見的誤解比如 POST 請求安全,GET引數通過 URL 傳遞,POST 放在請求體裡等等,這些回答沒有 GET 到點上,其實 GET,POST 都可以用來傳輸資訊,GET 請求可以用 body 傳輸資料,在POST 請求時你可以不用不用 body 而用 url 傳輸資料,這都是可以實現的,這就好比你可以用救護車來運貨,也可以用卡車來救人,都沒有問題的,但這不符合人們的認知, 不符合 HTTP 對其定義的語義,無規矩不成方圓,遵循語義大家溝通才能更高效,所以其實它們的區別只在語義上有區別,至於安全,那是 HTTPS 的事了。

最後:求三連

這篇文章斷斷續續寫了兩週,希望大家能三連走起,感謝支援!

未關注的朋友可以掃碼關注,接收後續更多精彩內容!

巨人的肩膀

  • 極客時間-透視 HTTP 協議
  • 圖解 HTTP
  • https://blog.konghy.cn/2019/08/06/dns-overview/
  • https://aws.amazon.com/cn/route53/what-is-dns/
  • https://developer.mozilla.org/en-US/docs/Web/HTTP
  • 跨域:https://blog.csdn.net/github_34708151/article/details/105383810
  • HTTP 狀態碼:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
  • http://entropymine.com/jason/testbed/mime/
  • https://httpwg.org/specs/rfc7541.html#static.table.definition
  • https://imququ.com/post/header-compression-in-http2.html
  • https://developers.google.com/web/fundamentals/performance/http2

相關文章