簡介

為了提高網站的訪問速度和效率,我們需要設計各種各樣的快取,通過快取可以避免不必要的額外資料傳輸和請求,從而提升網站的請求速度。對於HTTP協議來說,本身就自帶有HTTP快取。

今天我們就深入探討一下HTTP中的快取機制和使用。

HTTP中的快取種類

快取就是將請求的資源在本地儲存一份拷貝,從而在下一次請求的時候,直接返回該拷貝,不用再從伺服器下載資源,從而減少了資源的傳輸提升了效率。

除了直接訪問和返回資源之外,HTTP中的快取可以分成兩類,一種是共享cache,也就是說不同的客戶端都可以從該共享cache中獲取資源,並且這些資源是多個客戶端可以訪問的。還有一種是私有cache,這意味著該cache只能使用者或者客戶端私有訪問,其他使用者是無權訪問的。

私有cache很好理解,我們常用的瀏覽器中的cache基本上就是私有cache,這些cache是瀏覽器獨有的,並不會共享給其他的瀏覽器。

共享cache主要用在一些web代理上,比如web代理伺服器,因為web代理伺服器可能會為眾多的使用者提供資源服務,對於這些使用者共同訪問的資源就不必要每個使用者儲存一份了,只需要在web代理伺服器中儲存一份即可,這樣可以減少資源的無效拷貝。

HTTP中快取響應的狀態

對於HTTP快取來說,一般快取的是GET請求,因為GET請求除了URI之外,並沒有其他多餘的引數,並且其表示的意義是從伺服器獲取資源。

不同的GET請求,會返回不同的狀態碼。

如果是成功返回資源,則會返回200表示OK。

如果是重定向,則返回301。如果是異常,則返回404。如果是不完全的結果,則會返回206。

HTTP中的快取控制

HTTP中的快取控制是通過HTTP頭來表示的。在HTTP1.1中加入了Cache-Control,我們可以通過Cache-Control來控制請求和響應的快取情況。

如果不需要快取,則使用:

Cache-Control: no-store

如果需要對客戶端的快取進行驗證,則使用:

Cache-Control: no-cache

如果要強制進行驗證,則可以使用:

Cache-Control: must-revalidate

在這種情況下,過期的資源將不會被允許使用。

對於伺服器來說,可以通過Cache-Control來控制快取是private或者public的:

Cache-Control: private
Cache-Control: public

還有一個非常重要的快取控制就是過期時間:

Cache-Control: max-age=31536000

通過設定max-age,可以覆蓋Expires頭,表示在這個時間區間範圍之類,該資源可以看做是最新的,不需要重新從伺服器獲取。

Cache-Control是HTTP1.1中定義的header欄位,在HTTP1.0中也有一個類似的欄位叫做Pragma。通過設定 Pragma: no-cache可以得到類似Cache-Control: no-cache的效果。也就是強制客戶端重新提交快取到伺服器端進行校驗。

但是對於伺服器端的響應來說,並不包含Pragma,所以Pragma並不能完全替代Cache-Control。

快取重新整理

快取存放在客戶端之後,就可以在請求的時候被使用了。但是為了安全起見,我們需要給快取設定一個過期時間,只有在過期時間之前的時間範圍,快取才是有效的,如果超過了過期時間,則需要從伺服器重新獲取。

這樣的機制能夠保證客戶端獲取到的資源始終是最新的。並且能夠保證伺服器端對資源的更新能夠及時到達客戶端。

如果客戶端的資源在過期時間之類,那麼這個資源的狀態就是fresh,否則資源的狀態就是stale。

如果資源是stale狀態的,該資源並不會立即從客戶端清理出去,而是在下一次的請求中,向伺服器傳送一個If-None-Match的請求,判斷該資源在伺服器端是否仍然是fresh狀態的,如果該資源並沒有發生變化,則返回304 (Not Modified),表示該資源仍然有效。

而這個fresh的持續時間就是通過"Cache-Control: max-age=N" 來判斷的。

