Http實(shí)戰(zhàn)之緩存、重定向
Http緩存
關(guān)于Http緩存這部分內(nèi)容在網(wǎng)上查閱資料時(shí),發(fā)現(xiàn)很多文章將其分為*強(qiáng)制緩存、協(xié)商緩存或者對(duì)比緩存*,但筆者在RFC文檔中并沒有找到相關(guān)詞匯,所以本文并不會(huì)采用上述分類,而是以RFC文件及《Http權(quán)威指南》中的內(nèi)容為準(zhǔn)。除此之外還需要明確的一點(diǎn):「本文并不討論【代理緩存】,只分析客戶端本身進(jìn)行緩存的情況」。
RFC文檔:https://datatracker.ietf.org/doc/html/rfc2616#section-13,中主要涉及到的Http緩存相關(guān)的內(nèi)容如下:

緩存處理步驟

整個(gè)流程比較簡(jiǎn)單,可能需要解釋的有兩個(gè)名詞
判斷是否新鮮,也就是 新鮮度檢測(cè),可以理解為檢查緩存是否已經(jīng)過期服務(wù)器再驗(yàn)證,在確認(rèn)了緩存已經(jīng)過期的情況我們還需要到服務(wù)器去確認(rèn)過期的緩存是否還有效,如果仍然有效的話此時(shí)我們需要將客戶端的緩存重新生效,這個(gè)過程稱之為 再驗(yàn)證(revalidation)。
關(guān)于第二點(diǎn)小伙伴們可能會(huì)有疑問:
為什么確認(rèn)緩存已經(jīng)過期了還需要去服務(wù)端驗(yàn)證呢,緩存過期不應(yīng)該直接請(qǐng)求服務(wù)器返回最新數(shù)據(jù)嗎?
再驗(yàn)證的話多了一次驗(yàn)證過程不是增加了網(wǎng)絡(luò)開銷了嗎?
第一個(gè)問題,答:
「緩存過期并不意味著緩存中的數(shù)據(jù)跟服務(wù)器保存的數(shù)據(jù)不一致」,例如服務(wù)器通過Cache-Control 告訴了客戶端緩存有效期為兩小時(shí),但在接下來的兩小時(shí)內(nèi)服務(wù)器上的這份數(shù)據(jù)并沒有任何寫入操作,也就是說雖然客戶端在檢測(cè)的時(shí)候數(shù)據(jù)已經(jīng)過期了,但是客戶端此時(shí)緩存的數(shù)據(jù)仍然跟服務(wù)器保存的最新數(shù)據(jù)是一致的,此時(shí)也就沒必要讓服務(wù)重新發(fā)送一份客戶端已經(jīng)緩存的數(shù)據(jù),我們只需要服務(wù)器通過某種機(jī)制告訴客戶端緩存是可用的即可。
第二個(gè)問題,答:
「緩存的再校驗(yàn)跟向服務(wù)器請(qǐng)求最新數(shù)據(jù)往往會(huì)被合并成一個(gè)請(qǐng)求」。我們可以在發(fā)送請(qǐng)求時(shí)附加一些用于驗(yàn)證的頭信息,比如我們可以給緩存的實(shí)體打上一個(gè)標(biāo)簽,每次向服務(wù)器發(fā)送請(qǐng)求時(shí)攜帶上這個(gè)標(biāo)簽,當(dāng)進(jìn)行再驗(yàn)證時(shí)服務(wù)器校驗(yàn)客戶端當(dāng)前記錄的數(shù)據(jù)標(biāo)簽是否跟自身保存的一致,如果一致告訴服務(wù)器緩存是可用的(304響應(yīng)碼),如果不一致則返回最新數(shù)據(jù)及最新標(biāo)簽。如下:

