博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
重拾Java Network Programming(四)URLConnection & Cache
阅读量:6939 次
发布时间:2019-06-27

本文共 11907 字,大约阅读时间需要 39 分钟。

前言

本文将根据最近所学的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-modified-since:<date>说明在date之后文档被修改了的话,就执行请求的方法,即条件式的再验证。通常和服务器的last-modified响应头部配合使用。last-modified说明该资源最后一次的修改时间。如果资源的这个属性发生变化,则说明缓存已经失效。则服务器会返回最新的资源。否则会返回304 not modified响应。

这种方式的好处在于,如果资源未失效,则无需重传资源,可以有效的节省带宽。

与之相类似的有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方法写入缓存。该方法会返回一个输出流用于存储缓存。

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
> 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 Map
responses; 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 ConcurrentHashMap
map; 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 {        ConcurrentHashMap
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

转载地址:http://tusnl.baihongyu.com/

你可能感兴趣的文章
四大超能力加持,京东家电双11效率致胜
查看>>
关于Golang语言的web编程的实例及常见问题
查看>>
JDK5.0新特性(2)-注解(Annotation)
查看>>
C# 中的委托和事件
查看>>
代替Runas,自动输入密码
查看>>
老师的斐波那契数列算法
查看>>
AutoLISP图像控件Image应用示例
查看>>
MVVM中的命令绑定及命令参数
查看>>
动态加载JS脚本【转】
查看>>
单元测试
查看>>
ORALCE存储之ROWID
查看>>
[php]php设计模式 Composite (组合模式)
查看>>
SQL Server之事务基础知识
查看>>
win7 x64 下VC6的调试程序无法退出的问题
查看>>
VBA之四----给程序自动加行号
查看>>
Windows 下 Nginx + PHP5 的安装与配置
查看>>
在WCF服务中获取客户端的IP地址和端口号
查看>>
【技术贴】所有好友的QQ空间都打不开进不去的超简单解决办法!
查看>>
这种写法用过没:string.Format("{0,-10}", 8)
查看>>
Google Map Logs
查看>>