OkHttp原理解析2(拦截器篇)

534 阅读23分钟

image.png

Hello小伙伴们,现在公司已经恢复了正常办公,但是疫情依旧还在继续。最近工作实在是有点小忙,导致更新有点缓慢,实在抱歉,本文是OkHttp原理解析的第二篇, 主要针对OkHttp中几个 默认拦截器 的具体实现逻辑进行分析。

因为OkHttp的很大一部分逻辑都在拦截器中,因此本文会比较长,同时采用连载更新的方式进行描述,每完成一个拦截器的逻辑分析都会进行更新。

如有对OkHttp的框架流程不太了解的可优先阅读网我上篇博客 OkHttp原理解析1(框架流程篇)


我又要开始表演了~~~

但为了方便后续描述,我还是简单对上文做了个总结。大概分为以下几步。

    1. 创建OkHttpClient(可通过OkHttpClient.Builder创建,内部设置一些基础参数)
    1. 创建 Request 对象设置 url,请求类型等参数
    1. 通过OkHttpClient.newCall()创建RealCall对象,同时创建了Transmitter对象。
    1. 通过**RealCall.enqueue()或者RealCall.execute()**触发 异步或同步 请求。
    1. 调用OkHttpClient.dispatcher().enqueue(new AsyncCall(responseCallback))做请求准备,循环runningAsyncCalls和readyAsyncCalls 队列找出host相同的AsyncCall进行重用,并将readyAsyncCallsAsyncCall转移到runningAsyncCalls中,如果runningAsyncCalls超过64则终止转移,如相同主机计数器>5则终止转移本AsyncCall。
    1. 循环runningAsyncCalls调用AsyncCall.executeOn(executorService())
    • 6.1. AsyncCall为Runnable,执行run()方法,调用AsyncCall.execute()连带调用AsyncCall.getResponseWithInterceptorChain()设置拦截器List,首先设置用户自定义的拦截器。最后通过RealInterceptorChain.proceed()启动拦截器。

而拦截器的启动与运行依赖 责任链 模式,大概分为以下3步。

    1. 首先创建RealInterceptorChain对象,通过procee()判断各种异常,并获取当前Interceptor对象。
    1. 然后 通过Interceptor.intercept(RealInterceptorChain)启动当前拦截器逻辑,并且触发下一个拦截器启动。
    1. 如果当前拦截器出现异常等错误,则终止责任链

我们从添加拦截的的位置开始本文介绍,其实上文已经做了描述,源码如下所示。

//RealCall.getResponseWithInterceptorChain();
  Response getResponseWithInterceptorChain() throws IOException {
    // 建立一个完整的拦截器堆栈
    List<Interceptor> interceptors = new ArrayList<>();
     //将创建okhttpclient时的拦截器添加到interceptors
    interceptors.addAll(client.interceptors());
    //重试拦截器,负责处理失败后的重试与重定向
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    //请求转化拦截器(用户请求转为服务器请求,服务器响应转为用户响应)
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //缓存拦截器。负责
    //1.根据条件,缓存配置,有效期等返回缓存响应,也可增加到缓存。
    //2.设置请求头(If-None-Match、If-Modified-Since等) 服务器可能返回304(未修改)
    //3.可配置自定义的缓存拦截器。
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //网络连接拦截器,主要负责和服务器建立连接。
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      //创建okhttpclient时设置的networkInterceptors
      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);
      }
    }
  }

主要看我注释的部分,OkHttp主要涉及了以下几个默认拦截器

    1. client.interceptors();//用户自定义拦截器
    1. RetryAndFollowUpInterceptor(client)//失败重试拦截器
    1. BridgeInterceptor(client.cookieJar())//请求转化拦截器
    1. CacheInterceptor(client.internalCache())//缓存拦截器
    1. ConnectInterceptor(client)//网络连接拦截器
    1. CallServerInterceptor(forWebSocket)//数据流拦截器

OkHttp会把用户自定义的拦截器默认放到拦截器列表的头部,以方面优先执行,然后通过创建RealInterceptorChain对象,并调用RealInterceptorChain.proceed()启动第一个拦截器,然后调用拦截器的interceptor.intercept(next)执行第一个拦截器的逻辑并将下一个拦截器RealInterceptorChain对象传入依此类推。接下来我们就一个个进行分析。


