HTTP緩存是個大公司面試幾乎必考的問題,寫篇隨筆說一下HTTP緩存。 1. HTTP報文首部中有關緩存的欄位 在HTTP報文中,與緩存相關的信息都存在首部里,簡單說一下首部。 首部 HTTP首部欄位向請求報文和相應報文中添加了一些附加信息。本質上來說,它們只是一些鍵值對的列表。比如,下麵的首部行會 ...
HTTP緩存是個大公司面試幾乎必考的問題,寫篇隨筆說一下HTTP緩存。
1. HTTP報文首部中有關緩存的欄位
在HTTP報文中,與緩存相關的信息都存在首部里,簡單說一下首部。
首部
HTTP首部欄位向請求報文和相應報文中添加了一些附加信息。本質上來說,它們只是一些鍵值對的列表。比如,下麵的首部行會向Content-Length首部欄位賦值19:
Content-Length: 19
HTTP規範定義了幾中首部欄位。應用程式也可以隨意發明自己所用的首部。HTTP首部可以分為以下幾類:
通用首部
既可以出現在請求報文中,也可以出現在響應報文中。
請求首部
提供更多有關請求的信息。
響應首部
提供更多有關響應的信息。
實體首部
描述主體的長度和內容,或資源本身。
擴展首部
規範中沒有定義的新首部。
想瞭解更多有關HTTP首部或報文的信息,個人推薦《HTTP權威指南》。
首部中與緩存有關的欄位
通用首部欄位
請求首部欄位
響應首部欄位
實體首部欄位
2. 緩存的處理步驟
除了一些微小的細節,Web緩存的工作原理基本很簡單,對一條HTTP GET報文的基本緩存處理過程包括7個步驟。
- 接收——緩存從網路中讀取抵達的請求報文。
- 解析——緩存對報文解析,提取出URL和各種首部。
- 查詢——緩存查看是否有本地副本可用,如果沒有就向伺服器獲取一份副本,並將其保存在本地。
- 新鮮度檢測——緩存查看以緩存的副本是否新鮮,如果不是,就詢問伺服器是否有更新。
- 創建響應——緩存會用新的首部和以緩存的主體來構建一條響應報文。
- 發送——緩存通過網路將響應發揮給客戶端。
- 日誌——緩存可選的創建一個日誌文件來描述這個事務。
緩存的處理步驟如圖:
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">
它告訴瀏覽器每次請求頁面時都不要讀緩存,都得往伺服器發一次請求才行。
但是這種禁用緩存的形式作用不是那麼太大:
僅有IE才能識別這段meta標簽含義,其它主流瀏覽器僅能識別“Cache-Control: no-store”的meta標簽。
在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 欄位會被忽略:
Last-Modified值相等,即資源在服務端沒有新的修改;
服務端需返回2XX和412之外的狀態碼;
傳來的指定日期不合法
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緩存理解的很不錯了,如果是一百分的話也應該可以拿到八十分了。