通過前文所述相信大家對(duì)Http的緩存機(jī)制有了一個(gè)大概的了解,那么接下來我們就開始分析其中的細(xì)節(jié):「如何完成新鮮度檢測(cè)及再驗(yàn)證?」
新鮮度檢測(cè)
所謂新鮮度檢測(cè)實(shí)際就是檢測(cè)文檔是否過期,服務(wù)器可以通過Cache-Control首部和 Expires首部指定返回?cái)?shù)據(jù)的有效期,Expires是HTTP1.0的規(guī)范,使用的是決定時(shí)間,如下:
?Expires: Wed, 21 Oct 2022 07:28:00 GMT
?
Cache-Control是HTTP1.1的規(guī)范,搭配max-age并使用相對(duì)時(shí)間,如下:
?Cache-Control: max-age=30(單位為秒)
?
只要在有效期內(nèi),客戶端即可認(rèn)為此時(shí)的緩存數(shù)據(jù)是新鮮的,無須向服務(wù)器發(fā)送請(qǐng)求
再驗(yàn)證
如果客戶端檢測(cè)到此時(shí)緩存已經(jīng)過期,那么需要向服務(wù)器發(fā)起再驗(yàn)證,一個(gè)具有再驗(yàn)證功能的請(qǐng)求跟普通的請(qǐng)求唯一的區(qū)別在于請(qǐng)求頭中多了一些用于校驗(yàn)的字段,如下:
?參考:https://datatracker.ietf.org/doc/html/rfc7232
?
| 字段名 | 描述 | 備注 |
|---|---|---|
| If-Modified-Since | 如果從指定日期之后數(shù)據(jù)「【被修改】」過了則「驗(yàn)證」失敗,需要向服務(wù)器發(fā)送請(qǐng)求獲取最新數(shù)據(jù),如果驗(yàn)證成功,服務(wù)端返回「「304(Not Modified)」」 | 通過日期校驗(yàn),通常用于緩存再校驗(yàn),一般會(huì)結(jié)合響應(yīng)頭中的Last-Modified使用 |
| If-None-Match | 如果緩存中數(shù)據(jù)的標(biāo)簽跟服務(wù)器數(shù)據(jù)的標(biāo)簽不匹配則驗(yàn)證失敗,需要向服務(wù)器發(fā)送請(qǐng)求獲取最新數(shù)據(jù),與Etag 服務(wù)器響應(yīng)首部配合使用,如果驗(yàn)證成功,服務(wù)端返回「「304(Not Modified)」」 | 通過唯一標(biāo)識(shí)進(jìn)行校驗(yàn),通常用于緩存再校驗(yàn) |
「If-Modified-Since」再驗(yàn)證工作過程如下

客戶端在第一次緩存時(shí)同時(shí)也記錄了服務(wù)器返回的Last-Modified,再后續(xù)發(fā)現(xiàn)緩存過期時(shí)會(huì)向服務(wù)器發(fā)送一個(gè)再驗(yàn)證請(qǐng)求,在請(qǐng)求頭中添加一個(gè)If-Modified-Since字段,其值為Last-Modified的值,服務(wù)器在收到此請(qǐng)求后,先判斷在指定時(shí)間后數(shù)據(jù)是否發(fā)生了變化,如果沒有變化則返回「「304(Not Modified)」」,否則返回200狀態(tài)碼及最新數(shù)據(jù)。
「If-None-Match」:實(shí)體標(biāo)簽再驗(yàn)證