1. client.interceptors() 连接失败重试拦截器


用户自定义的拦截器具有优先执行的特权,具体执行用户自定义的逻辑,不过多介绍。


2. RetryAndFollowUpInterceptor(client) 连接失败重试拦截器


首先开始的是RetryAndFollowUpInterceptor失败重试拦截器,这个拦截器是可以在OkHttpClient.Builder对象中通过retryOnConnectionFailure(boolean retryOnConnectionFailure)设置是否开启,默认构建的OkHttpClient对象是开启的。 该拦截器主要的职责官方注释解释了这么几点。

    1. 无法访问ip地址,如果URL的主机有多个IP地址,则无法访问任何单个IP地址不会使整个请求失败。这可以提高多宿服务的可用性。
    1. 过时的池连接,通过ConnectionPool重用套接字以减少请求延迟,但这些连接偶尔会超时。
    1. 无法访问的代理服务器,可以使用ProxySelector,最终返回到直接连接。

说实话,这描述略显抽象,要不是我用过,我真不知道他在扯啥,咱们还是通过源码看下真像吧。

//我们应该尝试多少重定向和验证挑战?Chrome遵循21个重定向;Firefox、curl和wget遵循20;Safari遵循16;HTTP/1.0建议5。
private static final int MAX_FOLLOW_UPS = 20;

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

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      //通过transmitter创建ExchangeFinder,Address,RouteSelector三个对象
      transmitter.prepareToConnect(request);
      //判断如果当前请求结束了则抛出异常,可通过transmitter终止请求。
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        //启动下一个拦截器
        response = realChain.proceed(request, transmitter, null);
        //执行顺利则通过该字段退出循环。
        success = true;
      } catch (RouteException e) {
        // 如果是路由异常。请求还没发送。
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // 尝试与服务器通信失败。请求可能已发送。
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        //网络调用引发异常。释放所有资源。
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // 如果body不为空
      if (priorResponse != null) {
        //获得新的response
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Exchange exchange = Internal.instance.exchange(response);
      Route route = exchange != null ? exchange.connection().route() : null;
      // 调用followUpRequest()查看响应是否需要重定向,不需要就返回当前请求,如果需要返回新的请求
      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();
      }
      //如果重定向次数超过最大次数抛出异常
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;
      priorResponse = response;
    }
  }

  //RetryAndFollowUpInterceptor.recover()
  //报告并尝试从与服务器通信失败中恢复。如果{@code e}可恢复,则返回true;
  //如果失败是永久性的,则返回false。只有在缓冲了正文或在发送请求之前发生故障时,才可以恢复具有正文的请求。
 private boolean recover(IOException e, Transmitter transmitter,
      boolean requestSendStarted, Request userRequest) {
    // 用户设置的禁止重试
    if (!client.retryOnConnectionFailure()) return false;

    // 不能再发送请求体
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // 异常是致命的
    if (!isRecoverable(e, requestSendStarted)) return false;

    // 没有更多的路线可以尝试。
    if (!transmitter.canRetry()) return false;

    //对于故障恢复,请对新连接使用同一路由选择器
    return true;
  }

  /**
   * 查看响应是否需要重定向,不需要就返回当前请求,如果需要返回新的请求。
   * 找出接收{@code userResponse}时要发出的HTTP请求。这将添加身份验证头、遵循重定向或处理客户端请求超时。
   * 如果后续操作不必要或不适用,则返回null。
   */
  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 代理需要身份认证
      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);
      //401 需要身份认证
      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);

      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // "If the 307 or 308 status code is received in response to a request other than GET
        // or HEAD, the user agent MUST NOT automatically redirect the request"
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
       // 300多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
      case HTTP_MULT_CHOICE:
      // 301永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。
      case HTTP_MOVED_PERM:
      // 302临时移动。与301类似。但资源只是临时被移动。
      case HTTP_MOVED_TEMP:
      // 303查看其它地址。与301类似。使用GET和POST请求查看
      case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // 不要遵循重定向到不支持的协议。
        if (url == null) return null;

        //如果已配置,请不要遵循SSL和非SSL之间的重定向。
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // 大多数重定向不包括请求正文。
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // 跨主机重定向时,删除所有身份验证头。这对应用程序层来说可能很烦人,因为它们无法保留它们。
        if (!sameConnection(userResponse.request().url(), url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();
      //408 超时
      case HTTP_CLIENT_TIMEOUT:
        // 408's are rare in practice, but some servers like HAProxy use this response code. The
        // spec says that we may repeat the request without modifications. Modern browsers also
        // repeat the request (even non-idempotent ones.)
        if (!client.retryOnConnectionFailure()) {
          // 应用层指示我们不要重试请求
          return null;
        }

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

        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, 0) > 0) {
          return null;
        }

        return userResponse.request();
      // 503 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
      case HTTP_UNAVAILABLE:
        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;
    }
  }


