摘要:從而一方面減少了響應(yīng)時間,另一方面減少了服務(wù)器的壓力。表明響應(yīng)只能被單個用戶緩存,不能作為共享緩存即代理服務(wù)器不能緩存它。這種情況稱為服務(wù)器再驗證。否則會返回響應(yīng)。
前言
本文將根據(jù)最近所學(xué)的Java網(wǎng)絡(luò)編程實現(xiàn)一個簡單的基于URL的緩存。本文將涉及如下內(nèi)容:
HTTP協(xié)議
HTTP協(xié)議中與緩存相關(guān)的內(nèi)容
URLConnection 和 HTTPURLConnection
ResponseCache,CacheRequest,CacheResponse
WHAT & WHY正常來說,服務(wù)器和客戶端的HTTP通信需要首先通過TCP的三次握手建立連接,然后客戶端再發(fā)出HTTP請求并接收服務(wù)器的響應(yīng)。但是,在有些時候,服務(wù)器的資源并沒有發(fā)生改變。此時重復(fù)的向服務(wù)器請求同樣的資源會帶來帶寬的浪費。針對這種情況我們可以采用緩存的方式,既可以是本地緩存,也可以是代理服務(wù)器的緩存,來減少對服務(wù)器資源的不必要的訪問。從而一方面減少了響應(yīng)時間,另一方面減少了服務(wù)器的壓力。
那么我們?nèi)绾沃溃螘r可以直接使用緩存,何時因為當(dāng)前的緩存已經(jīng)過時,需要重新向資源所在的服務(wù)器發(fā)出請求呢?
緩存關(guān)鍵字HTTP1.0和HTTP1.1分別針對緩存提供了一些HEADER屬性供連接雙方參考。需要注意,如果是HTTP1.0的服務(wù)器,將無法識別HTTP1.1的緩存屬性。所以有時候為了向下兼容性,我們會設(shè)置多個和緩存相關(guān)的屬性。當(dāng)然,它們彼此之間是存在優(yōu)先級的,后面將會詳細(xì)介紹。
Expires支持HTTP1.0,說明該資源在Expires內(nèi)容之后過期。Expires關(guān)鍵字使用的是絕對日期。
Cache-control支持HTTP1.1,使用相對日期對緩存進行管理。它可定義的屬性包括:
max-age=[seconds]: 當(dāng)前時間經(jīng)過n秒后緩存資源失效
s-maxage=[seconds]: 從共享緩存獲取的數(shù)據(jù)在n秒后失效,私有緩存往往可以更久一些
public: 表明響應(yīng)可以被任何對象(包括:發(fā)送請求的客戶端,代理服務(wù)器,等等)緩存。
private: 表明響應(yīng)只能被單個用戶緩存,不能作為共享緩存(即代理服務(wù)器不能緩存它)。
no-cache: 允許緩存,但每次訪問緩存時必須重新驗證緩存的有效性
no-store: 緩存不應(yīng)存儲有關(guān)客戶端請求或服務(wù)器響應(yīng)的任何內(nèi)容。
must-revalidate: 緩存必須在使用之前驗證舊資源的狀態(tài),并且不可使用過期資源。
還有許多相關(guān)的屬性,想要詳細(xì)了解的話可以參考這篇文章。
僅僅是已緩存文檔的過期并不意味這它和原始服務(wù)器上目前處于活躍狀態(tài)的資源有實際的區(qū)別,只是意味著到了要核實的時間。這種情況稱為服務(wù)器再驗證。
if-modified-since:
這種方式的好處在于,如果資源未失效,則無需重傳資源,可以有效的節(jié)省帶寬。
與之相類似的有if-unmodified-since,該屬性的意思是如果資源在該日期之后被修改了,則不執(zhí)行請求方法。通常在進行部分文件傳輸時,獲取文件的其余部分之前要確保文件未發(fā)生變化,此時這個首部很有用。
If-None-Match/If-Match/If-Range有些時候,僅僅是使用最后修改日期再驗證是不夠的:
有些文檔可能被周期性重寫,但是實際的數(shù)據(jù)常常是一樣的。也就是說內(nèi)容沒有變化,但是修改日期變化了。
有些文檔可能被修改了,但是所做的修改并不重要,不需要所有的緩存都重裝數(shù)據(jù)。
有些服務(wù)器無法準(zhǔn)確的判定最后的修改日期
有些文檔會在更小的時間粒度發(fā)生變化(比如監(jiān)視器,股票等),此時以秒為最小單位的修改日期可能不夠用
為此,HTTP提供了實體標(biāo)簽(ETag)的比較。當(dāng)發(fā)布者對文檔進行修改時,可以修改文檔的實體標(biāo)簽來說明新的版本。這樣,只要實體標(biāo)簽改變,緩存就可以用If-None-Match條件首部來獲取新的副本。
服務(wù)器在響應(yīng)中會標(biāo)記當(dāng)前資源的ETag。一旦文檔過期后,可以使用HEAD請求來條件式再驗證。如果服務(wù)器上ETag改變,則會返回最新的資源。當(dāng)然,ETag可以包含多個內(nèi)容,說明本地存儲了多個版本的副本。如果沒有命中這些副本,再返回完整資源。
If-None-Match: "v2.4","v2.5","v2.6"
如果服務(wù)器收到的請求中既帶有if-modified-since,又帶有實體標(biāo)簽條件首部,那么只有這兩個條件都滿足時,才會返回304 not modified響應(yīng)。
Cache in JAVA默認(rèn)情況下。JAVA不緩存任何任何內(nèi)容。我們需要通過自己的實現(xiàn)來支持URL的緩存。我們需要實現(xiàn)以下抽象類:
ResponseCache
CacheRequest
CacheResponse
這里其實使用的是Template Pattern。有興趣的話可以去了解一下。
ResponseCache 需要實現(xiàn)的方法
//根據(jù)URI,請求的方法以及請求頭獲取緩存的響應(yīng)。如果響應(yīng)過期,則重新發(fā)出請求 public abstract CacheResponse get(URI uri, String rqstMethod, Map> rqstHeaders) throws IOException; //在獲取到響應(yīng)之后調(diào)用該方法 //如果該響應(yīng)不可以被緩存,則返回null //如果該響應(yīng)可以被緩存,則返回CacheRequest對象,可以利用其下的OutputStream來寫入緩存的內(nèi)容 public CacheRequest put(URI uri, URLConnection conn) throws IOException;
CacheRequest需要實現(xiàn)的方法:
//獲取寫入緩存的輸入流 @Override public OutputStream getBody() throws IOException; //放棄當(dāng)前的緩存 @Override public void abort();
CacheResponse需要實現(xiàn)的方法
//獲取響應(yīng)頭 @Override public Map> getHeaders() throws IOException; //獲取響應(yīng)體的輸入流,即從InputStream中即可讀取緩存的內(nèi)容 @Override public InputStream getBody() throws IOException;
這里的流程基本如下:
當(dāng)啟動URLConnection連接時,URLConnection會先訪問ResponseCache的get方法,詢問緩存是否命中想要的數(shù)據(jù)。輸入的參數(shù)包括URI,請求方法(通常指緩存GET請求),以及請求頭(如果請求頭中明確要求不訪問緩存,則直接返回null)。如果命中,則返回CacheResponse對象,從該對象中獲取緩存的輸入流。 如果沒有命中,則會啟動連接,并將獲取的數(shù)據(jù)使用ResponseCache的put方法寫入緩存。該方法會返回一個輸出流用于存儲緩存。
現(xiàn)在我需要實現(xiàn)緩存,我將會在put時判斷該資源是否允許緩存(通常有cache-control參數(shù)來提供)。我也會在get時判讀能否從緩存中命中資源以及該資源是否失效,如果失效就從緩存中刪除,否則直接返回,無需訪問服務(wù)器。這里我還通過一個后臺線程遍歷緩存數(shù)據(jù)結(jié)構(gòu),及時將失效的資源從緩存中刪除。
MyCacheRequest使用ByteArrayOutputStream將緩存內(nèi)容通過內(nèi)存IO存儲在內(nèi)存中
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.CacheRequest; public class MyCacheRequest extends CacheRequest{ private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); public MyCacheRequest(){ } @Override public OutputStream getBody() throws IOException { return outputStream; } @Override public void abort() { outputStream.reset(); } public byte[] getData(){ if (outputStream.size() == 0) return null; else return outputStream.toByteArray(); } }
MyCacheResponse存儲了請求頭,并將cache-control的信息封裝在了CacheControl類中:
import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.CacheResponse; import java.net.URLConnection; import java.util.Date; import java.util.List; import java.util.Map; public class MyCacheResponse extends CacheResponse { private final MyCacheRequest cacheRequest; private final Map> headers; private final Date expires; private final CacheControl control; public MyCacheResponse(MyCacheRequest cacheRequest, URLConnection uc, CacheControl control){ this.cacheRequest = cacheRequest; this.headers = uc.getHeaderFields(); this.expires = new Date(uc.getExpiration()); this.control = control; } @Override public Map > getHeaders() throws IOException { return this.headers; } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(cacheRequest.getData()); } public boolean isExpired() { Date now = new Date(); if (control.getMaxAge() !=null && control.getMaxAge().before(now)) return true; else if (expires != null) { return expires.before(now); } else { return false; } } public CacheControl getControl() { return control; } }
CacheControl類如下這里只用到了基本的max-age屬性和no-store屬性
import java.util.Date; import java.util.Locale; /** * 封裝HTTP協(xié)議中cache—control對應(yīng)的屬性 */ public class CacheControl { private Date maxAge; private Date sMaxAge; private boolean mustRevalidate; private boolean noCache; private boolean noStore; private boolean proxyRevalidate; private boolean publicCache; private boolean privateCache; private static final String MAX_AGE = "max-age="; private static final String SMAX_AGE = "s-maxage="; private static final String MUST_REVALIDATE = "must-revalidate"; private static final String PROXY_REVALIDATE = "proxy-revalidate"; private static final String NO_CACHE = "no-cache"; private static final String NO_STORE = "no-store"; private static final String PUBLIC_CACHE = "public"; private static final String PRIVATE_CACHE = "private"; public CacheControl(String s){ if (s == null || s.trim().isEmpty()) { return; // default policy } String[] components = s.split(","); Date now = new Date(); for (String component : components){ try { component = component.trim().toLowerCase(Locale.US); if (component.startsWith(MAX_AGE)){ int secondsInTheFuture = Integer.parseInt(component.substring(MAX_AGE.length())); maxAge = new Date(now.getTime() + 1000 * secondsInTheFuture); }else if (component.startsWith(SMAX_AGE)){ int secondsInTheFuture = Integer.parseInt(component.substring(SMAX_AGE.length())); sMaxAge = new Date(now.getTime() + 1000 * secondsInTheFuture); }else if (component.equals(MUST_REVALIDATE)){ mustRevalidate = true; }else if (component.equals(PROXY_REVALIDATE)){ proxyRevalidate = true; }else if (component.equals(NO_CACHE)){ noCache = true; }else if (component.equals(NO_STORE)){ noStore = true; }else if (component.equals(PUBLIC_CACHE)){ publicCache = true; }else if (component.equals(PRIVATE_CACHE)){ privateCache = true; } }catch (RuntimeException ex) { continue; } } } public Date getMaxAge() { return maxAge; } public Date getsMaxAge() { return sMaxAge; } public boolean isMustRevalidate() { return mustRevalidate; } public boolean isNoCache() { return noCache; } public boolean isNoStore() { return noStore; } public boolean isProxyRevalidate() { return proxyRevalidate; } public boolean isPublicCache() { return publicCache; } public boolean isPrivateCache() { return privateCache; } }
ResponseCache類使用ConcurrentHashMap進行緩存的同步讀寫。這里默認(rèn)緩存達(dá)到上限就不再存入新的緩存。建議可以通過隊列或是LinkedHashMap實現(xiàn)FIFO或是LRU管理。
import java.io.IOException; import java.net.*; import java.util.List; import java.util.Map; public class MyResponseCache extends ResponseCache{ private final Mapresponses; private final int SIZE; public MyResponseCache(Map responses, int size){ this.responses = responses; this.SIZE = size; } /** * * @param uri 路徑 - equals方法將不會調(diào)用DNS服務(wù) * @param rqstMethod - 請求方法 一般只緩存GET方法 * @param rqstHeaders - 判斷是否可以緩存 * @return * @throws IOException */ @Override public CacheResponse get(URI uri, String rqstMethod, Map > rqstHeaders) throws IOException { if ("GET".equals(rqstMethod)) { MyCacheResponse response = responses.get(uri); // check expiration date if (response != null && response.isExpired()) { responses.remove(uri); response = null; } return response; } return null; } @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { if (responses.size() >= SIZE) return null; CacheControl cacheControl = new CacheControl(conn.getHeaderField("Cache-Control")); if (cacheControl.isNoStore()){ System.out.println(conn.getHeaderField(0)); return null; } MyCacheRequest myCacheRequest = new MyCacheRequest(); MyCacheResponse myCacheResponse = new MyCacheResponse(myCacheRequest, conn ,cacheControl); responses.put(uri, myCacheResponse); return myCacheRequest; } }
CacheValidator后臺任務(wù),將失效的緩存刪除:
import java.net.URI; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class CacheValidator implements Runnable{ boolean stop; private ConcurrentHashMapmap; public CacheValidator(ConcurrentHashMap map){ this.map = map; } @Override public void run() { while (!stop){ for (Map.Entry entry : map.entrySet()){ if (entry.getValue().isExpired()){ System.out.println(entry.getKey()); map.remove(entry.getKey()); } } } } }
最后使用主線程啟動緩存,注意這里需要顯式的設(shè)置緩存器和開啟URLConnection的緩存。默認(rèn)情況下,JAVA不開啟緩存。同時JAVA全局只支持一個緩存的存在。
import java.io.BufferedInputStream; import java.io.IOException; import java.net.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) throws InterruptedException { ConcurrentHashMapmap = new ConcurrentHashMap<>(); MyResponseCache myResponseCache = new MyResponseCache(map, 20); //設(shè)置默認(rèn)緩存器 ResponseCache.setDefault(myResponseCache); //設(shè)置后臺線程 Thread thread = new Thread(new CacheValidator(map)); thread.setDaemon(true); thread.start(); System.out.println(map.size()); fetchURL(SOME_URL); TimeUnit.SECONDS.sleep(20000); } public static void fetchURL(String location){ try { URL url = new URL(location); URLConnection uc = url.openConnection(); //開啟緩存 uc.setDefaultUseCaches(true); BufferedInputStream bfr = new BufferedInputStream(uc.getInputStream()); int c; while ((c = bfr.read()) != -1){ // System.out.print((char) c); //do something } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
想要了解更多開發(fā)技術(shù),面試教程以及互聯(lián)網(wǎng)公司內(nèi)推,歡迎關(guān)注我的微信公眾號!將會不定期的發(fā)放福利哦~
HTTP 權(quán)威指南
Java Network Programming
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/61957.html
摘要:從而一方面減少了響應(yīng)時間,另一方面減少了服務(wù)器的壓力。表明響應(yīng)只能被單個用戶緩存,不能作為共享緩存即代理服務(wù)器不能緩存它。這種情況稱為服務(wù)器再驗證。否則會返回響應(yīng)。 前言 本文將根據(jù)最近所學(xué)的Java網(wǎng)絡(luò)編程實現(xiàn)一個簡單的基于URL的緩存。本文將涉及如下內(nèi)容: HTTP協(xié)議 HTTP協(xié)議中與緩存相關(guān)的內(nèi)容 URLConnection 和 HTTPURLConnection Respo...
摘要:不同類型的流入,往往對應(yīng)于不同類型的流數(shù)據(jù)。所以通常會將字節(jié)緩存到一定數(shù)量后再發(fā)送。如果是,則將兩個標(biāo)記都拋棄并且將之前的內(nèi)容作為一行返回。因此二者陷入死鎖。因此推出了和類。 前言 最近在重拾Java網(wǎng)絡(luò)編程,想要了解一些JAVA語言基本的實現(xiàn),這里記錄一下學(xué)習(xí)的過程。 閱讀之前,你需要知道 網(wǎng)絡(luò)節(jié)點(node):位于網(wǎng)絡(luò)上的互相連通的設(shè)備,通常為計算機,也可以是打印機,網(wǎng)橋,路由器等...
摘要:前言今天,我將梳理在網(wǎng)絡(luò)編程中很重要的一個類以及其相關(guān)的類。這類主機通常不需要外部互聯(lián)網(wǎng)服務(wù),僅有主機間相互通訊的需求??梢酝ㄟ^該接口獲取所有本地地址,并根據(jù)這些地址創(chuàng)建。在這里我們使用阻塞隊列實現(xiàn)主線程和打印線程之間的通信。 前言 今天,我將梳理在Java網(wǎng)絡(luò)編程中很重要的一個類InetAddress以及其相關(guān)的類NetworkInterface。在這篇文章中將會涉及: InetA...
摘要:從網(wǎng)絡(luò)加載圖片加載從加載從網(wǎng)絡(luò)加載從加載具體的方法實現(xiàn)接口的類以后再做分析,而從網(wǎng)絡(luò)加載兩步從網(wǎng)絡(luò)獲取數(shù)據(jù)處理數(shù)據(jù)。 4.從網(wǎng)絡(luò)加載 EngineJob current = jobs.get(key); if (current != null) { current.addCallback(cb); if (...
閱讀 2120·2021-11-24 09:39
閱讀 1503·2019-08-30 15:44
閱讀 1954·2019-08-29 17:06
閱讀 3406·2019-08-29 16:32
閱讀 3552·2019-08-29 16:26
閱讀 2662·2019-08-29 15:35
閱讀 3033·2019-08-29 12:50
閱讀 1646·2019-08-29 11:15