整個(gè)校驗(yàn)過程跟「If-Modified-Since」是一致的,唯一的區(qū)別在于「If-Modified-Since」校驗(yàn)的是日志,而「If-None-Match」校驗(yàn)的是數(shù)據(jù)對(duì)應(yīng)的唯一標(biāo)簽。如果 緩存數(shù)據(jù)中同時(shí)有 Etag 和 Last-Modified 字段的時(shí)候, Etag 的優(yōu)先級(jí)更高,也就是先會(huì)判斷 Etag 是否變化了,如果 Etag 沒有變化,然后再看 Last-Modified。
這里需要說明一下,除了上面提到的兩個(gè)頭部字段外,Http中還定義了一些其它的帶有校驗(yàn)含義的header,如下:
| 字段名 | 描述 | 備注 |
|---|---|---|
| If-Match | 與Etag 服務(wù)器響應(yīng)首部配合使用,校驗(yàn)失敗返回「「412(Precondition Failed)」」 | 并不用于緩存相關(guān)操作,而是用于避免錯(cuò)誤的更新操作(PUT、POST、DELETE),只有在滿足條件的情況下才允許更新,通常用于多人協(xié)作更新同一份數(shù)據(jù)時(shí) |
| If-Unmodified-Since | 如果從指定日期之后數(shù)據(jù)「【未被修改】」則「驗(yàn)證」成功。驗(yàn)證失敗時(shí)服務(wù)端需要返回「「412(Precondition Failed)」」 | 跟If-Match一樣能避免錯(cuò)誤的更新操作,不同的是If-Match比較的是標(biāo)簽而If-Unmodified-Since比較的是日期。另外在進(jìn)行部分文件的傳輸時(shí),獲取文件的其余部分之前,要確保文件未發(fā)生變化,此時(shí)這個(gè)首部是非常有用的。例如在端點(diǎn)續(xù)傳的場(chǎng)景下,需要保證服務(wù)端已經(jīng)傳送到客戶端的資源沒有發(fā)生變化。 |
| If-Range | 支持對(duì)不完整文檔的緩存,會(huì)搭配服務(wù)器響應(yīng)中的Last-Modified或者ETag使用,驗(yàn)證失敗時(shí)服務(wù)端需要返回「「412(Precondition Failed)」」 | 主要用于范圍請(qǐng)求或斷點(diǎn)續(xù)傳 |
讀者朋友們需要注意的是,雖然If-Match、If-Unmodified-Since看起來是If-None-Match跟If-Modified-Since的反義詞,但在HTPP協(xié)議中定義的語義是完全不一樣的。具體可以參考:https://datatracker.ietf.org/doc/html/rfc7232#page-17
緩存控制
關(guān)于緩存控制我們可以分為兩部分討論
服務(wù)端如何進(jìn)行緩存控制 客戶端如何進(jìn)行緩存控制
服務(wù)端控制
服務(wù)端進(jìn)行緩存控制主要依賴Cache-Control、及Expires請(qǐng)求頭,其中Expires已經(jīng)不推薦使用。其優(yōu)先級(jí)如下所示:
附加一個(gè) Cache-Control: no-store首部到響應(yīng)中去;附加一個(gè) Cache-Control: no-cache首部到響應(yīng)中去;附加一個(gè) Cache-Control: must-revalidate首部到響應(yīng)中去;附加一個(gè) Cache-Control: max-age首部到響應(yīng)中去;附加一個(gè) Expires日期首部到響應(yīng)中去;
Cache-Control: no-store,客戶端禁止使用緩存
Cache-Control: no-cache,客戶端可以進(jìn)行緩存,但每次使用緩存時(shí)必須跟服務(wù)器進(jìn)行「再驗(yàn)證」
Cache-Control: must-revalidate ,客戶端可以進(jìn)行緩存,在「緩存過期后」必須進(jìn)行「再驗(yàn)證」,跟no-cache的區(qū)別在于must-revalidate強(qiáng)調(diào)的是緩存過期后的行為,因?yàn)樵谀承┣闆r下為了提升效率客戶端會(huì)使用已經(jīng)過期的緩存,如果服務(wù)端指定了Cache-Control: must-revalidate ,那么緩存過期后不能直接使用,必須進(jìn)行再驗(yàn)證。no-cache不論緩存是否過期都需要客戶端發(fā)起再驗(yàn)證。
Cache-Control: max-age ,指明了緩存的有效期,是一個(gè)相對(duì)時(shí)間,單位為秒
Expires ,指明了緩存的有效,是一個(gè)決定時(shí)間。HTTP 設(shè)計(jì)者后來認(rèn)為,由于很多服務(wù)器的時(shí)鐘都不同步,或者不正確,所以最好還是用剩余秒 數(shù),而不是絕對(duì)時(shí)間來表示過期時(shí)間,已經(jīng)不推薦使用。
客戶端控制
上面我們介紹了,服務(wù)器端如何在響應(yīng)頭中添加響應(yīng)的字段來瀏覽來是否可以使用緩存,同樣,客戶端自己也可以控制,以瀏覽器為例,這里我們主要說三個(gè)場(chǎng)景:
瀏覽器刷新
即我們按F5刷新頁面的時(shí)候,該頁面的http請(qǐng)求中會(huì)添加:Cache-Control:max-age:0, 即說明緩存直接失效啦,就不走緩存了,直接從服務(wù)器端讀取數(shù)據(jù)。
瀏覽器強(qiáng)制刷新
即我們按ctrl+f5強(qiáng)制刷新頁面的時(shí)候,該頁面的http請(qǐng)求會(huì)添加:Cache-Control:no-cache; 即表示此時(shí)要首先去服務(wù)器端驗(yàn)證資源是否有更新,如果有更新則直接返回最新資源,如果沒有更新,則返回304,然后瀏覽器端判斷是304的話,則從緩存中讀取數(shù)據(jù)。
瀏覽器前進(jìn)后退重定向
當(dāng)我們點(diǎn)擊瀏覽器的前進(jìn)后退操作時(shí),這個(gè)時(shí)候請(qǐng)求中不會(huì)有Cache-Control的字段,沒有該字段,就表示會(huì)檢查緩存,直接利用之前的資源,不再重新請(qǐng)求服務(wù)器。
httpClient緩存代碼分析
需要引入HttpClinet緩存模塊