如果響應中並沒有這個頭,則會去判斷 Expires header 是否存在,如果存在那麼fresh的時間就可以使用Expires - Date 來進行計算。

如果響應中連Expires header都沒有,那麼怎麼去判斷資源的fresh時間呢?

這種情況下會去查詢Last-Modified header,如果這個header存在的話,那麼fresh時間就是(Date - Last-modified )/ 10 。

revving

為了提升HTTP請求的效率,我們當然希望快取時間越長越好,但是前面我們也提到了,快取時間過長會導致伺服器資源更新困難的問題。怎麼解決呢?

對於那些不經常更新的檔案,請求他們的URL可以由檔名+版本號來決定。同一個版本號表示該資源內容是固定不變的,我們可以對其快取一個非常長的時間。

當伺服器資源內容發生變化之後,只需要在請求的時候更新版本號即可。

雖然這樣的操作會造成伺服器資源的修改同時要修改客戶端請求的版本,但是在現代前端打包工具的幫助下,這並不是一個很大的問題。

快取校驗

當快取的資源過期之後,有兩種處理方式,一種是重新從伺服器請求資源,一種是對快取資源進行再次校驗。

當然再次校驗需要伺服器的支援,並需要設定"Cache-Control: must-revalidate"請求頭。

那麼客戶端怎麼去校驗資源是否有效呢?很明顯我們不能把資源從客戶端傳送到伺服器端進行校驗,這樣的操作方式太過複雜,並且在檔案比較大的請求下,會造成資源的浪費。

我們很容易想到的一種方法是對資原始檔進行hash執行,只要傳送這個hash運算的結果進行對比即可。

當然,在HTTP中,提供了一個ETags header,這header可以看做是資源的唯一標記,用來在客戶端和伺服器端進行校驗。這樣客戶端就可以請求一個If-None-Match,讓伺服器判斷該資源是否match。這種判斷被稱為強校驗。

還有一種弱校驗的方式,如果響應中帶有Last-Modified,則客戶端可以請求一個If-Modified-Since,來向伺服器詢問該檔案是否發生了變化。

對於伺服器端來說,它可以選擇是否進行檔案的校驗,如果不進行校驗,則可以直接返回一個200 OK狀態碼,並直接返回資源。如果進行校驗,則返回一個304 Not Modified,表示客戶端可以繼續使用快取的資源,同時還可返回一些其他的header欄位,比如更新快取的過期時間等。

Vary響應

在伺服器響應的時候,可以帶上Vary header。這個Vary header的值是響應頭中的某個key,比如Content-Encoding,表示對某個encoding的資源進行快取。

比如客戶端首先請求:

GET /resource HTTP/1.1
Accept-Encoding: *

伺服器端返回:

HTTP/1.1 200 OK
Content-Encoding: gzip
Vary: Content-Encoding

則將會把資源和gzip型別的Content-Encoding一起快取起來。

當客戶再次請求:

GET /resource HTTP/1.1
Accept-Encoding: br

因為當前快取的資源encoding方式是gzip,和客戶端接受的encoding方式並不一樣,所以重新需要從伺服器獲取:

HTTP/1.1 200 OK
Content-Encoding: br
Vary: Content-Encoding

這時候,客戶端又快取了一個br格式的資源。

下次客戶端再次請求br型別的資源,就可以命中快取了。

總結一下,Vary的意思是將資源再通過其他的型別比如encoding進行區分和快取。

但是這樣也會造成資源重複儲存的問題,同一個資源因為編碼格式的不同被快取了很多份。為了解決這個問題,就需要對資源請求進行標準化。

所謂標準化,就是在請求之前對請求的encoding方式進行校驗,只選擇其中的一種編碼方式進行請求,從而避免資源多次快取的情況。

總結

到此,HTTP快取就介紹完畢了,大家可以在實際的應用中對HTTP快取加深理解。

本文已收錄於 http://www.flydean.com/04-http-cache/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!