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

前言

分析基于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
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //建立Cache.Strategy对象调用其get()方法获得对应的CacheStragy
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //取出strategy中的 Request和cacheRespone
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    ...
    ...
复制代码

一、 关于RFC7234在Okhttp中的实现

1.一、 获取CacheStrategy缓存策略

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

...
    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.二、 CacheStrategy生成,缓存策略的生成。

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

  • 直接使用缓存
  • 不使用缓存
  • 有条件的使用缓存

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

具体判断ide

  1. 判断本地是否有cacheReponse 若是没有直接返回new CacheStrategy(request, null)
  2. 判断https的handshake是否丢失 若是丢失直接返回 return new CacheStrategy(request, null)
  3. 判断response和request里的cache-controlheader的值若是有no-store直接返回 return new CacheStrategy(request, null);
  4. 若是request的cache-contro 的值为no-cache或者请求字段有“If-Modified-Sine”或者“If-None—Match”(这个时候表示不能直接使用缓存了)直接返回 return new CacheStrategy(request, null); 5.判断是否过时,过时就带有条件的请求,未过时直接使用。

源码上加了注释ui

/** 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.一、 判断是否过时

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

image
特别的当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.二、计算缓存从产生开始到如今的年龄(RFC 7234)

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

  • 发起请求时间:sentRequestMillis。
  • 接收响应时间:receivedResponseMillis。
  • 当前时间:nowMillis。
  • 响应头中的age时间:ageSeconds,文件在缓存服务器中存在的时间。
    image

根据RFC 7234计算的算法以下。spa

private long cacheResponseAge() {
    //接收到的时间减去资源在服务器端产生的时间获得apparentReceivedAge
      long apparentReceivedAge = servedDate != null
          ? Math.max(0, receivedResponseMillis - servedDate.getTime())
          : 0;
    //age字段的时间和上一步计算的时间去大值
    
      long receivedAge = ageSeconds != -1
          ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
          : apparentReceivedAge;
    //接受时间减去发送时间
      long responseDuration = receivedResponseMillis - sentRequestMillis;
     //上一次响应时间到如今为止的差值
      long residentDuration = nowMillis - receivedResponseMillis;
     //三者相加获得cache的age
      return receivedAge + responseDuration + residentDuration;
    }
复制代码

1.2.三、计算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
            ? servedDate.getTime()
            : receivedResponseMillis;
        long delta = expires.getTime() - servedMillis;
        return delta > 0 ? delta : 0;
      } else if (lastModified != null
      //若是上述条件都不知足则使用lastModified字段计算,计算规则就是服务器最后响应时间和资源最后更改时间的十分之一做为保质期
          && cacheResponse.request().url().query() == null) {
        // As recommended by the HTTP RFC and implemented in Firefox, the
        // max age of a document should be defaulted to 10% of the
        // document's age at the time it was served. Default expiration // dates aren't used for URIs containing a query.
        long servedMillis = servedDate != null
            ? servedDate.getTime()
            : sentRequestMillis;
        long delta = servedMillis - lastModified.getTime();
        return delta > 0 ? (delta / 10) : 0;
      }
      //若是上述条件都不知足则直接返回0
      return 0;
    }
复制代码

二、对响应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
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it. } // 当networkRequest为空且cahceResponse为空的时候, //表示可使用缓存且如今的缓存不可用,返回504。 if (networkRequest == null && cacheResponse == null) { return new Response.Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(Util.EMPTY_RESPONSE) .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); } //直接使用缓存,缓存是不可变的 if (networkRequest == null) { return cacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); } //正常去作请求 Response networkResponse = null; try { networkResponse = chain.proceed(networkRequest); } finally { // If we're crashing on I/O or otherwise, don't leak the cache body. if (networkResponse == null && cacheCandidate != null) { closeQuietly(cacheCandidate.body()); } } // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
        //返回的结果responsecode 为304资源在服务器效验以后是没有改变的
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        
        //更新缓存
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    
    
    //若是不是304表示有变化
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    //写入缓存
    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
    //不能缓存移除
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

  private static Response stripBody(Response response) {
    return response != null && response.body() != null
        ? response.newBuilder().body(null).build()
        : response;
  }

复制代码

三、解惑

3.一、 ETAG和if-Modified-Sine何时生效

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

3.二、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.三、 什么条件下会使用缓存呢

  • 当缓存可用,没有超过有效期,且不须要通过验证的时候能够直接从缓存中获取出来。
  • 须要通过验证,通过服务端验证,响应码为304表示缓存有效可使用。

3.四、 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,表示过了保鲜期,也就是须要去验证了。

四、小结

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

  • 判断是否符合使用缓存的条件,是否有响应缓存,根据cache-contorl字段缓存是否可用,是否过时。
  • 若是须要服务器端进行验证,主要是两种方式request的header中 if-none-match:etag和If-Modified-Since:时间戳。
  • 响应码304表示验证经过,非304表示缓存不可用更新本地缓存。

相关文章
相关标签/搜索