HTTP 快取控制總結

孫世吉發表於2016-07-26

引言

通過網路獲取內容既緩慢,成本又高:大的響應需要在客戶端和伺服器之間進行多次往返通訊,這拖延了瀏覽器可以使用和處理內容的時間,同時也增加了訪問者的資料成本。因此,快取和重用以前獲取的資源的能力成為優化效能很關鍵的一個方面。

本文用於解決以下六個疑問。

  • 與快取相關的HTTP首部欄位主要有哪些?
  • 這些HTTP首部欄位之間的聯絡與區別?
  • HTTP快取首部欄位的優先順序?
  • HTTP快取首部欄位的特點與侷限性?
  • 使用者不同的頁面重新整理行為的差別?
  • 在實踐中我們該用哪些報文頭來控制快取呢?

文中使用的1.html以及doge.png如下所示

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>快取控制測試</title>
</head>
<body>
    <img src="doge.png">
</body>
</html>

那些年與快取相關的HTTP首部欄位

我們先來瞅一眼RFC2616規定的47種http報文首部欄位中與快取相關的欄位,事先了解一下能讓我們在心裡有個底:

1.通用首部欄位

2.請求首部欄位

3.響應首部欄位

4.實體首部欄位

石器時代的快取方式

在 http1.0 時代,給客戶端設定快取方式可通過兩個欄位——PragmaExpires來規範。雖然這兩個欄位早可拋棄,但為了做http協議的向下相容,你還是可以看到很多網站依舊會帶上這兩個欄位。例如在訪問 騰訊課堂 的時候,通過瀏覽器除錯工具可以看到部分HTTP響應是包含Expires頭部的。

1.Pragma

當該欄位值為no-cache的時候(事實上現在RFC中也僅標明該可選值),會知會客戶端不要對該資源讀快取,即每次都得向伺服器發一次請求才行。
舉個例子:

通過Fiddler給圖片資源額外增加以下頭部資訊

Cache-Control: public, max-age=86400
Pragma: no-cache

前者用來設定快取資源一天,後者禁用快取。

重新訪問該頁面會發現訪問該資源會重新發起一次請求,同時以上例子也能說明Pragma的優先順序是高於Cache-Control的。

2.Expires

有了Pragma來禁用快取,自然也需要有個東西來啟用快取和定義快取時間,對http1.0而言,Expires就是做這件事的首部欄位。 Expires的值對應一個GMT(格林尼治時間),比如Mon, 22 Jul 2002 11:12:01 GMT來告訴瀏覽器資源快取過期時間,如果還沒過該時間點則不發請求。

同樣舉個例子:

通過Fiddler給圖片資源額外加上以下頭部資訊

Expires: Fri, 11 Jun 2021 11:33:01 GMT

重新訪問該頁面會發現訪問圖片資源的時候,會直接從快取中讀取資源內容,而不發起請求。

如果Pragma頭部和Expires頭部同時存在,則起作用的會是Pragma,有興趣的同學可以自己試一下。

需要注意的是,響應報文中Expires所定義的快取時間是相對伺服器上的時間而言的,其定義的是資源“失效時刻”,如果客戶端上的時間跟伺服器上的時間不一致(特別是使用者修改了自己電腦的系統時間),那快取時間可能就沒啥意義了。

Cache-Control

針對上述的“Expires時間是相對伺服器而言,無法保證和客戶端時間統一”的問題,http1.1新增了 Cache-Control 來定義快取過期時間。注意:若報文中同時出現了 Expires 和 Cache-Control,則以 Cache-Control 為準。

也就是說優先順序從高到低分別是 Pragma -> Cache-Control -> Expires 。

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

"Cache-Control" ":" cache-directive

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

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

Cache-Control 允許自由組合可選值,例如:

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

它意味著該資源是從原伺服器上取得的,且其快取(新鮮度)的有效時間為一小時,在後續一小時內,使用者重新訪問該資源則無須傳送請求。 當然這種組合的方式也會有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。

快取校驗欄位

上述的首部欄位均能讓客戶端決定是否向伺服器傳送請求,比如設定的快取時間未過期,那麼自然直接從本地快取取資料即可(在chrome下表現為200 from cache),若快取時間過期了或資源不該直接走快取,則會發請求到伺服器去。

我們現在要說的問題是,如果客戶端向伺服器發了請求,那麼是否意味著一定要讀取回該資源的整個實體內容呢?

