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