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

1,376 阅读10分钟

前言

分析基于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;
    ...
    ...

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

具体判断

  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.判断是否过期,过期就带有条件的请求,未过期直接使用。

源码上加了注释

 /** 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,(当前缓存的年龄加上期望有效时间)小于(保鲜期加上过期但仍然有效期限)

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.2、计算缓存从产生开始到现在的年龄(RFC 7234)

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

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

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

    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.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
            ? 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;
    }

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
        ? 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、解惑

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表示缓存不可用更新本地缓存。