//Transmitter.prepareToConnect()
public void prepareToConnect(Request request) {
    if (this.request != null) {
      //判断是不是相同的连接,如果是则用之前的。
      if (sameConnection(this.request.url(), request.url()) && exchangeFinder.hasRouteToTry()) {
        return; // Already ready.
      }
      if (exchange != null) throw new IllegalStateException();
      //exchangeFinder
      if (exchangeFinder != null) {
        maybeReleaseConnection(null, true);
        exchangeFinder = null;
      }
    }

    this.request = request;
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }

//Transmitter.createAddress()
 private Address createAddress(HttpUrl url) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    if (url.isHttps()) {
      //https的设置
      sslSocketFactory = client.sslSocketFactory();
      hostnameVerifier = client.hostnameVerifier();
      certificatePinner = client.certificatePinner();
    }

    return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
        client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
  }

//ExchangeFinder.ExchangeFinder()
  ExchangeFinder(Transmitter transmitter, RealConnectionPool connectionPool,
      Address address, Call call, EventListener eventListener) {
    this.transmitter = transmitter;
    this.connectionPool = connectionPool;
    this.address = address;
    this.call = call;
    this.eventListener = eventListener;
    //创建路由选择器
    this.routeSelector = new RouteSelector(
        address, connectionPool.routeDatabase, call, eventListener);
  }
//RouteSelector.RouteSelector()
  RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
      EventListener eventListener) {
    this.address = address;
    this.routeDatabase = routeDatabase;
    this.call = call;
    this.eventListener = eventListener;

    resetNextProxy(address.url(), address.proxy());
  }

该拦截器的逻辑很多,但其实主要做两件事,一个是重试,一个是重定向,逻辑流程大概是这样的。

    1. 通过realChain.transmitter获取了transmitter对象,并启用一个while死循环,
    1. 然后通过transmitter.prepareToConnect(request)transmitter创建ExchangeFinder,Address,RouteSelector三个对象,并判断了是否是相同的连接,是否需要maybeReleaseConnection(),重置ExchangeFinder。
    • 2.1 . ExchangeFinder此拦截器只是创建ExchangeFinder,但ExchangeFinder中有个find()方法主要通过内部的 findHealthyConnection() 从 connectionPool 中找到一个可用的连接,这个连接可能是复用的,并 connect(),从而得到 输入/输出 流 (source/sink) ,返回一个 Exchange 给 CallServerIntercepter , 通过这个 Exchange 就可以添加请求头和请求体,并读取响应头和响应体,来交给上面的 Intercepter,层层向上传递。
    • 2.2 . Address为请求参数的封装类,包含url,端口,DNS,SSL,Proxy,ProxySelector,SocketFactory,主机名验证,证书校验等逻辑。
    • 2.3 . RouteSelector主要来选择路由,主要做三件事。1.收集所有的可用路由。2.选择可用路由。3.借助RouteDatabase内的Set对象来维护连接失败的路由信息,防止去连接失败路由浪费时间。
    1. 启动拦截器列表的下一个拦截器。
    1. 判断全局变量priorResponse是否为null,如果不为空则代表请求成功了。
    1. 执行 followUpRequest()查看响应是否需要重定向,如果不需要重定向则返回当前请求
    1. 判断重定向次数,如果超过最大值则退出。
    1. 重置request,并把当前的Response保存到priorResponse,进入下一次的while循环。

总结来说: 通过while死循环来获取response,每次循环开始都根据条件获取下一个request,如果没有request,则返回response,退出循环。而获取的request 的条件是根据上一次请求的response 状态码确定的,在循环体中同时创建了一些后续需要的对象


