1. 程式人生 > >HTTP緩存控制小結

HTTP緩存控制小結

charset 分布式 設置 點擊 .cn ron 不能 dst 無法

引言

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

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

  • 與緩存相關的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 時代,給客戶端設定緩存方式可通過兩個字段—— Pragma 和 Expires 來規範。雖然這兩個字段早可拋棄,但為了做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響應。

HTTP緩存控制小結