詳解HTTP緩存


HTTP緩存是個大公司面試幾乎必考的問題,寫篇隨筆說一下HTTP緩存。

1. HTTP報文首部中有關緩存的字段


在HTTP報文中,與緩存相關的信息都存在首部里,簡單說一下首部。

首部

HTTP首部字段向請求報文和相應報文中添加了一些附加信息。本質上來說,它們只是一些鍵值對的列表。比如,下面的首部行會向Content-Length首部字段賦值19:

Content-Length: 19

HTTP規范定義了幾中首部字段。應用程序也可以隨意發明自己所用的首部。HTTP首部可以分為以下幾類:

  • 通用首部

    既可以出現在請求報文中,也可以出現在響應報文中。

  • 請求首部

    提供更多有關請求的信息。

  • 響應首部

    提供更多有關響應的信息。

  • 實體首部

    描述主體的長度和內容,或資源本身。

  • 擴展首部

    規范中沒有定義的新首部。

想了解更多有關HTTP首部或報文的信息,個人推薦《HTTP權威指南》。

首部中與緩存有關的字段

  • 通用首部字段

  • 請求首部字段

  • 響應首部字段

  • 實體首部字段

2. 緩存的處理步驟


除了一些微小的細節,Web緩存的工作原理基本很簡單,對一條HTTP GET報文的基本緩存處理過程包括7個步驟。

  1. 接收——緩存從網絡中讀取抵達的請求報文。
  2. 解析——緩存對報文解析,提取出URL和各種首部。
  3. 查詢——緩存查看是否有本地副本可用,如果沒有就向服務器獲取一份副本,並將其保存在本地。
  4. 新鮮度檢測——緩存查看以緩存的副本是否新鮮,如果不是,就詢問服務器是否有更新。
  5. 創建響應——緩存會用新的首部和以緩存的主體來構建一條響應報文。
  6. 發送——緩存通過網路將響應發揮給客戶端。
  7. 日志——緩存可選的創建一個日志文件來描述這個事務。

緩存的處理步驟如圖:

3. 文檔過期和服務器再驗證


文檔過期

通過特殊的HTTP Cache-Control首部和Expires首部,HTTP讓原始服務器向每個文檔加了一個過期時間,這些首部說明了多長時間內可將這些內容視為新鮮的。

在文檔過期之前,緩存可以以任意頻率使用這些副本,而無需與服務器進行聯系,除非客戶端請求中包含有阻止提供已緩存或未驗證資源的首部。一旦已緩存的文檔過期,緩存就必須與服務器進行核對,詢問文檔是否被修改過,如果被修改過,就要獲取一份新鮮的並帶有新的過期日期的副本。

服務器再驗證

但是緩存文檔過期並不意味着它與服務器上的文檔有實際的區別,只是以為到了要進行核對的時間了。這種情況叫服務器再驗證,說明緩存需要詢問服務器文檔是否發生了變化。

  • 如果再驗證顯示內容發生了變化,緩存會獲取一份新的文檔副本,並將其存儲在舊文檔的位置上,然后將文檔發送給客戶端。

  • 如果再驗證顯示內容沒有發生變化,緩存只要獲取新的首部,包括一個新的過期日期,並對緩存中的首部進行更新就好了。

HTTP定義了幾個首部用來實現緩存是否新鮮的驗證,像開篇我們說到的If-Modified-Since和If-None-Match、Last-Modified等都屬於這樣的首部。

強緩存和協商緩存

我們根據是否需要向服務器發起請求將緩存分成兩類,不需要向服務器發起請求的緩存叫強緩存,也就是上面所說的文檔沒過期時候用到的緩存。需要向服務器發起請求的緩存叫協商緩存,也就是上面服務器再驗證用到的緩存。

下面我們詳細介紹強緩存和協商緩存。

4. 強緩存