3. BridgeInterceptor(client.cookieJar())//请求转化拦截器


  • 咱先不看逻辑,就瞅这个名字。 Bridge:桥, Interceptor:拦截器。 连起来翻译: 桥拦截器? 这是 堵桥 的意思么?那得先给我把98K才行啊,最好能配个手雷。

  • 在我简单看了下源码的注释以及逻辑后发现 这其实是将用户定义的对象Request转换为网络请求以及将网络返回转回用户Response对象的桥梁,那具体是怎么充当桥梁角色的呢,我们看下源代码。

 public BridgeInterceptor(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }
  • 首先是BridgeInterceptor的构造函数,在创建BridgeInterceptor对象的时候需要传入CookieJar,CookieJar获取的是OkHttpClient对象中的cookieJar,如果用户没有设置则默认使用OkHttpClient.Builder中的cookieJar,而OkHttpClient.Builder.cookieJar默认为CookieJar.NO_COOKIES。
 @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
    //通过用户Request对象对请求头进行包装。
    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.
    // 如果用户没有设置Accept-Encoding字段,okhttp默认使用Gzip进行压缩,
    //设置Accept-Encoding为Gzip的目的就是告诉服务器客户端能接受的数据编码类型。
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }
    //添加cookie的请求头
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      //Version.userAgent()返回的是okhttp的版本号。
      requestBuilder.header("User-Agent", Version.userAgent());
    }
    //启用责任链的下一个拦截器,并接收下一个拦截器的处理结果。
    Response networkResponse = chain.proceed(requestBuilder.build());
    //获取服务器返回的 Cookie,设置接收头
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
    
    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);
    //判断服务器是否支持gzip压缩,如果支持,则将压缩提交给Okio库来处理
    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();
  }

总结 BridgeInterceptor的代码也是一片,但是功能相对单一,主要可以分为两个部分来理解。

    1. 将用户的请求Request包装成网络请求。
    1. 将网络返回Response 包装成用户需要的返回对象。

包装网络请求主要对请求头做了特殊的处理,具体可以看以上注释。比较特殊的是Accept-Encoding。 如果用户没有自定义添加Accept-Encoding,那框架会默认添加gzip,同时返回的数据也通过gzip解码。 自动解压时需要注意,会移除Content-Encoding和Content-Length,所以java上层调用contentLength时为-1。 如果用户手动添加了Accept-Encoding则transparentGzip 为false,返回的数据框架不负责解压缩。


4. CacheInterceptor(client.internalCache())//缓存拦截器


  • 这个拦截器看名字大概能知道是用来处理缓存的,至于什么缓存?我用一段话简短的描述下。
  • http请求后会有缓存生成在客户端,在有缓存的前提下,后续再发起请求前可以配置两种策略
    • 1.对比缓存
    • 2.强制缓存

如果使用对比缓存则通过数据tag对比或时间对比,然后通过对比差异智能选择是否使用缓存,如果使用强制缓存策略则直接使用缓存数据。除此之外不同的是对比缓存必然会发起网络请求,只是数据没有变化会通过304状态码告诉客户端通过缓存获取数据。而强制缓存无需网络请求,直接缓存读取。

  • 其实上边这段话大家都能看明白,但就是映射不到具体的场景中,接下来我写一个简单的例子来描述。
        //创建缓存对象,同时设置缓存位置,以及缓存的大小。
        Cache cache = new Cache(new File("mnt/sdcard/package/cache"), 1024 * 1024);
        OkHttpClient okHttpClient = new OkHttpClient
                .Builder()
                //设置缓存对象
                .cache(cache)
                .build();
        Request request = new Request
                .Builder()
                .url("")
                 //noCache()代表不使用缓存,通过网络获取,此处如果是noStore()代表不缓存也不使用缓存,如果不写则默认缓存和使用缓存。
                .cacheControl(new CacheControl.Builder().noCache().build())
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

            }
        });
  • 以上为自定义缓存的使用方式,但是具体的内部原理是通过责任链模式实现的,具体得到CacheInterceptor类中详细分析,并且只要是责任链优先看intercept()方法。
