摘要:从而一方面减少了响应时间,另一方面减少了服务器的压力。表明响应只能被单个用户缓存,不能作为共享缓存即代理服务器不能缓存它。这种情况称为服务器再验证。否则会返回响应。
前言
本文将根据最近所学的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
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/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 (...
阅读 2104·2021-11-24 09:39
阅读 1494·2019-08-30 15:44
阅读 1945·2019-08-29 17:06
阅读 3392·2019-08-29 16:32
阅读 3542·2019-08-29 16:26
阅读 2654·2019-08-29 15:35
阅读 3025·2019-08-29 12:50
阅读 1635·2019-08-29 11:15