摘要:從而一方面減少了響應(yīng)時(shí)間,另一方面減少了服務(wù)器的壓力。表明響應(yīng)只能被單個(gè)用戶緩存,不能作為共享緩存即代理服務(wù)器不能緩存它。這種情況稱為服務(wù)器再驗(yàn)證。否則會(huì)返回響應(yīng)。
前言
本文將根據(jù)最近所學(xué)的Java網(wǎng)絡(luò)編程實(shí)現(xiàn)一個(gè)簡(jiǎn)單的基于URL的緩存。本文將涉及如下內(nèi)容:
HTTP協(xié)議
HTTP協(xié)議中與緩存相關(guān)的內(nèi)容
URLConnection 和 HTTPURLConnection
ResponseCache,CacheRequest,CacheResponse
WHAT & WHY正常來(lái)說(shuō),服務(wù)器和客戶端的HTTP通信需要首先通過(guò)TCP的三次握手建立連接,然后客戶端再發(fā)出HTTP請(qǐng)求并接收服務(wù)器的響應(yīng)。但是,在有些時(shí)候,服務(wù)器的資源并沒(méi)有發(fā)生改變。此時(shí)重復(fù)的向服務(wù)器請(qǐng)求同樣的資源會(huì)帶來(lái)帶寬的浪費(fèi)。針對(duì)這種情況我們可以采用緩存的方式,既可以是本地緩存,也可以是代理服務(wù)器的緩存,來(lái)減少對(duì)服務(wù)器資源的不必要的訪問(wèn)。從而一方面減少了響應(yīng)時(shí)間,另一方面減少了服務(wù)器的壓力。
那么我們?nèi)绾沃?,何時(shí)可以直接使用緩存,何時(shí)因?yàn)楫?dāng)前的緩存已經(jīng)過(guò)時(shí),需要重新向資源所在的服務(wù)器發(fā)出請(qǐng)求呢?
緩存關(guān)鍵字HTTP1.0和HTTP1.1分別針對(duì)緩存提供了一些HEADER屬性供連接雙方參考。需要注意,如果是HTTP1.0的服務(wù)器,將無(wú)法識(shí)別HTTP1.1的緩存屬性。所以有時(shí)候?yàn)榱讼蛳录嫒菪裕覀儠?huì)設(shè)置多個(gè)和緩存相關(guān)的屬性。當(dāng)然,它們彼此之間是存在優(yōu)先級(jí)的,后面將會(huì)詳細(xì)介紹。
Expires支持HTTP1.0,說(shuō)明該資源在Expires內(nèi)容之后過(guò)期。Expires關(guān)鍵字使用的是絕對(duì)日期。
Cache-control支持HTTP1.1,使用相對(duì)日期對(duì)緩存進(jìn)行管理。它可定義的屬性包括:
max-age=[seconds]: 當(dāng)前時(shí)間經(jīng)過(guò)n秒后緩存資源失效
s-maxage=[seconds]: 從共享緩存獲取的數(shù)據(jù)在n秒后失效,私有緩存往往可以更久一些
public: 表明響應(yīng)可以被任何對(duì)象(包括:發(fā)送請(qǐng)求的客戶端,代理服務(wù)器,等等)緩存。
private: 表明響應(yīng)只能被單個(gè)用戶緩存,不能作為共享緩存(即代理服務(wù)器不能緩存它)。
no-cache: 允許緩存,但每次訪問(wèn)緩存時(shí)必須重新驗(yàn)證緩存的有效性
no-store: 緩存不應(yīng)存儲(chǔ)有關(guān)客戶端請(qǐng)求或服務(wù)器響應(yīng)的任何內(nèi)容。
must-revalidate: 緩存必須在使用之前驗(yàn)證舊資源的狀態(tài),并且不可使用過(guò)期資源。
還有許多相關(guān)的屬性,想要詳細(xì)了解的話可以參考這篇文章。
僅僅是已緩存文檔的過(guò)期并不意味這它和原始服務(wù)器上目前處于活躍狀態(tài)的資源有實(shí)際的區(qū)別,只是意味著到了要核實(shí)的時(shí)間。這種情況稱為服務(wù)器再驗(yàn)證。
if-modified-since:
這種方式的好處在于,如果資源未失效,則無(wú)需重傳資源,可以有效的節(jié)省帶寬。
與之相類似的有if-unmodified-since,該屬性的意思是如果資源在該日期之后被修改了,則不執(zhí)行請(qǐng)求方法。通常在進(jìn)行部分文件傳輸時(shí),獲取文件的其余部分之前要確保文件未發(fā)生變化,此時(shí)這個(gè)首部很有用。
If-None-Match/If-Match/If-Range有些時(shí)候,僅僅是使用最后修改日期再驗(yàn)證是不夠的:
有些文檔可能被周期性重寫(xiě),但是實(shí)際的數(shù)據(jù)常常是一樣的。也就是說(shuō)內(nèi)容沒(méi)有變化,但是修改日期變化了。
有些文檔可能被修改了,但是所做的修改并不重要,不需要所有的緩存都重裝數(shù)據(jù)。
有些服務(wù)器無(wú)法準(zhǔn)確的判定最后的修改日期
有些文檔會(huì)在更小的時(shí)間粒度發(fā)生變化(比如監(jiān)視器,股票等),此時(shí)以秒為最小單位的修改日期可能不夠用
為此,HTTP提供了實(shí)體標(biāo)簽(ETag)的比較。當(dāng)發(fā)布者對(duì)文檔進(jìn)行修改時(shí),可以修改文檔的實(shí)體標(biāo)簽來(lái)說(shuō)明新的版本。這樣,只要實(shí)體標(biāo)簽改變,緩存就可以用If-None-Match條件首部來(lái)獲取新的副本。
服務(wù)器在響應(yīng)中會(huì)標(biāo)記當(dāng)前資源的ETag。一旦文檔過(guò)期后,可以使用HEAD請(qǐng)求來(lái)?xiàng)l件式再驗(yàn)證。如果服務(wù)器上ETag改變,則會(huì)返回最新的資源。當(dāng)然,ETag可以包含多個(gè)內(nèi)容,說(shuō)明本地存儲(chǔ)了多個(gè)版本的副本。如果沒(méi)有命中這些副本,再返回完整資源。
If-None-Match: "v2.4","v2.5","v2.6"
如果服務(wù)器收到的請(qǐng)求中既帶有if-modified-since,又帶有實(shí)體標(biāo)簽條件首部,那么只有這兩個(gè)條件都滿足時(shí),才會(huì)返回304 not modified響應(yīng)。
Cache in JAVA默認(rèn)情況下。JAVA不緩存任何任何內(nèi)容。我們需要通過(guò)自己的實(shí)現(xiàn)來(lái)支持URL的緩存。我們需要實(shí)現(xiàn)以下抽象類:
ResponseCache
CacheRequest
CacheResponse
這里其實(shí)使用的是Template Pattern。有興趣的話可以去了解一下。
ResponseCache 需要實(shí)現(xiàn)的方法
//根據(jù)URI,請(qǐng)求的方法以及請(qǐng)求頭獲取緩存的響應(yīng)。如果響應(yīng)過(guò)期,則重新發(fā)出請(qǐng)求 public abstract CacheResponse get(URI uri, String rqstMethod, Map> rqstHeaders) throws IOException; //在獲取到響應(yīng)之后調(diào)用該方法 //如果該響應(yīng)不可以被緩存,則返回null //如果該響應(yīng)可以被緩存,則返回CacheRequest對(duì)象,可以利用其下的OutputStream來(lái)寫(xiě)入緩存的內(nèi)容 public CacheRequest put(URI uri, URLConnection conn) throws IOException;
CacheRequest需要實(shí)現(xiàn)的方法:
//獲取寫(xiě)入緩存的輸入流 @Override public OutputStream getBody() throws IOException; //放棄當(dāng)前的緩存 @Override public void abort();
CacheResponse需要實(shí)現(xiàn)的方法
//獲取響應(yīng)頭 @Override public Map> getHeaders() throws IOException; //獲取響應(yīng)體的輸入流,即從InputStream中即可讀取緩存的內(nèi)容 @Override public InputStream getBody() throws IOException;
這里的流程基本如下:
當(dāng)啟動(dòng)URLConnection連接時(shí),URLConnection會(huì)先訪問(wèn)ResponseCache的get方法,詢問(wèn)緩存是否命中想要的數(shù)據(jù)。輸入的參數(shù)包括URI,請(qǐng)求方法(通常指緩存GET請(qǐng)求),以及請(qǐng)求頭(如果請(qǐng)求頭中明確要求不訪問(wèn)緩存,則直接返回null)。如果命中,則返回CacheResponse對(duì)象,從該對(duì)象中獲取緩存的輸入流。 如果沒(méi)有命中,則會(huì)啟動(dòng)連接,并將獲取的數(shù)據(jù)使用ResponseCache的put方法寫(xiě)入緩存。該方法會(huì)返回一個(gè)輸出流用于存儲(chǔ)緩存。
現(xiàn)在我需要實(shí)現(xiàn)緩存,我將會(huì)在put時(shí)判斷該資源是否允許緩存(通常有cache-control參數(shù)來(lái)提供)。我也會(huì)在get時(shí)判讀能否從緩存中命中資源以及該資源是否失效,如果失效就從緩存中刪除,否則直接返回,無(wú)需訪問(wèn)服務(wù)器。這里我還通過(guò)一個(gè)后臺(tái)線程遍歷緩存數(shù)據(jù)結(jié)構(gòu),及時(shí)將失效的資源從緩存中刪除。
MyCacheRequest使用ByteArrayOutputStream將緩存內(nèi)容通過(guò)內(nèi)存IO存儲(chǔ)在內(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存儲(chǔ)了請(qǐng)求頭,并將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對(duì)應(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進(jìn)行緩存的同步讀寫(xiě)。這里默認(rèn)緩存達(dá)到上限就不再存入新的緩存。建議可以通過(guò)隊(duì)列或是LinkedHashMap實(shí)現(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方法將不會(huì)調(diào)用DNS服務(wù) * @param rqstMethod - 請(qǐng)求方法 一般只緩存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后臺(tái)任務(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()); } } } } }
最后使用主線程啟動(dòng)緩存,注意這里需要顯式的設(shè)置緩存器和開(kāi)啟URLConnection的緩存。默認(rèn)情況下,JAVA不開(kāi)啟緩存。同時(shí)JAVA全局只支持一個(gè)緩存的存在。
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è)置后臺(tái)線程 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(); //開(kāi)啟緩存 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(); } } }
想要了解更多開(kāi)發(fā)技術(shù),面試教程以及互聯(lián)網(wǎng)公司內(nèi)推,歡迎關(guān)注我的微信公眾號(hào)!將會(huì)不定期的發(fā)放福利哦~
HTTP 權(quán)威指南
Java Network Programming
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/71037.html
摘要:從而一方面減少了響應(yīng)時(shí)間,另一方面減少了服務(wù)器的壓力。表明響應(yīng)只能被單個(gè)用戶緩存,不能作為共享緩存即代理服務(wù)器不能緩存它。這種情況稱為服務(wù)器再驗(yàn)證。否則會(huì)返回響應(yīng)。 前言 本文將根據(jù)最近所學(xué)的Java網(wǎng)絡(luò)編程實(shí)現(xiàn)一個(gè)簡(jiǎn)單的基于URL的緩存。本文將涉及如下內(nèi)容: HTTP協(xié)議 HTTP協(xié)議中與緩存相關(guān)的內(nèi)容 URLConnection 和 HTTPURLConnection Respo...
摘要:不同類型的流入,往往對(duì)應(yīng)于不同類型的流數(shù)據(jù)。所以通常會(huì)將字節(jié)緩存到一定數(shù)量后再發(fā)送。如果是,則將兩個(gè)標(biāo)記都拋棄并且將之前的內(nèi)容作為一行返回。因此二者陷入死鎖。因此推出了和類。 前言 最近在重拾Java網(wǎng)絡(luò)編程,想要了解一些JAVA語(yǔ)言基本的實(shí)現(xiàn),這里記錄一下學(xué)習(xí)的過(guò)程。 閱讀之前,你需要知道 網(wǎng)絡(luò)節(jié)點(diǎn)(node):位于網(wǎng)絡(luò)上的互相連通的設(shè)備,通常為計(jì)算機(jī),也可以是打印機(jī),網(wǎng)橋,路由器等...
摘要:前言今天,我將梳理在網(wǎng)絡(luò)編程中很重要的一個(gè)類以及其相關(guān)的類。這類主機(jī)通常不需要外部互聯(lián)網(wǎng)服務(wù),僅有主機(jī)間相互通訊的需求??梢酝ㄟ^(guò)該接口獲取所有本地地址,并根據(jù)這些地址創(chuàng)建。在這里我們使用阻塞隊(duì)列實(shí)現(xiàn)主線程和打印線程之間的通信。 前言 今天,我將梳理在Java網(wǎng)絡(luò)編程中很重要的一個(gè)類InetAddress以及其相關(guān)的類NetworkInterface。在這篇文章中將會(huì)涉及: InetA...
摘要:從網(wǎng)絡(luò)加載圖片加載從加載從網(wǎng)絡(luò)加載從加載具體的方法實(shí)現(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 (...
閱讀 1700·2021-11-02 14:47
閱讀 3648·2019-08-30 15:44
閱讀 1334·2019-08-29 16:42
閱讀 1731·2019-08-26 13:53
閱讀 935·2019-08-26 10:41
閱讀 3458·2019-08-23 17:10
閱讀 597·2019-08-23 14:24
閱讀 1717·2019-08-23 11:59