@Override 
public Response intercept(Chain chain) throws IOException {
    //通过本次请求的request对象获取请求的缓存对象Response 作为候选缓存,此处如果配置了则cache不为空,没有配置默认为空。
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //根据时间戳,请求request对象,本地候选缓存对象 生成一个缓存策略,以此判断使用网络缓存,本地缓存,还是都用。
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //不为空则使用网络缓存
    Request networkRequest = strategy.networkRequest;
   //不为空使用本地缓存
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      //跟踪满足strategy策略的http响应
      cache.trackResponse(strategy);
    }
     //如果候选缓存为空,但是本地缓存为空,证明本地缓存失效了,则关闭备选缓存cacheCandidate
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    //如果禁止使用网络缓存且本地缓存也未空则直接返回fial,code 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 (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    //  如果本地缓存存在,通过本地缓存和网络返回的响应组合构建一个reponse对象
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        //如果网络状态码为304则从本地缓存中获取
        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 response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      //将网络的response添加到缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //判断下是否为post请求,如果是则移除缓存,只保留get请求
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

总结 整体来说我把上述的一大段代码总结为以下6个步骤,可辅助快速了解该拦截器的原理。

  • 1.首先通过用户配置的cache对象结合request请求对象获取response缓存对象。
  • 2.根据时间戳,request请求对象,以及本地的response缓存对象联合生成本次请求的缓存策略,以此策略决定使用本地缓存,网络缓存还是都用。
  • 3.异常情况判断,如果用户配置缓存已经获取到,但本次缓存策略为不使用本地缓存,则关闭释放本地获取到的缓存数据,如果本次既不使用网络,也不使用本地则直接返回504错误。
  • 4.如果不可使用网络缓存,可使用本地缓存的情况下直接return本次缓存。
  • 5.如果使用网络缓存则通过责任链chain.proceed(networkRequest);继续往下一个拦截器传递。
  • 6.如果本地缓存不为空,且网络请求返回了304状态码,则通过本地缓存和网络返回共同构建一个response返回,且更新到本地缓存。
  • 7.如果本地缓存为空,则将网络返回的response返回并将response存在缓存文件中,如果是post请求则再移除response(此处本质意思是只缓存get请求,其实可以先判断在存储防止添加进去再移除。)

注: 缓存策略生成涉及几个规则

  • 1.用户创建Request手动配置了noCache()代表不使用本地缓存。
  • 2.用户创建Request手动配置了noStore()()代表不使用本地缓存同时网络请求的数据也不缓存到本地。
  • 3.通过“If-Modified-Since”字段判断服务器的数据是否修改过,如果修改过则返回服务器的最新数据,如果没修改过可使用本地的缓存数据。(判断规则通过时间判断,如果上次请求的时间>服务器的最新更新时间则证明此数据没有更新)
  • 4.通过“If-None-Match”字段判断服务器数据是否修改过,如果与服务器资源的ETag不同则代表数据已经更改,由服务器返回,如果相同则返回304从本地获取。(判断规则需要ETag配合,ETag是资源的一个唯一标识,每次请求前通过If-None-Match带着ETag标识去对比。)

5. ConnectInterceptor(client)//网络连接拦截器


该拦截器主要用于处理网络连接相关的拦截器,也是以责任链的模式进行调用的,因此我们首先看ConnectInterceptor中的intercept()方法。