我們試著這麼想——客戶端上某個資源儲存的快取時間過期了,但這時候其實伺服器並沒有更新過這個資源,如果這個資源資料量很大,客戶端要求伺服器再把這個東西重新發一遍過來,是否非常浪費頻寬和時間呢?

答案是肯定的,那麼是否有辦法讓伺服器知道客戶端現在存有的快取檔案,其實跟自己所有的檔案是一致的,然後直接告訴客戶端說“這東西你直接用快取裡的就可以了,我這邊沒更新過呢,就不再傳一次過去了”。

舉例來說:

C:小服,你幾歲了?
S:小客,我18歲了。
=================================
C:小服 ,你幾歲了?我猜你18歲了。
S:靠,你知道還問我?(304)
=================================
C:小服 ,你幾歲了?我猜你18歲了。
S:小客 ,我19歲了。(200)

為了讓客戶端與伺服器之間能實現快取檔案是否更新的驗證、提升快取的複用率,Http1.1新增了幾個首部欄位來做這件事情。

1. Last-Modified

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

Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT

客戶端會為資源標記上該資訊,下次再次請求時,會把該資訊附帶在請求報文中一併帶給伺服器去做檢查,若傳遞的時間值與伺服器上該資源最終修改時間是一致的,則說明該資源沒有被修改過,直接返回304狀態碼,內容為空,這樣就節省了傳輸資料量 。如果兩個時間不一致,則伺服器會發回該資源並返回200狀態碼,和第一次請求時類似。這樣保證不向客戶端重複發出資源,也保證當伺服器有變化時,客戶端能夠得到最新的資源。一個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) 狀態碼給客戶端。 Last-Modified 存在一定問題,如果在伺服器上,一個資源被修改了,但其實際內容根本沒發生改變,會因為Last-Modified時間匹配不上而返回了整個實體給客戶端(即使客戶端快取裡有個一模一樣的資源)。

2. ETag

為了解決上述Last-Modified可能存在的不準確的問題,Http1.1還推出了 ETag 實體首部欄位。 伺服器會通過某種演算法,給資源計算得出一個唯一標誌符(比如md5標誌),在把資源響應給客戶端的時候,會在實體首部加上“ETag: 唯一識別符號”一起返回給客戶端。例如:

Etag: "5d8c72a5edda8d6a:3239"

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

如果伺服器發現ETag匹配不上,那麼直接以常規GET 200回包形式將新的資源(當然也包括了新的ETag)發給客戶端;如果ETag是一致的,則直接返回304知會客戶端直接使用本地快取即可。

那麼客戶端是如何把標記在資源上的 ETag 傳回給伺服器的呢?請求報文中有兩個首部欄位可以帶上 ETag 值:

⑴ If-None-Match: ETag-value

示例為 If-None-Match: “5d8c72a5edda8d6a:3239″ 告訴服務端如果 ETag 沒匹配上需要重發資源資料,否則直接回送304 和響應報頭即可。 當前各瀏覽器均是使用的該請求首部來向伺服器傳遞儲存的 ETag 值。

⑵ If-Match: ETag-value

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

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

快取頭部對比

頭部 優勢和特點 劣勢和問題
Expires 1、HTTP 1.0 產物,可以在HTTP 1.0和1.1中使用,簡單易用。
2、以時刻標識失效時間。
1、時間是由伺服器傳送的(UTC),如果伺服器時間和客戶端時間存在不一致,可能會出現問題。
2、存在版本問題,到期之前的修改客戶端是不可知的。
Cache-Control 1、HTTP 1.1 產物,以時間間隔標識失效時間,解決了Expires伺服器和客戶端相對時間的問題。
2、比Expires多了很多選項設定。
1、HTTP 1.1 才有的內容,不適用於HTTP 1.0 。
2、存在版本問題,到期之前的修改客戶端是不可知的。
Last-Modified 1、不存在版本問題,每次請求都會去伺服器進行校驗。伺服器對比最後修改時間如果相同則返回304,不同返回200以及資源內容。 1、只要資源修改,無論內容是否發生實質性的變化,都會將該資源返回客戶端。例如週期性重寫,這種情況下該資源包含的資料實際上一樣的。
2、以時刻作為標識,無法識別一秒內進行多次修改的情況。
3、某些伺服器不能精確的得到檔案的最後修改時間。
ETag 1、可以更加精確的判斷資源是否被修改,可以識別一秒內多次修改的情況。
2、不存在版本問題,每次請求都回去伺服器進行校驗。
1、計算ETag值需要效能損耗。
2、分散式伺服器儲存的情況下,計算ETag的演算法如果不一樣,會導致瀏覽器從一臺伺服器上獲得頁面內容後到另外一臺伺服器上進行驗證時發現ETag不匹配的情況。

