重拾Java Network Programming(四)URLConnection & Cache

前言

本文將根據最近所學的Java網絡編程實現一個簡單的基於URL的緩存。本文將涉及以下內容:java

  • 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-modified-since:<date>說明在date以後文檔被修改了的話,就執行請求的方法,即條件式的再驗證。一般和服務器的last-modified響應頭部配合使用。last-modified說明該資源最後一次的修改時間。若是資源的這個屬性發生變化,則說明緩存已經失效。則服務器會返回最新的資源。不然會返回304 not modified響應。數據結構

這種方式的好處在於,若是資源未失效,則無需重傳資源,能夠有效的節省帶寬。ide

與之相相似的有if-unmodified-since,該屬性的意思是若是資源在該日期以後被修改了,則不執行請求方法。一般在進行部分文件傳輸時,獲取文件的其他部分以前要確保文件未發生變化,此時這個首部頗有用。fetch

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<String, List<String>> 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<String, List<String>> getHeaders() throws IOException; 

    //獲取響應體的輸入流,即從InputStream中便可讀取緩存的內容
    @Override
    public InputStream getBody() throws IOException;

這裏的流程基本以下:
當啓動URLConnection鏈接時,URLConnection會先訪問ResponseCache的get方法,詢問緩存是否命中想要的數據。輸入的參數包括URI,請求方法(一般指緩存GET請求),以及請求頭(若是請求頭中明確要求不訪問緩存,則直接返回null)。若是命中,則返回CacheResponse對象,從該對象中獲取緩存的輸入流。 若是沒有命中,則會啓動鏈接,並將獲取的數據使用ResponseCache的put方法寫入緩存。該方法會返回一個輸出流用於存儲緩存。

Cache Implementation In JAVA

如今我須要實現緩存,我將會在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<String, List<String>> 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<String, List<String>> 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 Map<URI, MyCacheResponse> responses;
    private final int SIZE;

    public MyResponseCache(Map<URI, MyCacheResponse> 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<String, List<String>> 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 ConcurrentHashMap<URI, MyCacheResponse> map;

    public CacheValidator(ConcurrentHashMap<URI, MyCacheResponse> map){
        this.map = map;
    }
    @Override
    public void run() {
        while (!stop){
            for (Map.Entry<URI, MyCacheResponse> 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 {
        ConcurrentHashMap<URI, MyCacheResponse> map = 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();
        }
    }

}

clipboard.png
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~

參考書籍

HTTP 權威指南 Java Network Programming
相關文章
相關標籤/搜索