@Override 
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //获取到Request请求对象
    Request request = realChain.request();
    //通过realChain获取到Transmitter对象,至于这个对象是干啥,先不用管
    Transmitter transmitter = realChain.transmitter();

    // 我们需要网络来满足这个要求。可能用于验证条件GET。(前边是直译的,简单来说是判断当前请求的类型是post还是get,如果是post则后续需要网络检测)
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //关键逻辑,通过transmitter.newExchange()创建了Exchange 对象然后调用realChain.proceed()启动下一个拦截器传递了。
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
    return realChain.proceed(request, transmitter, exchange);
  }
  • 对,没看错,该拦截器只有以上几行代码。但是连带逻辑会比较深入,我们一步一步进行分析,先看下transmitter.newExchange(chain, doExtensiveHealthChecks);内部逻辑。
 /** 返回一个新的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()");
      }
    }
    //只有这句是核心,我们需要继续跟踪
    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;
    }
  }
  • 以上逻辑中只有ExchangeCodec 对象的生成逻辑不太明确,因此我们需要继续跟踪 exchangeFinder.find(client, chain, doExtensiveHealthChecks);方法。
  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 {
      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);
    }
  }
  • 看到这个find()方法是不是有点熟悉的感觉?开始我还以为是我代码功力可以跟OKHTTP的作者睥睨了,后来才想明白原来第一个拦截器RetryAndFollowUpInterceptor中分析过相关的逻辑。
  • 此时我们使用exchangeFinder对象即是通过RetryAndFollowUpInterceptor拦截器中的transmitter.prepareToConnect()创建出来的,我粘贴一下上边描述的一段话。
    • ExchangeFinder中有个find()方法主要通过内部的 findHealthyConnection() 从 connectionPool 中找到一个可用的连接,这个连接可能是复用的,并调用RealConnection.connect(),从而得到 输入/输出 流 (source/sink) 和对应的Http2Connection对象。
  • 最后返回的ExchangeCodec 对象是通过RealConnection .newCodec(client, chain)生成的,我们继续跟进。
  ExchangeCodec newCodec(OkHttpClient client, Interceptor.Chain chain) throws SocketException {
    if (http2Connection != null) {
      return new Http2ExchangeCodec(client, this, chain, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1ExchangeCodec(client, this, source, sink);
    }
  }
  • 至此逻辑已经比较清晰了,RealConnection.newCodec()方法的返回值为ExchangeCodec ,而ExchangeCodec 为一个接口,其实现类只有两个,分别为Http2ExchangeCodec和Http1ExchangeCodec。如果http2Connection不为空则直接创建并返回Http2ExchangeCodec对象,然后返回到transmitter.newExchange()方法中,最后返回到最初的ConnectInterceptor.intercept()方法中,从而得到Exchange 对象然后通过 realChain.proceed(request, transmitter, exchange)传到下一个拦截器中。

其实该拦截器是OKHTTP中最核心的拦截器之一,以上的描述并没有最深层次的展开代码分析,为了更清晰的表述ConnectInterceptor的完整逻辑,我总结了成了以下几个步骤,可最快速度了解内部原理与逻辑。

拦截器intercept()方法有两步逻辑

  • 1.首先通过realChain.transmitter()获取到Transmitter对象,该对象是应用层和网络层通信的中转站,每一个请求都对应一个transmitter。
  • 2.通过transmitter.newExchange(.., ...)获取Exchange对象并通过realChain.proceed(..., transmitter, exchange)传递到下一个拦截器中,核心在于获取Exchange的过程

获取Exchange分为以下几步

  • 1.跟进transmitter.newExchange()后通过exchangeFinder.find(client, chain, doExtensiveHealthChecks)获取ExchangeCodec
  • 2.通过new Exchange(...)且将ExchangeCodec等参数传入获取到Exchange对象。

问题转换为ExchangeCodec对象的获取过程

  • 注意:而ExchangeCodec 为一个接口,其实现类只有两个,分别为Http2ExchangeCodec和Http1ExchangeCodec,而获取ExchangeCodec需要通过exchangeFinder.find(...)获取.
  • 1.exchangeFinder对象是在RetryAndFollowUpInterceptor拦截器逻辑中初始化的,具体通过exchangeFinder.repareToConnect()进行的初始化。
  • 2.exchangeFinder.find()中连带调用了exchangeFinder.findHealthyConnection(...)获取到了RealConnection对象(此时的逻辑是在找可用的健康连接)
  • 3.通过RealConnection.newCodec(...)获取到了ExchangeCodec对象

问题再次转换为获取RealConnection的过程

  • 1.exchangeFinder.findHealthyConnection(...)**死循环通过exchangeFinder.findConnection()**找可用的连接,同时不可用的做好标记,方便后续移除。
  • 2.第一次获取连接过程首先会尝试从transmitter.connection中获取
  • 3.第二次获取连接通过address,host,port,代理等信息去连接池匹配,如果未匹配到则取下一个代理的路由信息(多个Route,即多个IP地址),再次尝试从连接池获取。(transmitterAcquirePooledConnection()方法是尝试从线程池中获取连接的方法)
  • 4.第三次通过创建RealConnection实例,进行TCP + TLS 握手,与服务端建立连接。
  • 5.创建RealConnection实例的过程中还需要从连接池继续匹配,如果匹配到则释放刚刚创建的连接,如果没匹配到则将RealConnection加入连接池。

至此逐步返回则得到了Exchange实体,同时需要注意一个问题,怎么判断RealConnection是不是监控的连接?

  • 1.通过socket.isClosed(),isInputShutdown(),isOutputShutdown()判断socket是否可用。
  • 2.Http2的连接通过http2Connection.isShutdown()
  • 3.socket通过setSoTimeout()设置一秒延时,检测Stream是否可用,若可用则该连接不可用。

  1. CallServerInterceptor(forWebSocket)//数据流拦截器

诶呀妈呀,终于到了最后一个拦截器了,这篇博客写的太久了,终于要完结了。 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();
    //一看这名字就知道是写请求Header的
    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    //如果本次请求既不是get请求,也不是Head,并且请求的body不为空(换句话说是post请求,且有请求body)
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      //如果header中添加了(Expect: 100-continue),则需要等待服务端返回含有 "HTTP/1.1 100 Continue"的响应,然后再根据响应内容确定是否发送body,如果服务器可以接收body会返回null。
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        //询问服务器是否愿意接受body
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        //此处为服务器返回结果
        responseBuilder = exchange.readResponseHeaders(true);
      }
      //responseBuilder 为null代表服务器接收RequestBody
      if (responseBuilder == null) {
         //除非子类重写了isDuplex()方法,否则这个方法默认返回值为false
        //以下的逻辑是写body的操作。
        if (request.body().isDuplex()) {
          // Prepare a duplex body so that the application can send a request body later.
          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.
          BufferedSink bufferedRequestBody = Okio.buffer(exchange.createRequestBody(request, false));
           //写入请求主体信息
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
        //此处代表服务器不接收body,首先这个方法把资源释放了。
        exchange.noRequestBody();
        //这个判断是用于判断RealConnection对象中的http2Connection是否为空
        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.
          //如果为空,证明是http1.0,且服务器不接收body,这时应该关闭连接,防止http1.0被重用,因为只有http2.0才能被复用。
          exchange.noNewExchangesOnConnection();
        }
      }
    } else {
      //没有body,直接请求结束。
      exchange.noRequestBody();
    }

    if (request.body() == null || !request.body().isDuplex()) {
       //请求结束
      exchange.finishRequest();
    }

    if (!responseHeadersStarted) {
      //读取响应头开始回调
      exchange.responseHeadersStart();
    }

    if (responseBuilder == null) {
      //读取响应头
      responseBuilder = exchange.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
       //如果服务器返回了100就再次尝试获取真正的响应。
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual response
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }
    //回调读取响应头结束
    exchange.responseHeadersEnd(response);
    //如果状态码为101,获取响应body
    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.newBuilder()
          .body(exchange.openResponseBody(response))
          .build();
    }
    //如果请求的Header中key为“Connection”的值为"close",则请求完后需要关闭连接。
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      exchange.noNewExchangesOnConnection();
    }
    //如果状态码是204/205,那返回数据应该是空,如果返回长度大于0则报异常(ProtocolException)
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }

看到这里整个CallServerInterceptor拦截器的源码就分析结束了,其实这个拦截器是整个OKHTTP中网络io的核心,因此为了方便理解,我总结下这个拦截器内都干了什么事。

  • 1.首先通过责任链对象获取RealInterceptorChain对象,并获取到Exchange对象,通过Exchange.writeRequestHeaders()将header写入。
  • 2.如果Request中包含body,还需要判断请求头中是否包含"Expect: 100-continue"键值对,如果有则需要等待服务器返回“HTTP/1.1 100 Continue”的结果,以此结果决定是否发送body。(如果返回null则代表返回了100状态码,需要发送body,如返回4**状态码,则代表不发送body)
  • 3.通过okio.buffer()配合exchange.createRequestBody(request, true),构造出BufferedSink对象,然后通过 request.body().writeTo(BufferedSink)写请求body。
  • 4.请求发送结束后,通过exchange的responseHeadersStart()发送读取响应头回调,并通过readResponseHeaders(false)开始读取响应头(注意,如果服务器返回100状态码,需要再次读取一次响应。),读取完后需要调用exchange.responseHeadersEnd(response)回调告知读取响应头结束。
  • 5.开始通过 response.newBuilder().body(exchange.openResponseBody(response)).build();读取响应body。(具体的读取都在Http2ExchangeCodec类中进行)
  • 6.最后如果header中的键值对Connection是close,则需要在请求结束关闭连接。

这么久,终于写完了,相信看到这的你也对OKHTTP的流程有了更深入的了解,如果有错误希望各位指正。如果觉得有点收获,可以帮忙点点赞。