使用者重新整理/訪問行為

我們可以把重新整理/訪問介面的手段分成三類:

  • 在URI輸入欄中輸入然後回車/通過書籤訪問
  • F5/點選工具欄中的重新整理按鈕/右鍵選單重新載入
  • Ctl+F5

在瀏覽器中,有時候你會發現通過不同的手段訪問/重新整理介面頁面的呈現速度是不一樣的,那麼它們到底有什麼區別呢?
以下對這三種訪問情況進行實踐與討論。

準備工作:

為了模擬第一次訪問某網站,清除相關快取內容。為了方便討論與對比,以下內容以騰訊課堂 的index.css檔案為例。

首次訪問該網頁,檢視請求與響應資訊可以看到請求頭部沒有任何關於http快取相關的資訊。而返回的HTTPresponse包含了以下頭部資訊。

Cache-Control: max-age=31104000
Expires: Thu, 20 Jul 2017 02:18:41 GMT
Last-Modified: Fri, 15 Jul 2016 04:11:51 GMT

瀏覽器會對該檔案進行快取,直到該檔案過期、使用者清空cache或者使用者強制重新整理資源時間。

1、在URI輸入欄中輸入然後回車

我們可以看到返回響應碼是 200 OK (from cache),瀏覽器發現該資源已經快取了而且沒有過期(通過Expires頭部或者Cache-Control頭部),沒有跟伺服器確認,而是直接使用了瀏覽器快取的內容。其中響應內容和之前的響應內容一模一樣,例如其中的Date時間是上一次響應的時間。

所以我們也能看到該資源的Size為from cache

2、F5/點選工具欄中的重新整理按鈕/右鍵選單重新載入

F5的作用和直接在URI輸入欄中輸入然後回車是不一樣的,F5會讓瀏覽器無論如何都發一個HTTP Request給Server,即使先前的響應中有Expires頭部。所以,當我在當前 騰訊課堂 網頁中按F5的時候,瀏覽器會傳送一個HTTP Request給Server,但是包含這樣的Headers:

Cache-Control: max-age=0
If-Modified-Since: Fri, 15 Jul 2016 04:11:51 GMT

其中Cache-Control是Chrome強制加上的,而If-Modified-Since是因為獲取該資源的時候包含了Last-Modified頭部,瀏覽器會使用If-Modified-Since頭部資訊重新傳送該時間以確認資源是否需要重新傳送。 實際上Server沒有修改這個index.css檔案,所以返回了一個304(Not Modified),這樣的響應資訊很小,所消耗的route-trip不多,網頁很快就重新整理了。

上面的例子中沒有ETag,如果Response中包含ETag,F5引發的Http Request中也是會包含If-None-Match的。

3、Ctl+F5

那麼Ctrl+F5呢? Ctrl+F5要的是徹底的從Server拿一份新的資源過來,所以不光要傳送HTTP request給Server,而且這個請求裡面連If-Modified-Since/If-None-Match都沒有,這樣就逼著Server不能返回304,而是把整個資源原原本本地返回一份,這樣,Ctrl+F5引發的傳輸時間變長了,自然網頁Refresh的也慢一些。我們可以看到該操作返回了200,並重新整理了相關的快取控制時間。

實際上,為了保證拿到的是從Server上最新的,Ctrl+F5不只是去掉了If-Modified-Since/If-None-Match,還需要新增一些HTTP Headers。按照HTTP/1.1協議,Cache不光只是存在Browser終端,從Browser到Server之間的中間節點(比如Proxy)也可能扮演Cache的作用,為了防止獲得的只是這些中間節點的Cache,需要告訴他們,別用自己的Cache敷衍我,往Upstream的節點要一個最新的copy吧。

在Chrome 51 中會包含兩個頭部資訊, 作用就是讓中間的Cache對這個請求失效,這樣返回的絕對是新鮮的資源。

Cache-Control: no-cache
Pragma: no-cache

快取實踐