測(cè)試代碼如下:
public class CacheHttpClient {
static CacheConfig cacheConfig =
CacheConfig.custom().setMaxCacheEntries(1000).setMaxObjectSize(8192).build();
static CloseableHttpClient cachingClient =
CachingHttpClients.custom().setCacheConfig(cacheConfig).build();
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
while (true) {
scanner.nextLine();
HttpCacheContext context = HttpCacheContext.create();
HttpGet httpget = new HttpGet("http://www.mydomain.com/content/");
CloseableHttpResponse response = cachingClient.execute(httpget, context);
try {
CacheResponseStatus responseStatus = context.getCacheResponseStatus();
switch (responseStatus) {
case CACHE_HIT:
System.out.println(
"A response was generated from the cache with "
+ "no requests sent upstream");
break;
case CACHE_MODULE_RESPONSE:
System.out.println(
"The response was generated directly by the " + "caching module");
break;
case CACHE_MISS:
System.out.println("The response came from an upstream server");
break;
case VALIDATED:
System.out.println(
"The response was generated from the cache "
+ "after validating the entry with the origin server");
break;
default:
// do nothing
}
} finally {
response.close();
}
}
}
}
緩存的核心處理邏輯位于org.apache.http.impl.client.cache.CachingExec#execute中,如下:

緩存處理的核心代碼我在圖中已經(jīng)做了標(biāo)注
是否啟用緩存,代碼很簡(jiǎn)單

從緩存中獲取信息,這里的key實(shí)際就是訪問時(shí)使用的URI,緩存底層默認(rèn)使用的是一個(gè)Map

image-20220721214408121 緩存未命中時(shí)會(huì)向服務(wù)器發(fā)送真正的請(qǐng)求,代碼簡(jiǎn)單,不做分析
緩存命中,這時(shí)要處理兩種情況:「「緩存未過期」」、「「緩存過期+再驗(yàn)證」」