上面我們說可以通過Cache-Control首部和Expires首部來標明文檔的過期時間。如果沒有過期的話,自然就是從緩存里取文檔了:

顧名思義,這里的memory cache內存了,disk cache就是磁盤的緩存了,再往下說就到了webkit的緩存機制了。

Expires

為什么要先說Expires呢?因為相比於Cache-Control,Expires出現的較早,是HTTP1.0的東西,而Cache-Control是HTTP1.1的東西。

Expires的值對應一個GMT,也就是格林尼治時間,比如“Mon, 22 Jan 2019 11:12:01 GMT”來告訴瀏覽器資源緩存過期時間,如果還沒過該時間點則不發請求。

在客戶端我們同樣可以使用meta標簽來知會IE(也僅有IE能識別)頁面(同樣也只對頁面有效,對頁面上的資源無效)緩存時間:

<meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">

如果希望在IE下頁面不走緩存,希望每次刷新頁面都能發新請求,那么可以把“content”里的值寫為“-1”或“0”。但是是該方式僅僅作為知會IE緩存時間的標記,你並不能在請求或響應報文中找到Expires字段。

那么如果Pragma和Expires一起出現的話,Pragma的優先級是高的。

注意:響應報文中Expires所定義的緩存時間是相對服務器上的時間而言的,如果客戶端上的時間跟服務器上的時間不一致,特別是如果你修改了自己電腦的系統時間,那緩存時間可能就沒什么意義了。

Pragma

既然我們已經說了Expires是HTTP1.0的遺留物,那我們也要介紹下Pragma。

當該字段值為“no-cache”的時候,會通知客戶端不要對該資源讀緩存,即每次都得向服務器發一次請求才行。

Pragma屬於通用首部字段,在客戶端上使用時,常規要求我們往html上加上這段meta元標簽:

<meta http-equiv="Pragma" content="no-cache">

它告訴瀏覽器每次請求頁面時都不要讀緩存,都得往服務器發一次請求才行。

但是這種禁用緩存的形式作用不是那么太大:

  1. 僅有IE才能識別這段meta標簽含義,其它主流瀏覽器僅能識別“Cache-Control: no-store”的meta標簽。

  2. 在IE中識別到該meta標簽含義,並不一定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求,但是僅限頁面,頁面上的資源則不受影響。

所以這種在客戶端定義Pragma並沒有多少作用。

但是在響應報文中加上這個字段就不一樣了,瀏覽器在收到服務器的Pragma字段后會對資源進行標記,禁用其緩存行為,進而后續每次刷新頁面均能重新發出請求而不走緩存。

Cache-Control

針對上述的“Expires時間是相對服務器而言,無法保證和客戶端時間統一”的問題,HTTP1.1新增了 Cache-Control 來定義緩存過期時間,若報文中同時出現了 Pragma、Expires 和 Cache-Control,會以 Cache-Control 為准。

Cache-Control也是一個通用首部字段,這意味着它能分別在請求報文和響應報文中使用。在RFC中規范了 Cache-Control 的格式為:

"Cache-Control" ":" cache-directive"

作為請求首部時,cache-directive的可選值有:

作為響應首部時,cache-directive的可選值有:

我們依舊可以在HTML頁面加上meta標簽來給請求報頭加上 Cache-Control字段,並且可以有多個值:

Cache-Control: max-age=3600, must-revalidate

它意味着該資源是從原服務器上取得的,且其新鮮度的有效時間為一小時,在后續一小時內,用戶重新訪問該資源則無須發送請求。

當然這種組合的方式也會有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。

組合的形式還能做一些瀏覽器行為不一致的兼容處理。例如在IE我們可以使用 no-cache 來防止點擊“后退”按鈕時頁面資源從緩存加載,但在 Firefox 中,需要使用 no-store 才能防止歷史回退時瀏覽器不從緩存中去讀取數據,故我們在響應報頭加上如下組合值即可做兼容處理:

Cache-Control: no-cache, no-store