綜上對各種HTTP快取控制頭部的對比以及使用者可能出現的瀏覽器重新整理行為的討論,當我們在一個專案上做http快取的應用時,我們實際上還是會把上述提及的大多數首部欄位均使用上。

1、Expires / Cache-Control

Expires用時刻來標識失效時間,不免收到時間同步的影響,而Cache-Control使用時間間隔很好的解決了這個問題。 但是 Cache-Control 是 HTTP1.1 才有的,不適用於 HTTP1.0,而 Expires 既適用於 HTTP1.0,也適用於 HTTP1.1,所以說在大多數情況下同時傳送這兩個頭會是一個更好的選擇,當客戶端兩種頭都能解析的時候,會優先使用 Cache-Control。

2、Last-Modified / ETag

二者都是通過某個標識值來請求資源, 如果伺服器端的資源沒有變化,則自動返回 HTTP 304 (Not Changed)狀態碼,內容為空,這樣就節省了傳輸資料量。而當資源發生比那話後,返回和第一次請求時類似。從而保證不向客戶端重複發出資源,也保證當伺服器有變化時,客戶端能夠得到最新的資源。

其中Last-Modified使用檔案最後修改作為檔案標識值,它無法處理檔案一秒內多次修改的情況,而且只要檔案修改了哪怕檔案實質內容沒有修改,也會重新返回資源內容;ETag作為“被請求變數的實體值”,其完全可以解決Last-Modified頭部的問題,但是其計算過程需要耗費伺服器資源。

3、from-cache / 304

Expires和Cache-Control都有一個問題就是服務端作為的修改,如果還在快取時效裡,那麼客戶端是不會去請求服務端資源的(非重新整理),這就存在一個資源版本不符的問題,而強制重新整理一定會發起HTTP請求並返回資源內容,無論該內容在這段時間內是否修改過;而Last-Modified和Etag每次請求資源都會發起請求,哪怕是很久都不會有修改的資源,都至少有一次請求響應的消耗。
對於所有可快取資源,指定一個Expires或Cache-Control max-age以及一個Last-Modified或ETag至關重要。同時使用前者和後者可以很好的相互適應。

前者不需要每次都發起一次請求來校驗資源時效性,後者保證當資源未出現修改的時候不需要重新傳送該資源。而在使用者的不同重新整理頁面行為中,二者的結合也能很好的利用HTTP快取控制特性,無論是在位址列輸入URI然後輸入回車進行訪問,還是點選重新整理按鈕,瀏覽器都能充分利用快取內容,避免進行不必要的請求與資料傳輸。

4、避免304

同學們是否還記得我們在討論使用者重新整理頁面行為中體積的index.css檔案,它實際上被命名為index.03D344bd.css。而細心的同學也會發現它的Expires和Cache-Control時間出奇的長,這難道不會導致使用者無法得到其最近的內容嗎?

其做法實際上很簡單,它把服務側ETag的那一套理論搬到了前端來使用。 頁面的靜態資源以版本形式釋出,常用的方法是在檔名或引數帶上一串md5或時間標記符:

https://hm.baidu.com/hm.js?e23800c454aa573c0ccb16b52665ac26

http://tb1.bdstatic.com/tb/_/tbean_safe_ajax_94e7ca2.js


http://img1.gtimg.com/ninja/2/2016/04/ninja145972803357449.jpg

可以看到上面的例子中有不同的做法,有的在URI後面加上了md5引數,有的將md5值作為檔名的一部分,有的將資源放在特性版本的目錄中。

那麼在檔案沒有變動的時候,瀏覽器不用發起請求直接可以使用快取檔案;而在檔案有變化的時候,由於檔案版本號的變更,導致檔名變化,請求的url變了,自然檔案就更新了。這樣能確保客戶端能及時從伺服器收取到新修改的檔案。通過這樣的處理,增長了靜態資源,特別是圖片資源的快取時間,避免該資源很快過期,客戶端頻繁向服務端發起資源請求,伺服器再返回304響應的情況(有Last-Modified/Etag)。

結論:

  • 需要相容HTTP1.0的時候需要使用Expires,不然可以考慮直接使用Cache-Control
  • 需要處理一秒內多次修改的情況,或者其他Last-Modified處理不了的情況,才使用ETag,否則使用Last-Modified。
  • 對於所有可快取資源,需要指定一個Expires或Cache-Control,同時指定Last-Modified或者Etag。
  • 可以通過標識檔案版本名、加長快取時間的方式來減少304響應。

相關文章