對(duì)http協(xié)議了解后,這塊的代碼非常簡(jiǎn)單,所以筆者在這里也不贅述了
重定向
?https://datatracker.ietf.org/doc/html/rfc2616#page-61
?
接下來我們聊聊重定向,跟重定向相關(guān)的響應(yīng)碼如下:
| 狀態(tài)碼 | 原因短語 | 含 義 |
|---|---|---|
| 301 | Moved Permanently | 在請(qǐng)求的 URL 已被移除時(shí)使用。響應(yīng)的 Location 首部中應(yīng)該包含 資源現(xiàn)在所處的URL,「【301代表永久重定向】」,客戶端在后續(xù)訪問時(shí)應(yīng)該將URL替換為本次Location首部標(biāo)明的URL |
| 302 | Found | 「【302代表臨時(shí)重定向】」,客戶端后續(xù)訪問時(shí)不需要進(jìn)行替換,仍然應(yīng)該使用原來的URL |
| 303 | See Other | 其主要目的是允許 POST 請(qǐng)求的響應(yīng)將客戶端定向到某個(gè)資源上去 |
| 307 | Temporary Redirect | 也是臨時(shí)重定向,跟302類似 |
你可能已經(jīng)注意到 302、303 和 307 狀態(tài)碼之間存在一些交叉。這些狀態(tài)碼的用法有著細(xì)微的差別,大部分差別都源于 「【HTTP/1.0 和 HTTP/1.1 應(yīng)用程序?qū)?這些狀態(tài)碼處理方式的不同】」。
當(dāng) HTTP/1.0 客戶端發(fā)起一個(gè) POST 請(qǐng)求,并在響應(yīng)中收到 302 重定向狀態(tài)碼時(shí), 它會(huì)接受 Location 首部的重定向 URL,并向那個(gè) URL 發(fā)起一個(gè) GET 請(qǐng)求(而不 會(huì)像原始請(qǐng)求中那樣發(fā)起 POST 請(qǐng)求)。
HTTP/1.0 服務(wù)器希望 HTTP/1.0 客戶端這么做——如果 HTTP/1.0 服務(wù)器收到來自 HTTP/1.0 客戶端的 POST 請(qǐng)求之后發(fā)送了 302 狀態(tài)碼,服務(wù)器就期望客戶端能夠接 受重定向 URL,并向重定向的 URL 發(fā)送一個(gè) GET 請(qǐng)求。
問題出在 HTTP/1.1。HTTP/1.1 規(guī)范使用 303 狀態(tài)碼來實(shí)現(xiàn)同樣的行為(服務(wù)器發(fā) 送 303 狀態(tài)碼來重定向客戶端的 POST 請(qǐng)求,在它后面跟上一個(gè) GET 請(qǐng)求)。
為了避開這個(gè)問題,HTTP/1.1 規(guī)范指出,對(duì)于 HTTP/1.1 客戶端,用 307 狀態(tài)碼取 代 302 狀態(tài)碼來進(jìn)行臨時(shí)重定向。這樣服務(wù)器就可以將 302 狀態(tài)碼保留起來,為 HTTP/1.0 客戶端使用了。
HttpClient重定向代碼分析
核心代碼位于:org.apache.http.impl.execchain.RedirectExec#execute

重定向的處理策略都定義在redirectStrategy中,我們看下它的代碼:
isRedirected方法,是否需要重定向
實(shí)際就是判斷狀態(tài)碼是不是我們前文提到過的301、302、303、307。
getRedirect方法,重定向時(shí)需要封裝的請(qǐng)求信息:請(qǐng)求方法+URL
寫在最后
本來打算這篇文章作為《HTTP實(shí)戰(zhàn)》的最后一篇的,實(shí)在太忙再加上篇幅原因,關(guān)于數(shù)據(jù)壓縮、分塊傳輸、范圍請(qǐng)求就放到下篇文章吧~
「參考:」
《Http權(quán)威指南》
https://hc.apache.org/httpcomponents-client-4.5.x/current/tutorial/html/caching.html
https://datatracker.ietf.org/doc/html/rfc2616#page-74
https://datatracker.ietf.org/doc/html/rfc7232#page-13
https://www.digitalocean.com/community/tutorials/web-caching-basics-terminology-http-headers-and-caching-strategies
