本文将根据最近所学的Java网络编程实现一个简单的基于URL的缓存。本文将涉及以下内容:java
正常来讲,服务器和客户端的HTTP通讯须要首先经过TCP的三次握手创建链接,而后客户端再发出HTTP请求并接收服务器的响应。可是,在有些时候,服务器的资源并无发生改变。此时重复的向服务器请求一样的资源会带来带宽的浪费。针对这种状况咱们能够采用缓存的方式,既能够是本地缓存,也能够是代理服务器的缓存,来减小对服务器资源的没必要要的访问。从而一方面减小了响应时间,另外一方面减小了服务器的压力。面试
那么咱们如何知道,什么时候能够直接使用缓存,什么时候由于当前的缓存已通过时,须要从新向资源所在的服务器发出请求呢?编程
HTTP1.0和HTTP1.1分别针对缓存提供了一些HEADER属性供链接双方参考。须要注意,若是是HTTP1.0的服务器,将没法识别HTTP1.1的缓存属性。因此有时候为了向下兼容性,咱们会设置多个和缓存相关的属性。固然,它们彼此之间是存在优先级的,后面将会详细介绍。缓存
支持HTTP1.0,说明该资源在Expires内容以后过时。Expires关键字使用的是绝对日期。服务器
支持HTTP1.1,使用相对日期对缓存进行管理。它可定义的属性包括:max-age=[seconds]
: 当前时间通过n秒后缓存资源失效s-maxage=[seconds]
: 从共享缓存获取的数据在n秒后失效,私有缓存每每能够更久一些public
: 代表响应能够被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。private
: 代表响应只能被单个用户缓存,不能做为共享缓存(即代理服务器不能缓存它)。no-cache
: 容许缓存,但每次访问缓存时必须从新验证缓存的有效性no-store
: 缓存不该存储有关客户端请求或服务器响应的任何内容。must-revalidate
: 缓存必须在使用以前验证旧资源的状态,而且不可以使用过时资源。
还有许多相关的属性,想要详细了解的话能够参考这篇文章。微信
仅仅是已缓存文档的过时并不意味这它和原始服务器上目前处于活跃状态的资源有实际的区别,只是意味着到了要核实的时间。这种状况称为服务器再验证。网络
if-modified-since:<date>
说明在date以后文档被修改了的话,就执行请求的方法,即条件式的再验证。一般和服务器的last-modified
响应头部配合使用。last-modified
说明该资源最后一次的修改时间。若是资源的这个属性发生变化,则说明缓存已经失效。则服务器会返回最新的资源。不然会返回304 not modified响应。数据结构
这种方式的好处在于,若是资源未失效,则无需重传资源,能够有效的节省带宽。ide
与之相相似的有if-unmodified-since
,该属性的意思是若是资源在该日期以后被修改了,则不执行请求方法。一般在进行部分文件传输时,获取文件的其他部分以前要确保文件未发生变化,此时这个首部颇有用。fetch
有些时候,仅仅是使用最后修改日期再验证是不够的:
为此,HTTP提供了实体标签(ETag)的比较。当发布者对文档进行修改时,能够修改文档的实体标签来讲明新的版本。这样,只要实体标签改变,缓存就能够用If-None-Match
条件首部来获取新的副本。
服务器在响应中会标记当前资源的ETag。一旦文档过时后,可使用HEAD请求来条件式再验证。若是服务器上ETag改变,则会返回最新的资源。固然,ETag能够包含多个内容,说明本地存储了多个版本的副本。若是没有命中这些副本,再返回完整资源。
If-None-Match: "v2.4","v2.5","v2.6"
若是服务器收到的请求中既带有if-modified-since
,又带有实体标签条件首部,那么只有这两个条件都知足时,才会返回304 not modified响应。
默认状况下。JAVA不缓存任何任何内容。咱们须要经过本身的实现来支持URL的缓存。咱们须要实现如下抽象类:
这里其实使用的是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方法写入缓存。该方法会返回一个输出流用于存储缓存。
如今我须要实现缓存,我将会在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(); } } }
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注个人微信公众号!将会不按期的发放福利哦~
HTTP 权威指南 Java Network Programming