不管是軟件應用仍是硬件應用,緩存都扮演着重要的角色,其對提高性能的重要性不容置疑。html
本文主要介紹 HTTP 緩存,涉及其原理和應用。HTTP 緩存主要經過 HTTP 首部來控制。web
先看一個簡單的緩存示例:瀏覽器
app.js
時,服務器會返回資源內容和相關頭部,其中 Cache-Control: max-age=120
告訴瀏覽器說,這個資源的緩存有效期爲 120 秒,從當前時間 Date: Mon, 05 Mar 2018 08:00:00 GMT
開始算起。瀏覽器收到資源後便將 app.js
及其相應頭部存儲在本地。05 Mar, 2018 08:02:00 GMT
以前再次請求 app.js
,則瀏覽器會直接使用存儲在本地的資源,而不用再次向服務器發起請求。這個過程當中,咱們就說 app.js
被緩存且命中了。緩存
在進一步理解緩存以前,先看下跟緩存相關的幾個概念:服務器
HTTP 緩存涉及到請求-響應鏈上的多個角色,包括客戶端(本文指瀏覽器)、代理和服務器。
其中,瀏覽器自身也實現了緩存功能。瀏覽器在請求資源時,老是先從本地緩存中查找,若是找到未過時資源,則直接使用,不然向服務器發起請求。
代理也是服務器的一種,但通常狀況下不會把它單獨抽出來分析,只有在跟它有關的地方會把它區分於源服務器。因此,下文的示例圖中將不會把它列進去。app
HTTP 緩存的理解基本上能夠總結爲三個問題:性能
問題 1 說明存儲緩存數據的設備是多樣的,能夠存儲於各級代理服務器,也能夠存儲於瀏覽器本地。ui
問題 2 說明使用什麼辦法來判斷緩存數據是否已通過期,固然是比較時間啦,那麼如何比較呢?google
問題 3 說明緩存雖然過時了,可是其內容仍然可能與服務端一致,這時就不必從新下載相同數據,只須要向服務端詢問下是否能夠繼續使用緩存便可。spa
帶着上面三個問題去理解 HTTP 緩存頭部設置會更有助於理解和記憶。
有人根據是否須要進行問題 3 中的從新驗證把緩存策略的設置分爲強緩存和協商緩存,強緩存無須再次驗證的緩存策略,協商緩存是須要再次驗證的緩存策略。
二者的區別在於,協商緩存多發起了一次 HTTP 請求。
HTTP 緩存主要經過 HTTP 首部來實現緩存控制。這些與緩存相關的 HTTP 首部這裏統稱爲緩存首部,具體首部以下表所示。
首部字段 | 首次定義 | 首部類型 |
---|---|---|
Pragma | HTTP/1.0 | 通用首部 |
Age | HTTP/1.1 | 響應首部 |
Expires | HTTP/1.0 | 實體首部 |
Cache-Control | HTTP/1.1 | 通用首部 |
Etag | HTTP/1.1 | 響應首部 |
If-Match | HTTP/1.1 | 請求首部 |
If-None-Match | HTTP/1.1 | 請求首部 |
If-Modified-Since | HTTP/1.0 | 請求首部 |
Last-Modified | HTTP/1.0 | 實體首部 |
其中,「首次定義」是指首次出如今哪一個 HTTP 版本。之因此列出這項內容,是由於實際應用須要考慮兼容舊版 HTTP 。
現代的 HTTP 緩存策略主要使用 Cache-Control
實現,它是目前最新的緩存首部,用於取代較老的緩存首部如 Pragma
、Expires
等。因此應用中應該傾向於使用 Cache-Control
。可是爲了支持只實現了 HTTP/1.0 的客戶端設備,服務端一般仍是都會同時設置 Expires
、Pragma
和 Cache-Control
等,此時 Cache-Control
會有更高的優先級。提醒一下,現代瀏覽器都已支持 Cache-Control
。
Cache-Control
是通用首部,這意味着它既能夠出如今請求中,也能夠出如今響應中。
Cache-Control
的值可由多個字段組合而成,以逗號分隔,如 Cache-Control: private,max-age=3600
。下面對經常使用的可取字段進行說明。
public
: 表示當前響應數據全部用戶共享的,能夠被任何設備緩存,包括客戶端、代理服務器等。
private
: 表示當前響應數據是單個用戶所獨佔的,只能被客戶端緩存,不能被代理服務器緩存。
max-age=<seconds>
: 指定緩存的有效時間,單位爲秒。其值是任意整數,0 和負數表示緩存過時,正數值加上當前響應頭中的 Date
首部值即爲過時時間。
max-stale[=<seconds>]
: 只用於請求,表示客戶端仍然願意接受過時緩存,只要過時時間沒超過指定時間,若是未指定時間,則表示任何過時的時間。
min-fresh=<seconds>
: 只用於請求,表示客戶端願意接受還剩餘多少秒過時的緩存。
s-maxage=<seconds>
: 功能與 max-age
一致,但它僅做用於共享緩存,對私有緩存無效。
no-cache
: 並不是字面意思,它並不是禁止緩存,而是強制在使用已緩存數據以前,須要去服務端驗證一下是否可使用緩存數據。
no-store
: 真正的禁止緩存,任何設備都不容許緩存,每次請求都須要向服務端從新獲取數據。
no-transform
: 表示響應的實體數據不該被轉換。Content-Encoding
、Content-Range
和 Content-Type
首部也不能被修改。實際應用中,有些代理服務器會對圖片資源進行格式轉換以節省空間或者帶寬。
做爲通用首部,其部分指令值能夠出如今請求首部,也能夠出如今響應首部,二者可能略有區別:
指令值 | 請求 | 響應 |
---|---|---|
public |
- | 可共享數據,可被任何設備緩存 |
private |
- | 用戶私有數據,只能被客戶端緩存 |
no-cache |
使用前需驗證 | 使用前需驗證 |
no-store |
禁止使用緩存數據 | 禁止緩存 |
max-age |
要求資源的 age 小於這個時間 | 最大過時時間 |
min-fresh |
要求資源至少還剩餘多少過時時間 | - |
max-stale |
超過過時時間多少秒內仍願意接受 | - |
no-transform |
不要轉換格式 | 不要轉換格式 |
這些指令用在請求首部的狀況比較少見,最可能接觸的地方是 Chrome DevTools
中的 Network
標籤頁。
其中,有個 Disable cache
選項,選中後 DevTools
會自動給全部請求頭部加上 Cache-Control: no-cache
首部,以告訴瀏覽器和代理使用本地緩存以前必須先驗證。
If-Modified-Since
首部比較的是資源的修改時間,精度爲秒,是一種緩存過時後的經常使用驗證方式。通常來講,驗證資源是否修改過,對比資源的修改時間是一種最簡單的辦法。
使用過程以下:
app.js
時,服務器響應帶上 Last-Modified
首部,告訴客戶端當前資源的最後修改時間。客戶端根據 Cache-Control: max-age=120
,把 app.js
和響應首部緩存起來。app.js
時,把以前保存的 Last-Modified
時間放入 If-Modified-Since
首部發給服務器。服務器發現資源的 Last-Modified
時間沒有發生改變,因而直接響應 304 。客戶端收到 304 後,直接使用緩存的 app.js
,同時更新緩存有效期。app.js
時,把以前保存的 Last-Modified
時間放入 If-Modified-Since
首部發給服務器。服務器發現資源的 Last-Modified
時間已經發生改變,因而響應 200 ,將修改後的 app.js
和新的 Last-Modified
發送給客戶端。客戶端收到 200 後,從新下載新的 app.js
,並把新的 app.js
和響應首部緩存起來,替換原先的舊緩存。ETag
叫實體標籤(Entity Tag),用於表示實體資源是否發生變化,其生成原理相似 MD5 ,也是一種用於驗證的首部。當響應的首部信息或者消息實體發生變化時,實體標籤也會改變。
使用過程以下:
app.js
時,服務器響應帶上 ETag
首部,告訴客戶端當前資源的實體標籤。客戶端根據 Cache-Control: max-age=120
,把 app.js
和響應首部緩存起來。app.js
時,把以前保存的 ETag
值放入 If-None-Match
首部發給服務器。服務器發現本身的資源 ETag
值並沒有發生改變,因而直接響應 304 。客戶端收到 304 後,直接使用緩存的 app.js
,同時更新緩存有效期。app.js
時,把以前保存的 ETag
值放入 If-None-Match
首部發給服務器。服務器發現本身的資源 ETag
值已經發生改變,因而響應 200 ,將修改後的 app.js
和新的 ETag
發送給客戶端。客戶端收到 200 後,從新下載新的 app.js
,並把新的 app.js
和響應首部緩存起來,替換原先的舊緩存。當客戶端本地存儲有多個版本的資源時,會把全部的實體標籤都上傳,形如 ETag: "abc","def"
,服務端會使用 ETag
首部返回匹配中的實體標籤值。
實體標籤分爲強標籤(Strong ETag)和弱標籤(Weak ETag),弱標籤以 W/
開頭,如 ETag: W/"1234"
。強標籤使用強比較,弱標籤使用弱比較。強比較意味着兩個比較對象的每個字節都相同,弱比較意味着二者語義相同(Semantic Equivalence)。舉個栗子,假如響應首部包含一個渲染時間 Rendered-Time
,A 響應的渲染時間爲 365,B 響應的渲染時間爲 345,兩個響應的實體內容一致。這種狀況下,咱們能夠說 A 和 B 弱比較相等,強比較不相等。
通常來講,靜態內容使用強標籤,動態生成的內容使用弱標籤。
由此能夠看出,實體首部能夠解決一些 Last-Modified
沒法解決的問題:
If-Match
和 ETag
的另外一種用法:避免「空中碰撞」,以防編輯衝突。當客戶端使用 PUT 或者 POST 更新服務端資源時,須要使用 If-Match
來攜帶實體標籤給服務端,以確保客戶端要修改的資源沒有被別人修改過,避免覆蓋別人的修改。不過這種用法比較少,能夠不用深究。
Expires
指明資源的過時時間,如 Expires: Wed, 04 Jul 2012 08:26:05 GMT
。非法的日期格式(如 0)將會被當作過去的時間,表示該資源已通過期。
若是 Expires
和 Cache-Control
的 max-age
或者 s-maxage
同時出現,Expires
將被忽略。
Age
表示資源在代理服務器上已經緩存了多久時間,單位爲秒。若是是 Age: 0
,代表該資源剛剛從服務器獲取。它的計算方式通常使用代理服務器當前的時間減去緩存資源的 Date
時間。
Pragma
是 HTTP/1.0 中引入的首部,如今使用時通常用於向後兼容 HTTP/1.0,不鼓勵使用。
Pragma: no-cache
的做用與 Cache-Control: no-cache
一致,表示須要跟服務器進行驗證後才能使用緩存資源。
並非每一個服務器都會返回明確的緩存策略,這種狀況下客戶端會採起啓發式緩存策略。注意,只有在服務端沒有返回明確的緩存策略時纔會激活啓發式緩存策略。
啓發式緩存策略會根據其餘的首部信息來計算一個過時時間,其餘的首部一般是 Date
和 Last-Modified
。此時,緩存有效期通常取二者差值的 10% 。
使用啓發式緩存策略時,若是超過當前時間 24 小時且從未警告過,瀏覽器或者代理服務器應該在響應中產生一個警告首部字段 Warning: 113
。