资讯专栏INFORMATION COLUMN

聚焦http协议缓存策略(RFC7234)在okhttp中的实现

sanyang / 1540人阅读

摘要:计算缓存从产生开始到现在的年龄。响应头中的时间,文件在缓存服务器中存在的时间。响应码为表示经服务器效验缓存的响应式有效的可以使用,更新缓存年龄。小结通过本篇我们知道了协议的缓存策略,已经在中是如何实践的。

前言

分析基于okhttp v3.3.1

Okhttp处理缓存的类主要是两个CacheIntercepter缓存拦截器,以及CacheStrategy缓存策略。 CacheIntercepter在Response intercept(Chain chain)方法中先得到chain中的request然后在Cache获取到Response,然后将Request和Respone交给创建CahceStrategy.Factory对象,在对象中得到CacheStrategy。代码看的更清晰:

@Override public Response intercept(Chain chain) throws IOException { //cache中取Response对象cacheCandidate Response cacheCandidate = cache != null ");

1、 关于RFC7234在Okhttp中的实现

1.1、 获取CacheStrategy缓存策略

看下CacheStrategy.Factory使用原始的Request和在缓存中得到的Response对象CacheCandidate,怎样生成CacheStrategy的。 CacheStrategyFactory的生成

... public Factory(long nowMillis, Request request, Response cacheResponse) { //获取当前时间 this.nowMillis = nowMillis; this.request = request; this.cacheResponse = cacheResponse; if (cacheResponse != null) { this.sentRequestMillis = cacheResponse.sentRequestAtMillis(); this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis(); Headers headers = cacheResponse.headers(); for (int i = 0, size = headers.size(); i < size; i++) { String fieldName = headers.name(i); String value = headers.value(i); if ("Date".equalsIgnoreCase(fieldName)) { //取出缓存响应当时服务器的时间 servedDate = HttpDate.parse(value); servedDateString = value; } else if ("Expires".equalsIgnoreCase(fieldName)) { //取出过期时间 expires = HttpDate.parse(value); } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { //取出最后一次更改时间 lastModified = HttpDate.parse(value); lastModifiedString = value; } else if ("ETag".equalsIgnoreCase(fieldName)) { //取出etag etag = value; } else if ("Age".equalsIgnoreCase(fieldName)) { // ageSeconds = HttpHeaders.parseSeconds(value, -1); } } } }

1.2、 CacheStrategy生成,缓存策略的生成。

缓存策略最终会产生三种策略中的一种:

直接使用缓存

不使用缓存

有条件的使用缓存

CacheStrategy中最后request为空表示可以使用缓存,如果Response为空表示不能使用缓存 如果都为空 说明不能使用直接返回504

具体判断

    判断本地是否有cacheReponse 如果没有直接返回new CacheStrategy(request, null)

    判断https的handshake是否丢失 如果丢失直接返回 return new CacheStrategy(request, null)

    判断response和request里的cache-controlheader的值如果有no-store直接返回 return new CacheStrategy(request, null);

    如果request的cache-contro 的值为no-cache或者请求字段有“If-Modified-Sine”或者“If-None—Match”(这个时候表示不能直接使用缓存了)直接返回 return new CacheStrategy(request, null); 5.判断是否过期,过期就带有条件的请求,未过期直接使用。

源码上加了注释

/** Returns a strategy to use assuming the request can use the network. */ private CacheStrategy getCandidate() { // 在缓存中没有获取到缓存 if (cacheResponse == null) { return new CacheStrategy(request, null); } // https不满足的条件下不使用缓存 if (request.isHttps() && cacheResponse.handshake() == null) { return new CacheStrategy(request, null); } //Request和Resonse中不满足缓存的条件 if (!isCacheable(cacheResponse, request)) { return new CacheStrategy(request, null); } //在header中存在着If-None-Match或者If-Modified-Since的header可以作为效验,或者Cache-control的值为noCache表示客户端使用缓存资源的前提必须要经过服务器的效验。 CacheControl requestCaching = request.cacheControl(); if (requestCaching.noCache() || hasConditions(request)) { return new CacheStrategy(request, null); } //缓存的响应式恒定不变的 CacheControl responseCaching = cacheResponse.cacheControl(); if (responseCaching.immutable()) { return new CacheStrategy(null, cacheResponse); } //计算响应缓存的年龄 long ageMillis = cacheResponseAge(); //计算保鲜时间 long freshMillis = computeFreshnessLifetime(); //Request中保鲜年龄和CacheResponse中保鲜年龄取小 if (requestCaching.maxAgeSeconds() != -1) { freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds())); } //取Request的minFresh(他的含义是当前的年龄加上这个日期是否还在保质期内) long minFreshMillis = 0; if (requestCaching.minFreshSeconds() != -1) { minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds()); } //当缓存已经过期且request表示能接受过期的响应,过期的时间的限定。 long maxStaleMillis = 0; if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) { maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds()); } //判断缓存能否被使用 if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { Response.Builder builder = cacheResponse.newBuilder(); //已经过期但是能使用 if (ageMillis + minFreshMillis >= freshMillis) { builder.addHeader("Warning", "110 HttpURLConnection "Response is stale""); } //缓存的年龄已经超过一天的时间 long oneDayMillis = 24 * 60 * 60 * 1000L; if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) { builder.addHeader("Warning", "113 HttpURLConnection "Heuristic expiration""); } return new CacheStrategy(null, builder.build()); } // Find a condition to add to the request. If the condition is satisfied, the response body // will not be transmitted. String conditionName; String conditionValue; if (etag != null) { conditionName = "If-None-Match"; conditionValue = etag; } else if (lastModified != null) { conditionName = "If-Modified-Since"; conditionValue = lastModifiedString; } else if (servedDate != null) { conditionName = "If-Modified-Since"; conditionValue = servedDateString; } else { return new CacheStrategy(request, null); // No condition! Make a regular request. } Headers.Builder conditionalRequestHeaders = request.headers().newBuilder(); Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue); Request conditionalRequest = request.newBuilder() .headers(conditionalRequestHeaders.build()) .build(); return new CacheStrategy(conditionalRequest, cacheResponse); }

1.2.1、 判断是否过期

判断是否过期的依据:ageMillis + minFreshMillis < freshMillis + maxStaleMillis,(当前缓存的年龄加上期望有效时间)小于(保鲜期加上过期但仍然有效期限)

特别的当respone headercache-control:must-revalidate时表示不能使用过期的cache也就是maxStaleMillis=0。

//计算缓存从产生开始到现在的年龄。 long ageMillis = cacheResponseAge(); //计算服务器指定的保鲜值 long freshMillis = computeFreshnessLifetime(); //请求和响应的保鲜值取最小 if (requestCaching.maxAgeSeconds() != -1) { freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds())); } //期望在指定时间内的响应仍然有效,这是request的期望 long minFreshMillis = 0; if (requestCaching.minFreshSeconds() != -1) { minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds()); } //接受已经过期的响应时间, long maxStaleMillis = 0; if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) { maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds()); } //规则,缓存的年龄加上期望指定的有效时间期限小于实际的保鲜值加上过期时间 if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { //虽然缓存能使用但是已经过期了这时候要在header内加提醒 Response.Builder builder = cacheResponse.newBuilder(); if (ageMillis + minFreshMillis >= freshMillis) { builder.addHeader("Warning", "110 HttpURLConnection "Response is stale""); } //缓存已经超过一天了虽然还没有过期加提醒 long oneDayMillis = 24 * 60 * 60 * 1000L; if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) { builder.addHeader("Warning", "113 HttpURLConnection "Heuristic expiration""); } return new CacheStrategy(null, builder.build()); }

1.2.2、计算缓存从产生开始到现在的年龄(RFC 7234)

确定缓存的年龄的参数是一下四个。

发起请求时间:sentRequestMillis。

接收响应时间:receivedResponseMillis。

当前时间:nowMillis。

响应头中的age时间:ageSeconds,文件在缓存服务器中存在的时间。

根据RFC 7234计算的算法如下。

private long cacheResponseAge() { //接收到的时间减去资源在服务器端产生的时间得到apparentReceivedAge long apparentReceivedAge = servedDate != null ");

1.2.3、计算CacheRespone中的保鲜期

如果在CacheResone的cacheContro中获取maxAgeSecends就是保鲜器

否则,就尝试在expires header中获取值减去服务器响应的时间就是保鲜期

否在,服务器时间减去lastmodifide的时间的十分之一做为保鲜期。

private long computeFreshnessLifetime() { //cacheRespone中的control CacheControl responseCaching = cacheResponse.cacheControl(); if (responseCaching.maxAgeSeconds() != -1) { //如果cacheContro中存在maxAgeSecends直接使用 return SECONDS.toMillis(responseCaching.maxAgeSeconds()); } else if (expires != null) { //如果没有max-age就使用过期时间减去服务器产生时间 long servedMillis = servedDate != null ");

2、对响应CacheStrategy的处理

当networkRequest为空且cahceResponse为空的时候,表示可以使用缓存且现在的缓存不可用,返回504。responecode 504的语义:但是没有及时从上游服务器收到请求。

当networRequest为空且cacheResponse不为空表示,可以使用缓存且缓存可用,就可以直接返回缓存response了。

当networkRequest为空的时候,表示需要和服务器进行校验,或者直接去请求服务器。

响应码为304表示经服务器效验缓存的响应式有效的可以使用,更新缓存年龄。

响应码不为304更新缓存,返回响应;且有可能响应式不可用的,返回body为空,header有信息的respone。

@Override public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache != null ");

3、解惑

3.1、 ETAG和if-Modified-Sine什么时候生效

在CacheSategry里从respone中获取的,如果存在Etag或者Modify的字段就只用在ConditionalRequet设置对应的值做请求了,带有条件的请求,去服务器验证。

3.2、Request中的no-cache的语义以及和no-store的区别

nocache的意思是不使用不可靠的缓存响应,必须经过服务器验证的才能使用

CacheStrategy#Factory#getCandidate()中

CacheControl requestCaching = request.cacheControl(); if (requestCaching.noCache() || hasConditions(request)) { return new CacheStrategy(request, null); }

在CacheStrategy中的判断逻辑是:当request中请求头中cache-control的值为no-cache的时候或者请求头中存在if-none-match或者if-modified-sine的时候直接去请求服务放弃缓存。

3.3、 什么条件下会使用缓存呢

当缓存可用,没有超过有效期,且不需要经过验证的时候可以直接从缓存中获取出来。

需要经过验证,经过服务端验证,响应码为304表示缓存有效可以使用。

3.4、 must-revalidate 、no-cache、max-age = 0区别

must-revalidate(响应中cacheContorl的值)
如果过期了就必须去服务器做验证不能够使用过期的资源,这标志了max-StaleSe失效

no-cache 只能使用源服务效验过的respone,不能使用未经效验的respone。

根据Okhttp代码中处理的策略是在request中cache-control为no-cache的时候,就直接去服务端去请求,如果需要添加额外的条件需要自己手动去添加。

在respone中cache-control的条件为no-cache的时候表示客户端使用cache的时候需要经过服务器的验证,使用If-None-Match或者If-Modified-Since。

max-age=0表示保质期为0,表示过了保鲜期,也就是需要去验证了。

4、小结

通过本篇我们知道了http协议的缓存策略,已经在okhttp中是如何实践的。总的来说会有这样几个步骤

判断是否符合使用缓存的条件,是否有响应缓存,根据cache-contorl字段缓存是否可用,是否过期。

如果需要服务器端进行验证,主要是两种方式request的header中 if-none-match:etag和If-Modified-Since:时间戳。

响应码304表示验证通过,非304表示缓存不可用更新本地缓存。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/6709.html

相关文章

  • 前端开发中常见的 HTTP 相关知识(一)

    摘要:前言前端开发中总是要和接口和缓存打交道,所以相关内容多多少少还是要知道一些,干起活来才能事半功倍。处于中的应用层。部分安全性问题发布于年的版本,也是当前的最新标准。基于谷歌提出的而来,之前用于浏览器中来访问的加密服务,在发布后功成身退。 前言 前端开发中总是要和接口和缓存打交道,所以HTTP相关内容多多少少还是要知道一些,干起活来才能事半功倍。下面我从业务出发,简单说下一些可能会碰到的...

    刘东 评论0 收藏0
  • 重学前端学习笔记(十一)--浏览器工作解析(一)

    摘要:紧跟在后面的是请求头,每行用冒号分隔名称和值按下两次回车,收到服务端回复响应部分第一行被称作,它也分为三个部分,协议和版本状态码和状态文本。对前端来说系列的状态码是非常陌生的,原因是的状态被浏览器库直接处理掉了,不会让上层应用知晓。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要...

    paulli3 评论0 收藏0
  • 重学前端学习笔记(十一)--浏览器工作解析(一)

    摘要:紧跟在后面的是请求头,每行用冒号分隔名称和值按下两次回车,收到服务端回复响应部分第一行被称作,它也分为三个部分,协议和版本状态码和状态文本。对前端来说系列的状态码是非常陌生的,原因是的状态被浏览器库直接处理掉了,不会让上层应用知晓。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要...

    付永刚 评论0 收藏0
  • 重学前端学习笔记(十一)--浏览器工作解析(一)

    摘要:紧跟在后面的是请求头,每行用冒号分隔名称和值按下两次回车,收到服务端回复响应部分第一行被称作,它也分为三个部分,协议和版本状态码和状态文本。对前端来说系列的状态码是非常陌生的,原因是的状态被浏览器库直接处理掉了,不会让上层应用知晓。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要...

    alighters 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<