5. 協商緩存


顧名思義,客戶端通過與服務器進行協商是否使用緩存。前面我們已經說過了HTTP提供了實現緩存文件是否新鮮的驗證、提升緩存的復用率的幾個首部,就來說說這些首部。其實它們都是HTTP1.1新增的。

Last-Modified

服務器將資源傳遞給客戶端時,會將資源最后更改的時間以“Last-Modified: GMT”的形式加在實體首部上一起返回給客戶端。

客戶端會為資源標記上該信息,下次再次請求時,會把該信息附帶在請求報文中一並帶給服務器去做檢查,若傳遞的時間值與服務器上該資源最終修改時間是一致的,則說明該資源沒有被修改過,直接返回304狀態碼即可。

至於傳遞標記起來的最終修改時間的請求報文首部字段一共有兩個:

⑴ If-Modified-Since: Last-Modified-value

示例:

If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT

該請求首部告訴服務器如果客戶端傳來的最后修改時間與服務器上的一致,則直接回送304 和響應報頭即可。

當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 Last-Modified 值。

⑵ If-Unmodified-Since: Last-Modified-value

告訴服務器,若Last-Modified沒有匹配上,即資源在服務端的最后更新時間改變了,則應當返回412(Precondition Failed) 狀態碼給客戶端。

當遇到下面情況時,If-Unmodified-Since 字段會被忽略:

  1. Last-Modified值相等,即資源在服務端沒有新的修改;

  2. 服務端需返回2XX和412之外的狀態碼;

  3. 傳來的指定日期不合法

Last-Modified 說好卻也不是特別好,因為如果在服務器上,一個資源被修改了,但其實際內容根本沒發生改變,會因為Last-Modified時間匹配不上而返回了整個實體給客戶端。

ETag

為了解決上述Last-Modified可能存在的不准確的問題,Http1.1還推出了 ETag 實體首部字段。

服務器會通過某種算法,給資源計算得出一個唯一標志符(比如md5標志),在把資源響應給客戶端的時候,會在實體首部加上“ETag: 唯一標識符”一起返回給客戶端。

客戶端會保留該 ETag 字段,並在下一次請求時將其一並帶過去給服務器。服務器只需要比較客戶端傳來的ETag跟自己服務器上該資源的ETag是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。

如果服務器發現ETag匹配不上,那么直接以常規GET 200回包形式將新的資源以及新的Etag發給客戶端;如果ETag是一致的,則直接返回304知會客戶端直接使用本地緩存即可。

那么客戶端是如何把標記在資源上的 ETag 傳去給服務器的呢?請求報文中有兩個首部字段可以帶上 ETag 值:

⑴ If-None-Match: ETag-value

示例為

If-None-Match: "56fcccc8-1699"

告訴服務端如果 ETag 沒匹配上需要重發資源數據,否則直接回送304 和響應報頭即可。

當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 ETag 值。

⑵ If-Match: ETag-value

告訴服務器如果沒有匹配到ETag,或者收到了“*”值而當前並沒有該資源實體,則應當返回412(Precondition Failed) 狀態碼給客戶端。否則服務器直接忽略該字段。

If-Match 的一個應用場景是,客戶端走PUT方法向服務端請求上傳/更替資源,這時候可以通過 If-Match 傳遞資源的ETag。

需要注意的是,如果資源是走分布式服務器(比如CDN)存儲的情況,需要這些服務器上計算ETag唯一值的算法保持一致,才不會導致明明同一個文件,在服務器A和服務器B上生成的ETag卻不一樣。

如果 Last-Modified 和 ETag 同時被使用,則要求它們的驗證都必須通過才會返回304,若其中某個驗證沒通過,則服務器會按常規返回資源實體及200狀態碼。

如果面試的時候能說出這些,就代表了你對HTTP緩存理解的很不錯了,如果是一百分的話也應該可以拿到八十分了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM