Android-OkHttp3-拦截器

OkHttp3中有五大拦截器,分别是重试和重定向拦截器(RetryAndFollowUpInterceptor)、桥接拦截器(BridgeInterceptor)、缓存拦截器(CacheInterceptor)、连接拦截器(ConnectInterceptor)、请求服务器拦截器(CallServerInterceptor)

  • 重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会启动执行所有拦截器。
  • 桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。
  • 缓存拦截器在交出之前,读取并判断是否使用缓存;获得结果后判断是否保存缓存。
  • 连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果之后不进行额外的处理。
  • 请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。
    本文源码解析基于OkHttp3.14
// RealCall.java
  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }
// RealInterceptorChain.java
// 第一次进入,index是0
  public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
      throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.exchange != null && !this.exchange.connection().supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.exchange != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    // 调用index+1的拦截器的RealInterceptorChain
    RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
    // 取出第0个拦截器
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (exchange != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

    return response;
  }

一、重试和重定向拦截器(RetryAndFollowUpInterceptor)

重试和重定向拦截器主要分为重试和重定向两部分。重试和重定向拦截器,只处理响应结果

1.重试

RetryAndFollowUpInterceptor拦截器完成,主要工作是intercept方法中进行。

  • (1)接收请求发出的时候产生的异常,判断是路由异常还是IO异常
  • (2)通过调用recover方法进行重试
  • (3)重试时判断是否允许重试、判断是不是重试异常、判断是否有可以用来连接的路由线路
  • (4)在允许重试的情况下,出现协议异常、SSL握手异常中证书出现问题、SSL握手异常,这里的SSL握手导致的异常,第一种就是有证书,但是验证失败,第二种其实就是没有证书,出现这样的异常是不可以重试的
  • (5)如果允许重试,则有更多的线路
(1)RetryAndFollowUpInterceptor.intercept()
@Override public Response intercept(Chain chain) throws IOException {
  Request request = chain.request();
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  // Transmitter对象,是在RealCall中初始化创建的,在Transmitter中就会初始化创建ConnectionPool
  // 而ConnectionPool其实比较类似装饰者模式,封装RealConnectionPool
  Transmitter transmitter = realChain.transmitter();

  int followUpCount = 0;
  Response priorResponse = null;
  // 重试和重定向拦截器内部是采用一个无限死循环的while循环,这样做的目的就是为了在出现错误的时候可以进行请求重试等操作
  while (true) {
    transmitter.prepareToConnect(request);

    if (transmitter.isCanceled()) {
      throw new IOException("Canceled");
    }

    Response response;
    boolean success = false;
    try {
      // 调用RealInterceptorChain.proceed方法,这个RealInterceptorChain对象
      // 是在getResponseWithInterceptorChain()第一次调用RealInterceptorChain.proceed
      // 的时候,在其内部创建的index=1的RealInterceptorChain对象。
      response = realChain.proceed(request, transmitter, null);
      success = true;
    } catch (RouteException e) {
      // 路由溢出,连接未成功,请求没有发出去,在这里进行重试
      // 如果为true,说明可以进行重试,则不抛出异常,并且调用continue
      // 这样就继续执行下一次while循环,继续执行请求操作
      if (!recover(e.getLastConnectException(), transmitter, false, request)) {
        throw e.getFirstConnectException();
      }
      continue;
    } catch (IOException e) {
      // 请求发出去了,但是和服务器通信失败了(socket流正在读写数据的时候断开连接)
      boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
      if (!recover(e, transmitter, requestSendStarted, request)) throw e;
      continue;
    } finally {
      // The network call threw an exception. Release any resources.
      if (!success) {
        transmitter.exchangeDoneDueToException();
      }
    }

    // Attach the prior response if it exists. Such responses never have a body.
    if (priorResponse != null) {
      response = response.newBuilder()
          .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
          .build();
    }

    Exchange exchange = Internal.instance.exchange(response);
    Route route = exchange != null ? exchange.connection().route() : null;
    // 重定向判断,如果返回是null则不需要进行重定向
    Request followUp = followUpRequest(response, route);
    // 不需要重定向,则直接返回请求结果
    if (followUp == null) {
      if (exchange != null && exchange.isDuplex()) {
        transmitter.timeoutEarlyExit();
      }
      return response;
    }

    RequestBody followUpBody = followUp.body();
    if (followUpBody != null && followUpBody.isOneShot()) {
      return response;
    }

    closeQuietly(response.body());
    if (transmitter.hasExchange()) {
      exchange.detachWithViolence();
    }

    // 定义重定向的次数,最大为20次,一旦超过20次,则会抛出异常。
    if (++followUpCount > MAX_FOLLOW_UPS) {
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }

    request = followUp;
    priorResponse = response;
  }
}
(2)RetryAndFollowUpInterceptor.recover()

判断是否能够进行重试,如果能够则返回true表示可以进行重试。
不进行重试的情况:

  • 不允许重试的,则不进行重试
  • 只请求一次的,不进行重试;请求刚发起的不进行重试
  • 协议异常的不进行重试
  • 不是请求超时的不进行重试
  • SSL证书异常,比如证书损坏等不进行重试
  • SSL授权异常,比如过期等不进行重试
  • 没有更多路由路线的,不进行重试

反过来,可以进行重试的:满足下面所有条件

  • 设置允许进行重试
  • 不是只请求一次
  • 是超时请求
  • 不是SSL证书损坏
  • 不是SSL证书过期
  • 有多路由线路
private boolean recover(IOException e, Transmitter transmitter,
    boolean requestSendStarted, Request userRequest) {
  // 在配置okhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败则不再进行重试
  if (!client.retryOnConnectionFailure()) return false;

  // 由于requestSendStarted只在http2的io异常中为true,先不管http2
  // 这里的requestIsOneShot为true,表示只请求一次,那么也不重试
  if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

  // 判断是不是属于重试的异常,如果是,则返回false
  if (!isRecoverable(e, requestSendStarted)) return false;

  // 有没有可以用来连接的路由路线
  if (!transmitter.canRetry()) return false;

  // For failure recovery, use the same route selector with a new connection.
  return true;
}
(3)RetryAndFollowUpInterceptor.isRecoverable()

判断是否属于重试的异常,在一些情况下出现的重试异常是不能进行重试的,如果可以进行重试,可以判断是否有多路路由可以切换,有则切换一条路由进行重试。

private boolean isRecoverable(IOException e, boolean requestSendStarted) {
  // 出现协议异常,不能重试
  // 比如服务器返回的响应码是204或者205,但是response的body又是大于0的
  // 因为204的是服务器没返回任何内容
  // 205是服务器重置内容,但是没返回任何内容
  if (e instanceof ProtocolException) {
    return false;
  }

  // requestSendStarted认为它一直为false(不管http2),异常属于socket超时异常,直接判定可以重试
  if (e instanceof InterruptedIOException) {
    return e instanceof SocketTimeoutException && !requestSendStarted;
  }

  // SSL握手异常中,证书出现问题,不能重试
  if (e instanceof SSLHandshakeException) {
    // If the problem was a CertificateException from the X509TrustManager,
    // do not retry.
    if (e.getCause() instanceof CertificateException) {
      return false;
    }
  }
  // SSL握手未授权异常,不能重试
  if (e instanceof SSLPeerUnverifiedException) {
    // e.g. a certificate pinning error.
    return false;
  }

  return true;
}

在isRecoverable()方法中判断的异常主要包括下面三种:
(1)协议异常,如果是那么直接判定不能重试;(你的请求或者服务器的响应本身就存在问题,没有按照http协议来定义数据,再重试也没有用)
(比如在发起http请求的时候,没有携带host:XXXXX, 这就是一个协议异常)
(2)超时异常,可能由于网络波动造成了Socket管道的超时,那么是可以重试的。
(3)SSL证书异常或者SSL验证失败异常,前者是证书验证失败,后者可能就是压根没瘾证书,或者证书数据不正确,则不能进行重试。

2.重定向

如果请求结束后没有发生异常,并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重定向的判断。重定向的判断,其实是在整个责任链执行完成得到Response之后,再判断是否需要进行重定向的。
即在RetryAndFollowUpInterceptor.intercept方法中调用了followUpRequest方法来判断是否需要进行重定向。
(1)如果followUpRequest方法返回为null则不需要进行重定向
(2)而在调用followUpRequest方法进行判定是否需要重定向时,在OkHttp中对followUpRequest的次数做了判断,重定向次数最大为20次

(1)RetryAndFollowUpInterceptor.followUpRequest()

该方法其实就是在RRetryAndFollowUpInterceptor.intercept方法执行完完整的责任链之后,做重定向处理判断的。
重定向一般是30X的响应码的时候,才需要,此时从响应头中取出Location的值作为重定向的url;但是也有一些其他的响应码需要进行重定向,比如407,使用了代码,比如401需要身份验证,这些也需要进行重定向;如果是返回响应码408的话,用户请求超时,这个情况其实可以认为是重试,而不是重定向;如果是503的话,则是服务器不可用,但是要满足响应头的Retry-After值为0,表示服务器还无法处理请求,需要过多少秒进行请求,而重定向,则需要是在Retry-After=0的时候,其实这情况也是重试。

private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
  if (userResponse == null) throw new IllegalStateException();
  int responseCode = userResponse.code();

  final String method = userResponse.request().method();
  switch (responseCode) {
    // 407 客户端使用了HTTP代理服务器,在请求头中添加"Proxy-Author-ization",
    // 让代理服务器授权
    case HTTP_PROXY_AUTH:
      Proxy selectedProxy = route != null
          ? route.proxy()
          : client.proxy();
      if (selectedProxy.type() != Proxy.Type.HTTP) {
        throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
      }
      return client.proxyAuthenticator().authenticate(route, userResponse);

    case HTTP_UNAUTHORIZED:
      // 401 需要身份验证,有些服务器接口需要验证使用者身份,在请求头中添加Authorization
      // 所以添加Authorization请求头的方式,对于Okhttp3请求来说可以有专门的方法
      // 通过给OkHttpClient.Builder的authenticator方法设置
      return client.authenticator().authenticate(route, userResponse);

    case HTTP_PERM_REDIRECT:
    case HTTP_TEMP_REDIRECT:
      // 308 永久重定向
      // 307 临时重定向
      // 如果请求方式不是GET活着HEAD,框架不会自动重定向
      if (!method.equals("GET") && !method.equals("HEAD")) {
        return null;
      }
      // fall-through
    case HTTP_MULT_CHOICE:
    case HTTP_MOVED_PERM:
    case HTTP_MOVED_TEMP:
    case HTTP_SEE_OTHER:
      // 300 301 302 303
      // 这部分如果是允许重定向,那么就需要根据响应头中的Location字段进行重定向
      // 然后取出Location字段的值
      // 如果用户不允许重定向,那就返回null
      if (!client.followRedirects()) return null;
      // 从响应头中取出location
      String location = userResponse.header("Location");
      if (location == null) return null;
      // 根据location重新配置url
      HttpUrl url = userResponse.request().url().resolve(location);

      // 如果重新配置的url为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
      if (url == null) return null;

      // 如果重定向在http到https之间切换,需要检查用户是不是允许(默认是允许)
      boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
      if (!sameScheme && !client.followSslRedirects()) return null;

      // Most redirects don't include a request body.
      // 构建重定向的request的builder
      Request.Builder requestBuilder = userResponse.request().newBuilder();
      /**
      * 重定向请求中,只要不是PROPFIND请求,无论是POST还是其他的方法都要改为
      * GET请求方式,即只有PROPFIND请求才有请求体
      */
      // 请求不是get与head
      if (HttpMethod.permitsRequestBody(method)) {
        final boolean maintainBody = HttpMethod.redirectsWithBody(method);
        // 除了PROPFIND请求之外都改为GET请求
        if (HttpMethod.redirectsToGet(method)) {
          requestBuilder.method("GET", null);
        } else {
          RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
          requestBuilder.method(method, requestBody);
        }
        // 不是PROPFIND的请求,把请求头中关于请求体的数据删掉
        if (!maintainBody) {
          requestBuilder.removeHeader("Transfer-Encoding");
          requestBuilder.removeHeader("Content-Length");
          requestBuilder.removeHeader("Content-Type");
        }
      }

      // 在跨主机重定向时,删除身份验证请求头
      if (!sameConnection(userResponse.request().url(), url)) {
        requestBuilder.removeHeader("Authorization");
      }
      // 如果是需要重定向,那么就构建重定向的新请求,进行build,进行重定向请求
      return requestBuilder.url(url).build();

    case HTTP_CLIENT_TIMEOUT:
      // 408客户端请求超时
      // 408算是连接失败了,所以判断用户是不是允许重试
      if (!client.retryOnConnectionFailure()) {
        // The application layer has directed us not to retry the request.
        return null;
      }

      RequestBody requestBody = userResponse.request().body();
      if (requestBody != null && requestBody.isOneShot()) {
        return null;
      }

      // 如果是本身这次的响应就是重新请求的产物同时上一次之所以重新请求还是408
      // 那么就不再重新请求
      if (userResponse.priorResponse() != null
          && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
        // We attempted to retry and got another timeout. Give up.
        return null;
      }

      // 如果服务器告诉我们了Retry-After多久后重试,那框架不需要管
      if (retryAfter(userResponse, 0) > 0) {
        return null;
      }

      return userResponse.request();

    case HTTP_UNAVAILABLE:
      // 503服务器不可用,和408差不多,但是只在服务器告诉你Retry-After:0
      // 意思就是立即重试,才重新请求
      if (userResponse.priorResponse() != null
          && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
        // We attempted to retry and got another timeout. Give up.
        return null;
      }

      if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
        // specifically received an instruction to retry without delay
        return userResponse.request();
      }

      return null;

    default:
      return null;
  }
}

二、桥接拦截器

BridgeInterceptor用于连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设置请求头内容长度、编码、gzip压缩、cookie等;获取响应后保存Cookie等操作。

请求头 说明
Content-Type 请求体类型,如:application/x-www-form-urlencoded
Content-Length/Transfer-Encoding 请求体解析方式
Host 请求的主机站点
Connection: Keep-Alive 保持长连接
Accept-Encoding: gzip 接收响应支持gizp压缩
Cookie cookie身份辨别
User-Agent 请求的用户信息,如:操作系统、浏览器等

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事:
(1)保持cookie,在下次请求则会读取对应的数据设置进入请求头,默认的CookieJar不提供实现
需要通过OkhttpClient.builder().cookieJar().build()
在cookieJar()方法中,实现CookieJar接口,然后自己保存,自己load
(2)如果使用gzip返回的数据,则使用GzipSource包装便于解析

(1)BridgeInterceptor#intercept
  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
    // 首先补全请求头中的信息
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
    // 请求下一个拦截器,获取响应信息
    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);
    // 如果使用了gzip,则通过GzipSource包装请求体数据
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }

三、缓存拦截器

CacheInterceptor缓存拦截器,在发出请求之前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应(只存在GET请求的缓存)
步骤:

  • (1)从缓存中获得对应请求的响应缓存
  • (2)创建CacheStrategy,创建时会判断是否能够使用缓存,在CacheStrategy中存在两个成员:networkRequest和cacheResponse。组合情况如下:
networkRequest cacheResponse 说明
null not null 直接使用缓存
not null null 向服务器发起请求
null null 直接gg,okhttp直接返回504
not null not null 发起请求,若得到响应为304(无修改),则更新缓存响应并返回

networkRequest是网络请求对象,cacheResponse是缓存结果对象,通过这两个对象的组合进行判定是否需要执行下一步的拦截器。
如果是networkRequest为null的时候,需要考虑cacheResponse是否为null,如果cacheResponse不为null的话,则可以使用缓存,如果为null则返回504请求失败;如果networkRequest不为null,则需要判断cacheResponse是否为null,如果cacheResponse为null则进行网络请求,如果不为null,则发起请求,请求响应码为304的话,则使用缓存,否则使用请求结果。

  • (3)交给下一个责任链继续处理
  • (4)后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存GET请求的响应)

1.缓存策略

依赖于CacheStrategy。相关请求头和响应头。

响应头 说明 例子
Date 消息发送的时间 Date:Sat,18 Nov 2028 06:17:41 GMT
Expires 资源过期的时间 Expires:Sat, 18 Nov 2028 06:17:41 GMT
Last-Modified 资源最后修改时间 Last-Modified:Fri,22 Jul 2016 02:57:17 GMT
Etag 资源在服务器的唯一标识 ETag:"16df0-5383097a03d40"
Age 服务器用缓存响应请求,该缓存从产生到现在经过多长时间(秒) Age:3825683
Cache-Control - -
请求头 说明 例子
If-Modified-Since 服务器没有在指定的时间后修改请求对应资源,返回304(无修改) If-Modified-Since: Fri, 22 Jul 2016 02:57:17 GMT
If-None-Match 服务器将其余请求资源对应的Etag值进行比较,匹配返回304 If-None-Match:"16df0-5383097a03d40"
Cache-Control - -
(1)Cache-Control

Cache-Control可以在请求头中存在,也可以在响应头中存在,对应的value可以设置多种组合:
max-age=[秒]:资源最大有效时间
public:表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
private:表明该资源只能被单个用户缓存,默认是private
no-store:资源不允许被缓存
no-cache:请求不使用缓存(这里其实是对比缓存使用,通过比较Etag和Last-Modified)
immutable:响应资源不会改变
min-fresh=[秒]:请求缓存最小新鲜度(用户认为这个缓存有效的时长)
must-revalidate:可以缓存,但是必须再想服务器做校验
max-stale=[秒]:请求缓存过期后多久内仍然有效

(2)强制缓存的依据

Expires:服务器返回给客户端的数据到期时间,如果下一个请求的时间小于到期时间,则可以直接使用缓存服务器里的数据。Http1.0中的。所以这个的作用基本可以忽略。因为这个时间是由服务器来规定,而客户端的时间和服务器的时间是会存在误差的。
Cache-Control:private、publiic、max-age=xxx、no-cache、no-store这是一系列的缓存策略。
Expires和Cache-Control同时存在的时候,Cache-Control的优先级更高

(3)对比缓存的依据(Cache-Control设置为no-cache,是响应头)

Last-Modified/If-Modified-Since
对比缓存会依赖于Last-Modified/If-Modified-Since
当服务器返回数据的时候,告诉客户端Last-Modified时间
客户端再次请求If-Modified-Since与最后一次修改时间做对比,如果是大于Last-Modified则资源修改过,如果是小于Last-Modified,则没有修改过,返回304
Etag/If-None-Match(优先级比Last-Modified高)
Etag:当前资源在服务器的唯一标识UUID(生成规则由服务器规定)服务器返回
再次请求If-None-Match将Etag带到服务端去,服务器拿到If-None-Match之后,就会判断与etag是否相等,如果相等,就返回304
304一般都是网络请求框架自己处理,如果不是自己开发框架,则一般不会处理304。
当客户端需要发送相同的请求时,根据Date + Cache-control来判断是否缓存过期,如果过期了,会在请求中携带If-Modified-Since和If-None-Match两个头。两个头的值分别是响应中Last-Modified和ETag头的值。服务器通过这两个头判断本地资源未发生变化,客户端不需要重新下载,返回304响应。说明,请求头中有If-Modified-Since和If-None-Match两个,则会发起请求,这个请求是有可能返回304,表示缓存虽然过期,但是依然可以继续使用

缓存控制.png

2.缓存详细流程

如果从缓存中获得本次请求URL对应的Response,首先会从响应中获得以上数据备用。
CacheStrategy是一个缓存策略类,该类告诉CacheInterceptor是使用缓存还是使用网络请求;
Cache是封装了实际的缓存操作;
DiskLruCache:Cache基于DiskLruCache;
在CacheInterceptor中使用的是InternalCache对象,该对象其实是在OkHttp3中的Cache类中实现的接口实现类对象

  @Override public Response intercept(Chain chain) throws IOException {
    // OkHttp的缓存拦截器,只对get请求有效。并且需要配置cache
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    // 这里的cacheCandidate是从本地缓存中获取的Response
    // 根据请求对象Request获取到该接口请求对应的响应缓存
    // 该类其实就是缓存策略类,主要就是用来决定最后是进行网络请求还是从缓存获取
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    
    // 如果cache不为null,则更新统计指标
    // 主要是更新:请求次数、使用网络请求次数、使用缓存次数
    if (cache != null) {
      cache.trackResponse(strategy);
    }

    // 如果缓存的cacheResponse==null,说明缓存不可用,则关闭
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // 如果网络请求和本地缓存都为null,则构造一个504的Response结果
    // 提示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();
    }

    // 如果网络请求为null,而cacheResponse不为null,则直接使用缓存数据
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    // 网络请求不为null,那么有两种情况,一种是本地缓存为null,一种是不为null
    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());
      }
    }

    // 如果本地缓存结果不为null,那么判断网络请求的结果
    if (cacheResponse != null) {
      // 判断网络请求结果是否为304,如果是304,则使用本地缓存
      // 如果不是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());
      }
    }

    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;
  }
(1)CacheStrategy

缓存策略判断流程:

  • 缓存是否存在,不存在则进行网络请求。即cacheResponse=null
  • 如果是https请求,但是缓存中没有握手的信息,则不使用缓存cacheResponse=null
  • 响应码以及响应头判断。缓存中的响应码为200,203,204,300,301,404,405,410,414,501,308的情况下,只判断服务器设置了Cache-Control:no-store,则不使用缓存;如果是302,307则需要存在Expires:时间、CacheControl:max-age/public/private
  • 用户的请求配置。如果Cache-Control的值是noCache或者网络请求的Request中的If-Modified-Since或者If-None-Match不为null,第一个情况就是直接请求,第二种情况可能会返回304,表示缓存过期
  • 资源是否不变
  • 响应的缓存是否有效

CacheStrategy.Factory()
这里主要是通过工厂类对象,初始化本地缓存响应头字段,并且初始化CacheStrategy缓存策略类对象

public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = cacheResponse;
  // 如果该请求的对应的本地缓存不为null,则取出该缓存的响应头字段
  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++) {
      // 遍历响应头,取出对应的字段name和value
      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)) {
        // 服务器返回给客户端的该数据资源在服务器端的uuid
        etag = value;
      } else if ("Age".equalsIgnoreCase(fieldName)) {
        ageSeconds = HttpHeaders.parseSeconds(value, -1);
      }
    }
  }
}

CacheStrategy.get()
CacheStrategy.get()是用来判断缓存的命中,会调用CacheStrategy的get()方法。其实就是通过一系列的缓存策略,通过是否使用缓存的一系列逻辑判断,构建一个CacheStrategy策略

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();

  // 如果可以使用缓存,那networkRequest必定为null;
  // 指定了只使用缓存但是networkRequest又不会null
  // 则会冲突,拦截器就会返回504,请求失败
  // 这里candidate的networkRequest不为null,且request的onlyIfCached为true是意味着不使用网络
  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // We're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
  }

  return candidate;
}

CacheStrategy.getCandidate()

private CacheStrategy getCandidate() {
  // TODO:第一步:判断缓存是否存在
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  // TODO:第二步:https请求的缓存。如果本地缓存没有握手,则进行网络请求
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  // If this response shouldn't have been stored, it should never be used
  // as a response source. This check should be redundant as long as the
  // persistence store is well-behaved and the rules are constant.
  // TODO:第三步:响应码以及响应头
  // 如果缓存不可用,则进行网络请求
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }

  // TODO:第四步:用户的请求配置
  // 如果Cache-Control的值是noCache或者网络请求的Request中的If-Modified-Since或者If-None-Match不为null,则进行网络请求
  // Request中有If-Modified-Since或者If-None-Match,说明本地缓存已经被判定是过期的
  // 则需要请求服务器,然后服务器会通过接收到的If-Modified-Since或者If-None-Match判断数据是否发生改变
  // 如果发生了改变,则返回200,并且返回新的数据,如果没有发生改变,则返回304,使用本地缓存数据
  // 这里的网络请求主要是请求查看是否还可以使用缓存
  // 但是OkHttp3的框架中,请求对象Request的CacheControl设置为noCache的时候
  // 表示的就是请求的时候不使用缓存
  // 请求头中设置Cache-Control:no-cache,表示是告诉服务器本地无缓存
  // 如果是响应头设置Cache-Control:no-cache则是表示需要重新请求服务器进行验证
  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }

  // TODO:第五步:资源是否不变
  CacheControl responseCaching = cacheResponse.cacheControl();
  /** 这是3.10的源码,在3.10中会判断缓存响应中是否存在Cache-Control: immutable
  * 如果存在,则响应资源将一直不会改变,可以使用缓存。
  *if (responseCaching.immutable()) {
  *  return new CacheStrategy(null, cacheResponse);
  *}
  */

  // TODO:第六步:响应的缓存是否有效
  // 6.1:获得缓存的响应从创建到现在的时间
  long ageMillis = cacheResponseAge();
  // 6.2:获取这个响应有效缓存时长(缓存有效时间)与minFreshMillis的区别
  // 其实就是minFreshMillis是请求认为的缓存有效时间
  long freshMillis = computeFreshnessLifetime();

  // 如果请求中指定了max-age表示指定了能拿的缓存有效时长,就需要总和响应
  // 有效缓存时长与请求能拿缓存的时长,获得最小能够使用响应缓存的时长
  // max-age是资源最大有效时间
  if (requestCaching.maxAgeSeconds() != -1) {
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }

  // 6.3:请求包含Cache-control: min-fresh=[秒] 能够使用还未过指定时间
  // 的缓存(用户请求认为的缓存有效时间,最小新鲜度)
  long minFreshMillis = 0;
  if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }

  // 6.4
  // 6.4.1:Cache-Control:must-revalidate 可缓存但是必须再向服务器进行确认
  // 6.4.2:Cache-Control:max-state=[秒] 缓存过期后还能使用指定的时长,
  // 如果未指定多少秒,则表示无论过期多长时间都可以;如果指定了,则只要是
  // 指定时间内就能使用缓存
  // 如果前者不满足,则自动忽略后者。只有不是必须向服务器进行确认的情况下
  // 才会判断缓存的过期时间
  long maxStaleMillis = 0;
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }

  // 6.5:不需要与服务器验证有效性 && 获得缓存的响应从创建到现在的时间 + 
  // 请求认为的缓存有效时间(最小新鲜度) 小于 缓存有效时间 + 过期后还可以使用的时间
  // 如果满足条件,则允许使用缓存
  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);
}

3.解析CacheStrategy.getCandidate()

CacheStrategy.getCandidate()其实就是获取CacheStrategy对象,其内部根据一些时间和请求头的配置

(1)第一步:缓存是否存在

cacheResponse是从缓存中找到的响应,如果为null,说明缓存不存在,则创建对应的CacheStrategy实例对象,且只存在networkRequest,即需要发起网络请求。

(2)第二步:https请求的缓存

如果本次是https缓存,但是缓存中缺少必要的握手信息,则缓存无效,依然创建对应的CacheStrategy实例对象,且只有networkRequest,即需要发起网络请求

(3)第三步:响应码以及响应头

判断逻辑在isCacheable方法中。这里其实就是判断该请求是否可以使用缓存

public static boolean isCacheable(Response response, Request request) {
  // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
  // This implementation doesn't support caching partial content.
  switch (response.code()) {
    case HTTP_OK:
    case HTTP_NOT_AUTHORITATIVE:
    case HTTP_NO_CONTENT:
    case HTTP_MULT_CHOICE:
    case HTTP_MOVED_PERM:
    case HTTP_NOT_FOUND:
    case HTTP_BAD_METHOD:
    case HTTP_GONE:
    case HTTP_REQ_TOO_LONG:
    case HTTP_NOT_IMPLEMENTED:
    case StatusLine.HTTP_PERM_REDIRECT:
      // These codes can be cached unless headers forbid it.
      break;

    case HTTP_MOVED_TEMP:
    case StatusLine.HTTP_TEMP_REDIRECT:
      // These codes can only be cached with the right response headers.
      // http://tools.ietf.org/html/rfc7234#section-3
      // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
      if (response.header("Expires") != null
          || response.cacheControl().maxAgeSeconds() != -1
          || response.cacheControl().isPublic()
          || response.cacheControl().isPrivate()) {
        break;
      }
      // Fall-through.

    default:
      // All other codes cannot be cached.
      return false;
  }

  // A 'no-store' directive on request or response prevents the response from being cached.
  return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}

缓存中的响应码为200,203,204,300,301,404,405,410,414,501,308的情况下,只判断服务器设置了Cache-Control:no-store(资源不能被缓存),如果服务器没有设置,则response.cacheControl().noStore()为false,取反即为true,那么在if (!isCacheable(cacheResponse, request))得到的就是false;如果服务器设置了资源不能被缓存,则说明需要进行网络请求,则直接创建CacheStrategy实例,且只传入networkRequest。
如果响应码是302,307(重定向),则需要进一步判断是否存在一些允许缓存的响应头。如果存在Expires,或者Cache-Control的值为max-age=[秒]:资源最大有效时间,public:表明该资源可以被任何用户缓存,比如客户端,private:表明该资源只能被单个用户缓存。
在这里Expires是服务器返回给客户端的数据到期时间
在同时没有设置Cahce-Control: no-store的时候,就可以进一步判断缓存是否可以使用。如果依然设置了Cache-Control: no-store,则不管是任何响应码,都不能缓存,都只能直接进行网络请求。
所以综合判断优先级如下:

  • 响应码不为200,203,204,300,301,404,405,410,414,501,308,302,307缓存不可用
  • 当响应码为302,307时,没有包含Expires或者Cache-Control没有设置max-age、public、private三个中的一个,则缓存不可用
  • 当存在Cache-Control:no-store响应头,则缓存不可用
(4)第四步:用户的请求配置
  // 请求头中设置Cache-Control:no-cache,表示是告诉服务器本地无缓存
  // 如果是响应头设置Cache-Control:no-cache则是表示需要重新请求服务器进行验证
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
private static boolean hasConditions(Request request) {
  return request.header("If-Modified-Since") != null 
          || request.header("If-None-Match") != null;
}

依然是判断资源是否允许缓存,如果是不允许的,则直接创建CacheStrategy对象实例,参数传入networkRequest对象。但是如果是允许的,则在这里就需要先对用户本次发起的请求进行判定,如果用户指定了If-Modified-Since或者If-None-Match(因为这里还没到缓存过期处理部分),说明可能是大于最后一次修改时间或者是资源id并不匹配,所以需要重新请求。如果允许缓存,且请求头中没有If-Modified-Since和If-None-Match均为null,则说明有缓存,可以进行下一步的判断缓存是否可以使用。

(5)第五步:资源是否不变

在OkHttp3.10的源码中,CacheStrategy.getCandidate()中存在下面判断:

if (responseCaching.immutable()) {
    return new CacheStrategy(null, cacheResponse);
}

但是在OkHttp3.14的源码中,这部分判断已经去除。如果响应头中包含了Cache-Control: immutable,则说明资源不会改变,则在用户请求时,可以直接使用缓存而不需要请求,所以在创建CacheStrategy对象实例的时候,networkRequest参数传的是null,而Response传入的就是cacheResponse。

(6)响应的缓存有效期

在这里,进一步根据缓存响应中的信息判断是否处于有效期内。如果满足:
缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长
缓存存活时间:其实就是缓存从创建到现在已经持续了多久的时间,在OkHttp的CacheStrategy.getCandidate()方法中,即ageMillis

private long cacheResponseAge() {
  // 代表客户端收到响应到服务器发出响应的一个时间差
  // servedDate是从缓存中获得Data响应头对应的时间(服务器发出本响应的时间)
  // receivedResponseMillis为本次响应(cacheResponse)对应的客户端接收到响应的时间
  long apparentReceivedAge = servedDate != null
      ? Math.max(0, receivedResponseMillis - servedDate.getTime())
      : 0;
  // 代表客户端的缓存在收到时已经存在多久(在服务器中存在多久)
  // 这个其实是根据请求头中key为Age的值,与Cache-Control: max-age不同
  // ageSeconds是从响应头中获取的Age响应头对应的秒数(本地缓存的响应是由服务器
  // 的缓存返回,这个缓存在服务器存在的时间)
  // ageSeconds与上一步的计算结果apparentReceivedAge的最大值为收到响应时,
  // 这个响应数据已经存在多久。
  // 这里这样做,其实可能是服务器刚发出本响应的时候创建的缓存,那么age的时间
  // 可能就会小于客户端接收到响应的时间-服务器发出响应的时间的差值
  long receivedAge = ageSeconds != -1
      ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
      : apparentReceivedAge;
  // responseDuration是缓存对应的请求,在发送请求与接收请求之间的时间差
  long responseDuration = receivedResponseMillis - sentRequestMillis;
  // 是这个缓存接收到的时间到现在的一个时间差
  long residentDuration = nowMillis - receivedResponseMillis;
  // 缓存在服务器已经存在的时间+客户端接收请求与发送请求的时间差+
  // 接收到响应缓存到现在的时间差
  // 本次请求去获取缓存,这个缓存是上一次请求保存的
  return receivedAge + responseDuration + residentDuration;
}

缓存新鲜度:即这个响应有效缓存的时长,即freshMillis
缓存新鲜度(有效时长)的判断会有多种情况,按优先级排列如下:

  • 缓存响应包含Cache-Control: max-age=[秒]资源最大有效时间
  • 缓存响应包含Expires: 时间,则通过Date或接收该响应时间计算资源有效时间
  • 缓存响应包含Last-Modified: 时间,则通过Date或发送该响应对应请求的时间计算资源有效时间;并且根据建议以及在FireFox浏览器的实现,使用得到结果的10%来作为资源的有效时间
private long computeFreshnessLifetime() {
  CacheControl responseCaching = cacheResponse.cacheControl();
  // 如果资源最大有效时间存在,则直接返回该值作为缓存新鲜度
  if (responseCaching.maxAgeSeconds() != -1) {
    return SECONDS.toMillis(responseCaching.maxAgeSeconds());
  } else if (expires != null) {
    // expires是数据到期时间
    // servedDate是从缓存中获得Data响应头对应的时间(服务器发出本响应的时间)
    // receivedResponseMillis为本次服务器响应对应的客户端发出请求的时间
    // (这个应该是客户端接收到响应的时间)
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : receivedResponseMillis;
    // 计算差值,即为从接收到缓存到数据到期还有多久
    long delta = expires.getTime() - servedMillis;
    return delta > 0 ? delta : 0;
  } else if (lastModified != null
      && 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;
  }
  return 0;
}

缓存最小新鲜度:请求认为的缓存有效时间,即minFreshMillis。假设缓存本身的新鲜度为100毫秒,而缓存最小新鲜度为10毫秒,那么缓存真正的有效时间为90毫秒。
过期后继续使用时长:即超过缓存有效时间之后,还可以使用的时长,即在这个时间内,依然可以使用缓存,即maxStaleMillis,也就是请求头中的Cache-Control: max-state=[秒]缓存过期后仍然有效的时长

  // 第六步:响应的缓存是否有效
  // 6.1:获得缓存的响应从创建到现在的时间
  long ageMillis = cacheResponseAge();
  // 6.2:获取这个响应有效缓存时长(缓存有效时间)与minFreshMillis的区别
  // 其实就是minFreshMillis是请求认为的缓存有效时间
  long freshMillis = computeFreshnessLifetime();

  // 如果请求中指定了max-age表示指定了能拿的缓存有效时长,就需要总和响应
  // 有效缓存时长与请求能拿缓存的时长,获得最小能够使用响应缓存的时长
  // max-age是资源最大有效时间
  if (requestCaching.maxAgeSeconds() != -1) {
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }

  // 6.3:请求包含Cache-control: min-fresh=[秒] 能够使用还未过指定时间
  // 的缓存(用户请求认为的缓存有效时间,最小新鲜度)
  long minFreshMillis = 0;
  if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }

  // 6.4
  // 6.4.1:Cache-Control:must-revalidate 可缓存但是必须再向服务器进行确认
  // 但是不可以使用过期资源
  // 6.4.2:Cache-Control:max-state=[秒] 缓存过期后还能使用指定的时长,
  // 如果未指定多少秒,则表示无论过期多长时间都可以;如果指定了,则只要是
  // 指定时间内就能使用缓存
  // 如果前者不满足,则自动忽略后者。只有不是必须向服务器进行确认的情况下
  // 才会判断缓存的过期时间
  long maxStaleMillis = 0;
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }

  // 6.5:不需要与服务器验证有效性 && 获得缓存的响应从创建到现在的时间 + 
  // 请求认为的缓存有效时间(最小新鲜度) 小于 缓存有效时间 + 过期后还可以使用的时间
  // 如果满足条件,则允许使用缓存
  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());
  }

看这部分源码,前面的4个小步骤,是计算对应的时间,即缓存存活时间、缓存新鲜度、缓存最小新鲜度、过期后依然可以使用缓存的时间。

(7)缓存过期处理

如果前面六步中,并没有使用缓存,也没有在之前进行网络请求,那么说明缓存存在,但是缓存不可用,即本地缓存过期。那么就需要判断是否etag和lastModified,首先判断Etag标签,将Etag标签的作为value,然后以If-None-Match作为key,添加到请求头中;如果Etag为null,则考虑lastModified,将最后修改时间作为value,然后以If-Modified-Since作为key,添加到请求头中;这样做的目的是因为本地缓存已经过期,所以交给服务器来做比较,如果服务器返回了304,说明是使用了服务器的缓存,则框架自动更新本地缓存。

(8)总结

1、如果从缓存获取的Response是null,那就需要使用网络请求获取响应;
2、如果是Https请求,但是又丢失了握手信息,那也不能使用缓存,需要进行网络请求;
3、如果判断响应码不能缓存且响应头有no-store标识,那就需要进行网络请求;
这里需要特定的响应码,并不是所有的响应码都可以在没有no-store的情况下去使用缓存
并且302和307还需要有特定的响应头配置,即Expires:时间(服务器返回给客户端的数据到期时间),
Cache-Control:max-age资源最大有效时间,Cache-Control: public或者private
可缓存对象
4、如果请求头有no-cache标识或者有If-Modified-Since/If-None-Match,那么需要进行网络请求;
5、如果响应头没有no-cache标识,且缓存时间没有超过极限时间,那么可以使用缓存,不需要进行网络请求;
6、如果缓存过期了,判断响应头是否设置Etag/Last-Modified/Date,没有那就直接使用网络请求否则需要考虑服务器返回304;
并且,只要需要进行网络

四、连接拦截器

连接拦截器主要做的事情有以下几件:

(1)获取对应的发射器、Request和RealInterceptorChain
(2)在Transmitter.newExchange()调用ExchangeFinder.find方法,目的是为了找到RealConnection对象,查找RealConnection的过程,首先是查找发射器的现有连接是否存在,如果存在,则直接使用,如果不存在则从连接池中查找,如果连接池中也不存在,则会创建。这个过程中,总共查询了三次连接池,第一次其实就是普通的查询,即路由集合传入的是null,多路复用也是传的false;第二次连接池查询是基于多路由,路由集合部分传入了路由集合;在第二次连接池中查询没有找到可用连接之后,才会去创建一个新的连接,并且与服务器进行握手,握手之后,又会进行第三次查询连接池,第三次查询是基于多路复用情况下进行的。
(3)查询到haelthy connection之后,通过RealConnection封装OkHttpClient和链对象,根据是Http/2还是Http/1x创建不同的ExchangeCodec实现类,一般是Http1ExchangeCodec
(4)封装Exchange对象

在这里涉及到一些相关的类:
RouteDataBase:这是一个关于路由信息的白名单和黑名单类,处于黑名单的路由信息会被避免不必要的尝试;该类是在RealConnectionPool中被使用;
RealConnecton:Connect子类,主要实现连接的建立等工作;
ConnectionPool:连接池,实现连接的复用;
Http1ExchangeCodec:可用于发送HTTP / 1.1消息的套接字连接;
Http2ExchangeCodec:使用HTTP / 2帧编码请求和响应。

(1)ConnectionInterceptor

ConnectInterceptor连接拦截器,打开与目标服务器的连接,并执行下一个拦截器。

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
  // Transmitter对象,是在RealCall中初始化创建的,在Transmitter中就会初始化创建ConnectionPool
  // 而ConnectionPool其实比较类似装饰者模式,封装RealConnectionPool
    Transmitter transmitter = realChain.transmitter();

    // 我们需要网络来满足这个请求。可能是为了验证一个条件GET请求(缓存验证等)。
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    // 这个Exchange类,其实就是用于发送单个Http请求和一个响应对,
    // 在Exchange中会处理实际的I/O(ExchangeCodec)上分层连接管理和事件。
    // 所以要封装Exchange也需要依赖于Transmitter发射器,所以寻找到的连接RealConnection
    // 也需要保存在Transmitter中。
    // 在3.14的源码中,Transmitter的工作替换了StreamAllocation
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    return realChain.proceed(request, transmitter, exchange);
  }
}

ConnectInterceptor的代码简单,主要是因为主要的逻辑都被封装到了其他类中。
首先我们看到的,在这里依赖于Transmitter这个连接发射器,调用newExchange方法,其内部就是将RealInterceptorChain对象和OkHttpClient对象封装成Http1ExchangeCodec,而其内部主要是先通过ExchangeFinder寻找一个有效的与请求主机的连接,通过调用ExchangeFinder.find方法

(2)Transmitter.newExchange()

Transsmitter.newExchange方法,其实就是封装一个Exchange交换器,用来在下一个拦截器中携带最新的Request和Response

  Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
      if (noMoreExchanges) {
        throw new IllegalStateException("released");
      }
      if (exchange != null) {
        throw new IllegalStateException("cannot make a new request because the previous response "
            + "is still open: please call response.close()");
      }
    }
    // 这里就是寻找一个可用的连接,封装一个对应的Http版本的套接字连接
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      return result;
    }
  }
(3)ExchangeFinder.find

该方法其实就是封装ExchangeCodec对象,ExchangeCodec主要是用来编码Http请求,并且对Http响应进行解码的。在CallServerInterceptor拦截器中,会多次调用到Exchange对象,而Exchange对象中的很多方法都是通过ExchangeCodec来实现。

public ExchangeCodec find(
    OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
  int connectTimeout = chain.connectTimeoutMillis();
  int readTimeout = chain.readTimeoutMillis();
  int writeTimeout = chain.writeTimeoutMillis();
  int pingIntervalMillis = client.pingIntervalMillis();
  boolean connectionRetryEnabled = client.retryOnConnectionFailure();

  try {
    // 这里通过发射器查询对应的socket连接池中是否存在有效的连接,如果存在则获取
    // 如果获取到的是一个全新的连接,则不需要进行是否healthy的检查
    // 否则需要进行检查
    RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
        writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
    return resultConnection.newCodec(client, chain);
  } catch (RouteException e) {
    trackFailure();
    throw e;
  } catch (IOException e) {
    trackFailure();
    throw new RouteException(e);
  }
}

在调用findHealthyConnection方法中,会调用到findConnection方法,在findConnection方法中会调用到RealConnectionPool.transmitterAcquirePooledConnection(),该方法的主要作用就是为尝试为请求获取对应的连接。并且将对应的连接保存到对应的发射器Transmitter中。

(4)ExchangeFinder.findHealthyConnection()

该方法的主要目的就是查找到一个健康可用的连接,用于请求服务器。在findHealthyConnection方法中,内部其实就是调用了findConnection方法

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  boolean foundPooledConnection = false;
  RealConnection result = null;
  Route selectedRoute = null;
  RealConnection releasedConnection;
  Socket toClose;
  synchronized (connectionPool) {
     // 请求已被取消
    if (transmitter.isCanceled()) throw new IOException("Canceled");
    hasStreamFailure = false; // This is a fresh attempt.

    // Attempt to use an already-allocated connection. We need to be careful here because our
    // already-allocated connection may have been restricted from creating new exchanges.
    // 尝试获取已经创建并且分配的连接,已经创建过的连接可能已经被
    // 限制创建新的流
    releasedConnection = transmitter.connection;
    // 如果已经创建过的连接已经被限制创建新的流,就释放该连接
    // (releaseConnectionNoEvents中会把该连接置空,并返回该连接的socket已关闭)
    // RealConnection中的noNewExchanges则说明无法在此连接上建立新的Exchange
    // 此时就调用releaseConnectionNoEvents释放连接资源,返回等待关闭的Socket套接字
    // 每个Transmitter中都会有一个RealConnection,如果不可用,则释放该RealConnection中资源
    // 并且把这个RealConnection中对应的Socket返回用于关闭连接通道
    toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
        ? transmitter.releaseConnectionNoEvents()
        : null;

    // 已经创建过的连接还能使用,就直接使用
    if (transmitter.connection != null) {
      // We had an already-allocated connection and it's good.
      result = transmitter.connection;
      // 这个为null,用于说明该连接是有效的
      releasedConnection = null;
    }

    // 如果已经创建过的连接不能使用
    if (result == null) {
      // Attempt to get a connection from the pool.
      // TODO:第一次尝试从连接池中查询可用的连接,如果找到可用的连接,则会先分配给发射器
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
        // 从连接池中找到可用的连接,因为已经被优先赋值给了发射器transmitter.connection
        foundPooledConnection = true;
        result = transmitter.connection;
      } else if (nextRouteToTry != null) {
        selectedRoute = nextRouteToTry;
        nextRouteToTry = null;
      } else if (retryCurrentRoute()) {
        selectedRoute = transmitter.connection.route();
      }
    }
  }
  closeQuietly(toClose);

  if (releasedConnection != null) {
    eventListener.connectionReleased(call, releasedConnection);
  }
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
  }
  if (result != null) {
    // 经过查询已经分配的连接、连接池两个过程之后找到可用的连接
    // 直接返回该连接
    return result;
  }

  // 判断是否需要路由选择(多IP操作的情况下)
  boolean newRouteSelection = false;
  // 第一次连接池中没有查询到可用的连接的时候,
  // 则在进行第二次连接池查询之前,判断是否需要路由选择,以便进行通过路由集合查询连接池
  // 条件:已经选择的路由为null并且(路由选择器中对应的已选择的路由集合为null或者没有下一个)
  if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
    newRouteSelection = true;
    routeSelection = routeSelector.next();
  }

  List<Route> routes = null;
  synchronized (connectionPool) {
    if (transmitter.isCanceled()) throw new IOException("Canceled");

    // 如果有下一个路由
    if (newRouteSelection) {
      // 第二次尝试从连接池中查询可用连接
      // 此时与上一次不同的是这里传入了路由集合
      routes = routeSelection.getAll();
      if (connectionPool.transmitterAcquirePooledConnection(
          address, transmitter, routes, false)) {
        // 从连接池中找到可用连接
        foundPooledConnection = true;
        result = transmitter.connection;
      }
    }

    // 如果连接池中没有可用连接
    if (!foundPooledConnection) {
      if (selectedRoute == null) {
        selectedRoute = routeSelection.next();
      }

      // 创建一个新的连接,交由下面进行socket连接
      result = new RealConnection(connectionPool, selectedRoute);
      connectingConnection = result;
    }
  }

  // 这里是第二次从连接池中查询到了可用连接之后
  // 直接返回,如果第二次从连接池中并没有找到可用连接,创建了一个新的连接
  // 则这里不会返回
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
    return result;
  }

  // 如果第二次查询连接池没有找到可用连接,则把新创建的连接进行TCP+TLS握手
  // 与服务端建立连接,这是一个阻塞操作
  // 这里其实就会根据连接池中查询不到可用连接,而创建一个新的socket套接字
  // 在RealConnection的connect方法中调用connectSocket方法初始化rawSocket
  // 然后接着调用establishProtocol将rawSocket赋值给socket
  // Do TCP + TLS handshakes。连接Server
  // TODO: 在这里进行TCP连接,TCP连接开始之后,进行TLS安全连接过程,然后才会结束TCP连接
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);
  // 将路由信息添加到RouteDatabase
  connectionPool.routeDatabase.connected(result.route());

  Socket socket = null;
  synchronized (connectionPool) {
    connectingConnection = null;
    // 最后一次尝试从连接池中获取连接,最后一个参数为true,即要求多路复用
    // 为了保证多路复用,会再次确认连接池中此时是否有同样的连接
    // 这里的多路复用是为了确保http2的连接多路复用功能
    // 即在创建一个新的连接的时候,判断是否是Http2
    if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
      // 如果获取到,就关闭在创建里的连接,返回从连接池中获取的连接
      result.noNewExchanges = true;
      socket = result.socket();
      result = transmitter.connection;

      // 刚刚连接成功的路由,因为我们从连接池中找到可用连接而变成不健康的
      // 我们可以保存用于下次尝试的路由
      nextRouteToTry = selectedRoute;
    } else {
      // 最后一次尝试从连接池中获取,没有找到可用的连接
      // 则将连接添加到连接池中
      connectionPool.put(result);
      transmitter.acquireConnectionNoEvents(result);
    }
  }
  closeQuietly(socket);

  eventListener.connectionAcquired(call, result);
  return result;
}

在OkHttp3中,RealConnection默认会保存5个空闲连接,保持5分钟。这些空闲连接如果5分钟内没有被使用,则会从连接池中移除。而OkHttp中的连接池,其实是RealConnectionPool,而ConnectionPool其实可以认为是一个装饰类,封装RealConnectionPool的功能。

  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.delegate = new RealConnectionPool(maxIdleConnections, keepAliveDuration, timeUnit);
  }
ConnectionInterceptor流程.png
(5)RealConnectionPool.transmitterAcquirePooledConnection()

尝试从连接池中查询可用的连接,如果找到可用的连接,则会先分配给发射器。这里就是从连接池中取出连接

boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
    @Nullable List<Route> routes, boolean requireMultiplexed) {
  assert (Thread.holdsLock(this));
  for (RealConnection connection : connections) {
    if (requireMultiplexed && !connection.isMultiplexed()) continue;
    if (!connection.isEligible(address, routes)) continue;
    // 寻找到符合要求的允许复用的连接,保存到Transmitter发射器中
    transmitter.acquireConnectionNoEvents(connection);
    return true;
  }
  return false;
}
(6)RealConnectionPool.cleanup

线程池的cleanup方法,是在RealConnectionPool中定义的一个Runnable对象cleanupRunnable中的run方法调动,是一个无限循环调用。在cleanup中,会遍历所有的连接,找到空闲连接数,并且找到空闲时间最久的那个连接以及空闲时间,然后判断连接池中空闲连接数是否大于5或者空闲最久的是否超过5分钟,如果满足其中一个则执行remove操作

  private final Runnable cleanupRunnable = () -> {
    while (true) {
      long waitNanos = cleanup(System.nanoTime());
      if (waitNanos == -1) return;
      if (waitNanos > 0) {
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        synchronized (RealConnectionPool.this) {
          try {
            RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };

往连接池中put连接,就会通过一个线程池执行连接池的清理任务

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
  long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          // 连接池中处于使用状态的连接数
          inUseConnectionCount++;
          continue;
        }
        // 处于空闲状态的连接数
        idleConnectionCount++;

        // 找到空闲时间最久的那个连接
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }


      // 如果空闲时间大于keepAliveDurationNs(默认5分钟)
      // 或者空闲的连接总数大于maxIdleConnections(默认5个)
      // 执行移除操作
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        // We've found a connection to evict. Remove it from the list, then close it below (outside
        // of the synchronized block).
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // 空闲最久的那个连接的空闲时长与keepAliveDurationNs的差值
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }
    // 关闭需要移除的空闲连接的socket
    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }
(7)RealConnection.newCodec

在寻找找有效的请求连接之后,通过调用RealConnection的newCodec。即该方法是在ExchangeFinder.find方法中调用findHealthyConnection方法获取有效连接之后。
这里返回的是一个ExchangeCodec对象,而ExchangeCodec是一个接口,其实现类是Http1ExchangeCodec和Http2ExchangeCodec,Http2ExchangeCodec是用于Http/2版本的。
返回一个Http1ExchangeCodec
接着在发射器中,将其封装成一个Exchange对象。
这个Exchange类,其实就是用于发送单个Http请求和一个响应对,在Exchange中会处理实际的I/O(ExchangeCodec)上分层连接管理和事件。

(8)RealConnection
  // RealConnectionPool中缓存的是RealConnection对象
  public final RealConnectionPool connectionPool;
  private Socket socket;

RealConnectionPool

private final Deque<RealConnection> connections = new ArrayDeque<>();

在RealConnection中,封装了Socket与一个Socket连接池。而isEligible方法,主要的目的是在从Socket连接池中获取到对应的有效连接的时候,判断是否有资格处理针对该域名主机的该请求事件。
isEligible方法是在ExchangeFinder调用findConnection方法寻找有效连接的时候,调用RealConnectionPool.transmitterAcquirePooledConnection方法的时候调用的。

boolean isEligible(Address address, @Nullable List<Route> routes) {
  // 如果连接不接受新的流,则完成。这样的情况一般有两种,一种是达到最大并发流
  // 或者是连接就不允许建立新的流;如Http1.x正在使用的连接不能给其他人用
  // (最大并发流为:1)或者连接被关闭,那么就不允许复用。
  if (transmitters.size() >= allocationLimit || noNewExchanges) return false;

  
  // DNS、代理、SSL证书、服务器域名、端口完全相同则可复用
  // 如果上述条件都不满足,在HTTP/2的某些场景下可能仍可复用
  if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
  if (address.url().host().equals(this.route().address().url().host())) {
    return true; // This connection is a perfect match.
  }

  // At this point we don't have a hostname match. But we still be able to carry the request if
  // our connection coalescing requirements are met. See also:
  // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
  // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/

  // 1. This connection must be HTTP/2.
  if (http2Connection == null) return false;

  // 2. The routes must share an IP address.
  if (routes == null || !routeMatchesAny(routes)) return false;

  // 3. This connection's server certificate's must cover the new host.
  if (address.hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
  if (!supportsUrl(address.url())) return false;

  // 4. Certificate pinning must match the host.
  try {
    address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
  } catch (SSLPeerUnverifiedException e) {
    return false;
  }

  return true; // The caller's address can be carried by this connection.
}

在连接拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行Http数据的收发。

五、请求服务器拦截器

CallServerInterceptor通过封装的Exchange中的ExchangeCodec对象(Http1ExchangeCodec)发出请求到服务器并且解析生成Response

  • (1)封装request,放在BufferedSink缓存流中
  • (2)如果不是get请求或者不是head请求,且有request body,则判断请求头中是否有Expect:100-continue,如果有,则封装请求信息,优先发送一次请求,得到响应码为100,再发送请求体进行请求
  • (3)根据不同情况的responseBuilder是否为null,初始化responseBuilder
  • (4)使用responseBuilder发起第一次请求,接收response
  • (5)判断response的code是否为100,如果是100,则进行第二次请求,这次请求是真实的数据请求,如果不是100,则不进行第二次请求
  • (6)重新builder返回结果,将response进行重新封装,将请求获取到的响应数据封装成responseBody,而不是之前的数据流的形式。
  • (7)判断是否关闭长连接,并且处理无响应体异常问题

1.CallServerInterceptor.intercept()

@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Exchange exchange = realChain.exchange();
  Request request = realChain.request();

  long sentRequestMillis = System.currentTimeMillis();

  // 调用Exchange.writeRequestHeaders,其内部就是调用了ExchangeCodec
  // 的writeRequestHeaders方法,将请求信息写到缓存中(BufferedSink),
  // 其实就是将请求信息转成缓存流,使用BufferedSink来进行缓存
  // 这里其实就是OkHttp采用的Okio的方式
  exchange.writeRequestHeaders(request);

  boolean responseHeadersStarted = false;
  Response.Builder responseBuilder = null;
  // 这里整个请求都与一个请求头有关:Expect: 100-continue
  // 这个请求头代表了在发送请求体之前需要和服务器确定是否愿意接受客户端发送的请求体。
  // 所以permitsRequestBody判断为是否会携带请求体的方式(POST),
  // 如果命中if,则会先给服务器发起一次查询是否愿意接收请求体,这个时候如果
  // 服务器愿意会响应100(没有响应体,responseBuilder即为null)
  // 直到返回为100-continue之后,才能够继续发送剩余请求数据
  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
    // 如果请求希望带上"Expect: 100-continue",则在发送请求之前,需要先
    // 等待HTTP/1.1 100 Continue的响应。如果没有得到响应,即responseBuilder=null
    // 则返回得到的结果,并且不需要传请求体
    if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
      // 真正调用请求的地方,通过flushRequest之后,将请求信息发送给服务器
      exchange.flushRequest();
      responseHeadersStarted = true;
      // 注册请求监听回调
      exchange.responseHeadersStart();
      // 获取Expect: 100-continue的请求头的请求之后的responseBuilder
      // 如果服务器不允许接收请求体,则这里的responseBuilder不为null
      // 如果服务器允许接收请求,则这里的responseBuilder为null,code为100
      // 返回为null,是因为传入的参数为true,导致直接return null;
      responseBuilder = exchange.readResponseHeaders(true);
    }

    if (responseBuilder == null) {
      // 是否是双工请求
      if (request.body().isDuplex()) {
        exchange.flushRequest();
        BufferedSink bufferedRequestBody = Okio.buffer(
            exchange.createRequestBody(request, true));
        request.body().writeTo(bufferedRequestBody);
      } else {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        // 写入请求头中带有Expect: 100-continue的第一次请求的响应码为100时,向请求中写入请求体,进行二次请求
        BufferedSink bufferedRequestBody = Okio.buffer(
            exchange.createRequestBody(request, false));
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      }
    } else {
      exchange.noRequestBody();
      if (!exchange.connection().isMultiplexed()) {
        // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
        // from being reused. Otherwise we're still obligated to transmit the request body to
        // leave the connection in a consistent state.
        exchange.noNewExchangesOnConnection();
      }
    }
  } else {
    // 如果服务器不同意接收请求体,那么需要标记该链接不能再被复用
    // 则调用Exchange.noRequestBody(),会清除Socket连接池中的链接
    // 同时关闭相关的Socket链接。
    exchange.noRequestBody();
  }

  if (request.body() == null || !request.body().isDuplex()) {
    exchange.finishRequest();
  }

  // 如果POST请求没有Expect: 100-continue或者是GET请求
  // 则在这里注册请求的Call的监听回调
  if (!responseHeadersStarted) {
    exchange.responseHeadersStart();
  }

  // 如果可以继续进行请求,则通过Exchange获取responseBuilder用于获取
  // 服务器的请求返回Response
  // 这个时候responseBuilder的情况为:
  // 1.POST方式请求,请求头中包含Expect,服务器允许接收请求体,
  // 并且已经发出了请求体,responseBuilder为null
  // 2.POST方式请求,请求体中包含Expect,服务器不允许接收请求体,
  // responseBuilder不为null
  // 3.POST方式请求,未包含Expect,直接发出请求,responseBuilder为null
  // 4.POST方式请求,没有请求体,responseBuilder为null
  // 5.GET方式请求,responseBuilder为null
  if (responseBuilder == null) {
    // 获取Response.Builder实例
    // 这个实例并没有发起真正的请求,只是添加了readHeaders等配置信息
    // 具体请求在下面,添加request,并且执行build
    responseBuilder = exchange.readResponseHeaders(false);
  }

  // 如果这个response不是Expect: 100-continue的响应结果
  // 这会是直接发出请求得到的结果。
  // request可能是请求头中包含Expect: 100-continue的第一次请求,
  // 那么就需要在第一次请求之后再进行一次真实的数据请求,获取真实的Response
  // 也可能是没有包含Expect: 100-continue的对服务器的直接请求
  // 那么这次请求返回的Response就是真实的Response
  // 获取到真实的Response之后,需要对Response进行数据封装,
  // 因为返回的响应体其实还是类似于请求体封装那样的一种字符串形式的
  Response response = responseBuilder
      .request(request)
      .handshake(exchange.connection().handshake())
      .sentRequestAtMillis(sentRequestMillis)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();

  // 对应上面5种请求,读取响应头并且组成响应Response,注意:这个Response
  // 没有响应体。同时需要注意的是,如果服务器接受Expect: 100-continue
  // 这是不是意味着我们发起了两次Request?那此时的响应头是第一次查询服务器
  // 是否支持接受请求体的,而不是真正的请求对应的结果响应
  int code = response.code();
  if (code == 100) {
    // 如果响应的code=100,这代表了请求Expect: 100-continue成功的响应
    // 需要马上再次读取一次响应头,这才是真正的请求对应结果响应头
    // 这次其实就是在服务器愿意客户端发送请求之后
    // 客户端再一次对服务器进行请求得到的response
    response = exchange.readResponseHeaders(false)
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    code = response.code();
  }

  exchange.responseHeadersEnd(response);

  // forWebSocket代表webSocket看的请求,这里一般并不是,所以进入else
  // 在else中就是读取响应体数据。
  if (forWebSocket && code == 101) {
    // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
    response = response.newBuilder()
        .body(Util.EMPTY_RESPONSE)
        .build();
  } else {
    // 针对服务器返回的response进行封装。
    response = response.newBuilder()
        .body(exchange.openResponseBody(response))
        .build();
  }

  // 然后判断请求和服务器是不是都希望长连接
  // 一旦有一方指明close,那么就需要关闭socket。
  if ("close".equalsIgnoreCase(response.request().header("Connection"))
      || "close".equalsIgnoreCase(response.header("Connection"))) {
    exchange.noNewExchangesOnConnection();
  }

  // 而如果服务器返回204/205
  // 一般情况而言不会存在这些返回码,但是一旦出现这意味着没有响应体,
  // 但是解析到的响应头中包含Content-Length且不为0,这表示响应体的数据
  // 字节长度。此时出现了冲突,直接抛出协议异常。
  if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
    throw new ProtocolException(
        "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
  }

  return response;
}

2.Exchange.writeRequestHeaders()

缓存请求信息,将请求信息缓存到BufferedSink中。即Http1ExchangeCodec中的BufferedSink中

public void writeRequestHeaders(Request request) throws IOException {
  try {
    eventListener.requestHeadersStart(call);
    codec.writeRequestHeaders(request);
    eventListener.requestHeadersEnd(call, request);
  } catch (IOException e) {
    eventListener.requestFailed(call, e);
    trackFailure(e);
    throw e;
  }
}

3.Http1Exchange.readResponseHeaders

在这里,如果传入的expectContinue=true,那么就会返回为null,如果传入的是false,就会返回responseBuilder实例。

@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
  if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
    throw new IllegalStateException("state: " + state);
  }

  try {
    StatusLine statusLine = StatusLine.parse(readHeaderLine());

    Response.Builder responseBuilder = new Response.Builder()
        .protocol(statusLine.protocol)
        .code(statusLine.code)
        .message(statusLine.message)
        .headers(readHeaders());

    if (expectContinue && statusLine.code == HTTP_CONTINUE) {
      return null;
    } else if (statusLine.code == HTTP_CONTINUE) {
      state = STATE_READ_RESPONSE_HEADERS;
      return responseBuilder;
    }

    state = STATE_OPEN_RESPONSE_BODY;
    return responseBuilder;
  } catch (EOFException e) {
    // Provide more context if the server ends the stream before sending a response.
    String address = "unknown";
    if (realConnection != null) {
      address = realConnection.route().address().url().redact();
    }
    throw new IOException("unexpected end of stream on "
        + address, e);
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容