OkHttp讲解(二)

Android知识总结

1、拦截器 Interceptor

1.1 okhttp的工作流程图


可以看出,Interceptor贯穿了整个请求过程,是在请求执行过程中扮演重要角色。
这是okhttp的请求执行过程,从应用发出request,到应用收到response,期间经历了N个拦截器。

  • 蓝色块上方是APPLication Interceptor,也就是应用程序拦截器,即开发者自己自定义的拦截器,代码中的client.interceptors()获取的就是这类拦截器
  • 蓝色块下方是NetWork Interceptor,也就是网络拦截器,用来观察单个网络请求和响应,只能调用一次proceed方法
  • 蓝色块代表的就是OKHttp提供的拦截器,共有5个,也是我们需要关注的重点

1.2 拦截器的分类

okhttp工作流程图中,橙色框框内的那些拦截器,属于okhttp库内部定义的,一般情况下不会更改。所以这里只讨论开发者能够自定义的拦截器。
  分为两类:
  1)ApplicationInterceptor(应用拦截器)
  2)NetworkInterceptor(网络拦截器)

相同点

  • 都能对server返回的response进行拦截
  • 这两种拦截器本质上都是基于Interceptor接口,由开发者实现这个接口,然后将自定义的Interceptor类的对象设置到okhttpClient对象中。所以,他们的对象,本质上没什么不同,都是Interceptor的实现类的对象。
  • 两者都会被add到OkHttpClient内的一个ArrayList中。当请求执行的时候,多个拦截器会依次执行(list本身就是有序的)。

不同点

  • okhttpClient添加两种拦截器的api不同。添加应用拦截器的接口是addInterceptor(),而添加网络拦截器的接口是addNetworkInterceptor().
  • 两者负责的区域不同,从最上方图中可以看出,应用拦截器作用于okhttpCore到Application之间,网络拦截器作用于 network和okhttpCore之间
  • 在某种特殊情况下(比如:访问了一个url,结果这个url发生了重定向),网络拦截器有可能被执行多次,但是不论任何情况,application只会被执行一次。
  • 在执行时addInterceptor一定会执行,addNetworkInterceptor不一定会执行,因为在addNetworkInterceptor之前的Interceptor发生异常退出时addNetworkInterceptor就不会被执行。
  • addInterceptor 先拿到的是Request后拿到的是Response,addNetworkInterceptor反之。
  • 打印Log日志时,一把放在 addNetworkInterceptor,因为其打印的是完整的 Request。

1.3 okhttp库内部定义的拦截器

1.3.0、RealCall

在okhttp框架中,当客户端通过OkHttpClient发起同步或异步请求时,okhttp框架将会创建一个RealCall,这个实例将根据客户端提供的Request,发起同步或异步网络请求操作,在RealCall被创建时,将会创建一个Interceptor的具体实现。我们知道,okhttp框架将网络请求的步骤,通过Interceptor接口进行了统一的分层式设计,将每个环节都分成了不同的Interceptor,Interceptor又被称为拦截器,这是该网络框架设计的精髓所在,通过不同的拦截器规则,处理网络请求过程中的不同环节,最终通过链式调用,实现一个完整的网络请求操作。

  Response getResponseWithInterceptorChain() throws IOException {
    List<Interceptor> interceptors = new ArrayList<>();
    //添加开发者应用层自定义的Interceptor
    interceptors.addAll(client.interceptors());
    //这个Interceptor是处理请求失败的重试,重定向    
    interceptors.add(retryAndFollowUpInterceptor);
    //这个Interceptor工作是添加一些请求的头部或其他信息
    //并对返回的Response做一些友好的处理(有一些信息你可能并不需要)
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //这个Interceptor的职责是判断缓存是否存在,读取缓存,更新缓存等等
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //这个Interceptor的职责是建立客户端和服务器的连接
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      //添加开发者自定义的网络层拦截器
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));
    //一个包裹这request的chain
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    //把chain传递到第一个Interceptor手中
    return chain.proceed(originalRequest);
  }

1.3.1、RetryAndFollowUpInterceptor 重试和失败重定向拦截器

这个拦截器它的作用主要是负责请求的重定向操作,用于处理网络请求中,请求失败后的重试链接。把StreamAllocation对象,传递给后面的拦截器。

private static final int MAX_FOLLOW_UPS = 20;

从这个静态变量可以看出 RetryAndFollowUpInterceptor 重定向最多20次。

 @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    //创建一个新的流
    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);
    //重定向次数
    int followUpCount = 0;
     // 上一个重试得到的响应
    Response priorResponse = null;
    while (true) {
      //如果RealCall调用了cancel,即取消请求,那就释放资源,抛出异常结束请求
      if (canceled) {
          //如果取消了则删除连接上的call请求
        streamAllocation.release();
        throw new IOException("Canceled");
      }
      // 定义请求的响应
      Response response = null;
      //// 是否释放连接,默认为true
      boolean releaseConnection = true;
      try {
        //调用下一个拦截器 即BridgeInterceptor;进行网络连接,获取response
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
        // 如果没有发送异常,修改标志 不需要重试
        releaseConnection = false;
      } catch (RouteException e) {
        //出现路由连接异常,通过recover方法判断能否恢复连接,如果不能将抛出异常不再重试
        //recover(...)检测连接是否还可以继续
        if (!recover(e.getLastConnectException(), false, request)) {
          throw e.getLastConnectException();
        }
        //能恢复连接,修改标志 不释放连接
        releaseConnection = false;
          //回到下一次循环 继续重试 除了finally代码外,下面的代码都不会执行
        continue;
      } catch (IOException e) {//后续拦截器在与服务器通信中抛出IO异常
        //判断该异常是否是连接关闭异常
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        ////通过recover方法判断能否恢复连接,如果不能将抛出异常不再重试
        if (!recover(e, requestSendStarted, request)) throw e;
        //能恢复连接, 修改标志 不释放连接
        releaseConnection = false;
        //回到下一次循环 继续重试 除了finally代码外,下面的代码都不会执行
        continue;
      } finally {
        // 如果releaseConnection为true,说明后续拦截器抛出了其它异常,那就释放所有资源,结束请求
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

      // 走到这里,说明网络请求已经完成了,但是响应码并不一定是200
      // 可能是其它异常的响应码或者重定向响应码
      
      // 如果priorResponse 不等于null,说明前面已经完成了一次请求
      // 那就通过上一次的response构建新的response,但是body为null.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }
      //对response进行响应码的判断,如果需要进行重定向,那就获取新的Request
      Request followUp = followUpRequest(response);
      // 如果为null,那就没必要重新请求,说明已经有了合适的Response,直接返回
      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }
       //关闭,忽略任何已检查的异常
      closeQuietly(response.body());
      //检测followUp(重定向)次数是否超过20次,如果超过就抛出异常,避免消耗客户端太多资源
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      //如果该请求体被UnrepeatableRequestBody标记,则不可重试
      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }
      //判断重连前的Request与重新构建的Request是否有相同的连接,即host、port、scheme是否一致
      if (!sameConnection(response, followUp.url())) {
        // 如果不是相同的url连接,先释放之间的,再创建新的StreamAllocation
        streamAllocation.release();
        streamAllocation = new StreamAllocation(
            client.connectionPool(), createAddress(followUp.url()), callStackTrace);
      } else if (streamAllocation.codec() != null) {
         // 如果相同,但是本次请求的流没有关闭,那就抛出异常
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }
       //把重定向的请求赋值给request,以便再次进入循环执行
      request = followUp;
      priorResponse = response;
    }
  }

RetryAndFollowUpInterceptor ,主要就是完成两件事情:重试重定向

  • 重试
    重试流程

请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。
RouteException

    catch (RouteException e) {
        // 路由异常,连接未成功,请求还没发出去
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;
     } 

IOException

     catch (IOException e) {
        // 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
        //HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } 

两个异常都是根据 recover 方法判断是否能够进行重试,如果返回 true ,则表示允许重试。

 /**
  * 判断当与服务器通信失败时,连接能否进行恢复
  * 返回true,表示可以进行恢复
  * 返回false 表示不能恢复,即不能重连
  */
  private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
    //根据抛出的异常,做出连接、连接路线的一些处理,并且释放连接,关闭连接
    streamAllocation.streamFailed(e);
    // 判断开发者是否禁用了失败重连
    // 在构建OKHttpClient的时候可以通过build进行配置
    //如果禁用,那就返回false,不进行重连
    if (!client.retryOnConnectionFailure()) return false;
    // 如果不是连接关闭异常,且请求体被UnrepeatableRequestBody标记,那不能恢复
    //如果是IOException,由于requestSendStarted只在http2的io异常中可能为false
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
    // 判断是不是属于重试的异常
    if (!isRecoverable(e, requestSendStarted)) return false;
    // 有没有可以用来连接的路由路线
    // 如果没有,返回false
    if (!streamAllocation.hasMoreRoutes()) return false;
    // 走到这里说明可以恢复连接,尝试重连
    return true;
  }

所以首先使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行请求的重试。其中某些异常是在 isRecoverable 中进行判断:

  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // 出现协议异常,不能重试
    if (e instanceof ProtocolException) {
      return false;
    }
    // 如果不是超时异常,不能重试
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }
    //SSL握手异常中,证书出现问题,不能重试
    if (e instanceof SSLHandshakeException) {
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    // SSL握手未授权异常 不能重试
    if (e instanceof SSLPeerUnverifiedException) {
      return false;
    }
    return true;
  }
  • 1、协议异常:如果是那么直接判定不能重试;(你的请求或者服务器的响应本身就存在问题,没有按照http协议来定义数据,再重试也没用)
  • 2、超时异常:可能由于网络波动造成了Socket连接的超时,可以使用不同路线重试。
  • 3、SSL证书异常/SSL验证失败异常:前者是证书验证失败,后者可能就是压根就没证书,或者证书数据不正确,那还怎么重试?

经过了异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有可用路由路线来进行连接。简单来说,比如 DNS 对域名解析后可能会返回多个 IP,在一个IP失败后,尝试另一个IP进行重试。

  • 重定向

如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重定向的判断。重定向的判断位于 followUpRequest 方法

  private Request followUpRequest(Response userResponse, 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-Authorization”,让代理服务器授权
      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 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization”
      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);
      // 308 永久重定向
      // 307 临时重定向
      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
        // 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      // 300 301 302 303
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // 如果用户不允许重定向,那就返回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);

        // 如果为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 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, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();
      //408 客户端请求超时
      case HTTP_CLIENT_TIMEOUT:
        //408 算是连接失败了,所以判断用户是不是允许重试
        if (!client.retryOnConnectionFailure()) {
          // The application layer has directed us not to retry the request.
          return null;
        }
        // UnrepeatableRequestBody实际并没发现有其他地方用到
        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
          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();
      //503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求
      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;
    }
  }

整个是否需要重定向的判断内容很多,记不住,这很正常,关键在于理解他们的意思。如果此方法返回空,那就表示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的 Request ,但是需要注意的是,我们的 followup 在拦截器中定义的最大次数为20次。

总结

本拦截器是整个责任链中的第一个,这意味着它会是首次接触到 Request 与最后接收到 Response 的角色,在这个拦截器中主要功能就是判断是否需要重试重定向
重试的前提是出现了 RouteException 或者 IOException 。一但在后续的拦截器执行过程中出现这两个异常,就会通过 recover 方法进行判断是否进行连接重试
重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用 followUpRequest 根据 Response 的响应码(当然,如果直接请求失败, Response 都不存在就会抛出异常)。 followup 最大发生20次。

1.3.2、BridgeInterceptor 桥接拦截器

主要是补充用户创建请求当中缺少的一些必要的请求头。BridgeInterceptor 为用户构建的一个 Request 请求转化为能够进行网络访问的请求,同时将网络请求回来的响应 Response 转化为用户可用的 Response。比如设置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。这个拦截器相对比较简单。

得到响应:

  • 1、读取Set-Cookie响应头并调用接口告知用户,在下次请求则会读取对应的数据设置进入请求头, 默认CookieJar无实现;
  • 2、响应头Content-Encoding为gzip,使用GzipSource包装解析。
@Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    //组织Request Header包括这是keep-alive, Cookie添加,gzip等
    ....
    //传递
    Response networkResponse = chain.proceed(requestBuilder.build());
    //组织Response Header 包括cookie保存更新,Gzip解压等
    ....
    return responseBuilder.build();
  }

请求头

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

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情

  • 1、保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的 CookieJar 不提供实现
  • 2、如果使用gzip返回的数据,则使用 GzipSource 包装便于解析。

总结

桥接拦截器的执行逻辑主要就是以下几点:
对用户构建的 Request 进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的 Request 将符合网络请求规范的Request交给下一个拦截器处理,并获取 Response 如果响应体经过了GZIP压缩,那就需要解压,再构建成用户可用的 Response 并返回。

1.3.3、CacheInterceptor 缓存拦截器

如果当前未使用网络,并且缓存不可以使用,通过构建者模式创建一个Response响应,抛出504错误。如果有缓存 但是不能使用网络 ,直接返回缓存结果。这是在进行网络请求之前所做的事情,当网络请求完成,得到下一个拦截器返回的response之后,判断response的响应码是否是HTTP_NOT_MODIFIED = 304,(未改变)是则从缓存中读取数据。

 @Override 
  public Response intercept(Chain chain) throws IOException {
    //通过request从缓存中获取响应(只会存在Get请求的缓存)
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
  /**
    * CacheStrategy 是一个缓存策略类 比如强制缓存 对比缓存等 它决定是使用缓存还是进行网络请求
    * 其内部维护了Request、Response
    * 如果Request为null表示不使用网络
    * 如果Response为null表示不使用缓存
    */
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //根据缓存策略获取缓存Request和Response
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    //根据缓存策略,更新统计指标:请求次数、使用网络请求次数、使用缓存次数
    if (cache != null) {
      cache.trackResponse(strategy);
    }
     // 能从缓存中获取响应但是缓存策略是不使用缓存,那就关闭获取的缓存
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // 根据策略,不使用网络,又没有缓存的直接报错,并返回错误码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 {
      //前面两个都没有返回,继续执行下一个Interceptor,即ConnectInterceptor
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // 如果发生了IO异常或者其它异常,关闭缓存避免内存泄漏
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // 如果缓存策略是可以使用缓存
    if (cacheResponse != null) {
      // 且网络响应码是304 HTTP_NOT_MODIFIED说明本地缓存可以使用
      // 且网络响应是没有响应体的
      // 这时候就合并缓存响应和网络响应并构建新的响应
      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();
        // 在合并标头之后但在剥离Content-Encoding标头之前更新缓存
        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)) {
        // 缓存响应的部分信息
        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;
  }

CacheInterceptor ,在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。 (只会存在Get请求的缓存)

步骤为:

  • 1、从缓存中获得对应请求的响应缓存
  • 2、创建 CacheStrategy ,创建时会判断是否能够使用缓存,在 CacheStrategy 中存在两个成员: networkRequest与 cacheResponse 。他们的组合如下:

拦截器通过CacheStrategy判断使用缓存或发起网络请求。此对象中的networkRequest与cacheResponse分别代表 需要发起请求或者直接使用缓存



即:networkRequest存在则优先发起网络请求,否则使用cacheResponse缓存,若都不存在则请求失败!

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

缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多。在缓存拦截器中判断是否可以使用缓存,或是请求服务器都是通过 CacheStrategy 判断。

缓存策略
CacheStrategy 。首先需要认识几个请求头与响应头

其中 Cache-Control 可以在请求头存在,也能在响应头存在,对应的value可以设置多种组合

  • 1、max-age=[秒] :资源最大有效时间;
  • 2、public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
  • 3、private :表明该资源只能被单个用户缓存,默认是private
  • 4、no-store :资源不允许被缓存
  • 5、no-cache :(请求)不使用缓存
  • 6、immutable :(响应)资源不会改变
  • 7、min-fresh=[秒] :(请求)用户认为这个缓存有效的时长
  • 8、must-revalidate :(响应)不允许使用过期缓存
  • 9、max-stale=[秒] :(请求)缓存过期后多久内仍然有效

假设存在max-age=100,min-fresh=20。这代表了用户认为这个缓存的响应,从服务器创建响应 到 能够缓存使用的时间为100-20=80s。但是如果max-stale=100。这代表了缓存有效时间80s过后,仍然允许使用100s,可以看成缓存有效时长为180s。

缓存检测过程

1.3.4、ConnectInterceptor 连接拦截器

链接流程

在 okhttp底层是通过 socket 的方式于服务端进行连接的,并且在连接建立之后会通过 okio 获取通向 server 端的输入流 Source 和输出流 Sink。

@Override 
public Response intercept(Chain chain) throws IOException {
   RealInterceptorChain realChain = (RealInterceptorChain) chain;
   Request request = realChain.request();
    //获取可复用流
   StreamAllocation streamAllocation = realChain.streamAllocation();
   boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //创建输出流
   HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
   //根据HTTP/1.x(keep-alive)和HTTP/2(流复用)的复用机制,发起连接
   RealConnection connection = streamAllocation.connection();
   return realChain.proceed(request, streamAllocation, httpCodec, connection);
 }

虽然代码量很少,实际上大部分功能都封装到其它类去了,这里只是调用而已。
首先我们看到的 StreamAllocation 这个对象是在第一个拦截器:重定向拦截器创建的,但是真正使用的地方却在这里。
"当一个请求发出,需要建立连接,连接建立后需要使用流用来读写数据";而这个StreamAllocation就是协调请求、连接与数据流三者之间的关系,它负责为一次请求寻找连接,然后获得流来实现网络通信。
这里使用的 newStream 方法实际上就是去查找或者建立一个与请求主机有效的连接,返回的 HttpCodec 中包含了输入输出流,并且封装了对HTTP请求报文的编码与解码,直接使用它就能够与请求主机完成HTTP通信。
StreamAllocation 中简单来说就是维护连接: RealConnection ——封装了Socket与一个Socket连接池。可复用的 RealConnection 需要

  public boolean isEligible(Address address, @Nullable Route route) {
    // If this connection is not accepting new streams, we're done.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // If the non-host fields of the address don't overlap, we're done.
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // If the host exactly matches, we're done: this connection can carry the address.
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }
    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;

    // 2. The routes must share an IP address. This requires us to have a DNS address for both
    // hosts, which only happens after route planning. We can't coalesce connections that use a
    // proxy, since proxies don't tell us the origin server's IP address.
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;

    // 3. This connection's server certificate's must cover the new host.
    if (route.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.
  }

以上代码解析:

 if (allocations.size() >= allocationLimit || noNewStreams) return false;

连接到达最大并发流或者连接不允许建立新的流;如http1.x正在使用的连接不能给其他人用(最大并发流为:1)或者连接被关闭;那就不允许复用;

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

DNS、代理、SSL证书、服务器域名、端口完全相同则可复用;
如果上述条件都不满足,在HTTP/2的某些场景下可能仍可以复用(http2先不管)。
所以综上,如果在连接池中找到个连接参数一致并且未被关闭没被占用的连接,则可以复用。

总结

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

1.3.5、CallServerInterceptor 请求服务器拦截器(OkHttp核心拦截器,网络交互的关键)

主要负责将请求写入到 IO 流当中,并且从 IO 流当中获取服务端返回给客服端的响应数据。
CallServerInterceptor 在 ConnectInterceptor 拦截器的功能就是负责与服务器建立 Socket 连接,并且创建了一个 HttpStream 它包括通向服务器的输入流和输出流。而接下来的 CallServerInterceptor 拦截器的功能使用 HttpStream 与服务器进行数据的读写操作的
okhttp的拦截器就是在intercept(Chain chain)的回调中对Request和Response进行修改,然后直接返回了response 而不是进行继续递归,具体执行RealConnection里面是通过OKio实现的。在okhttp中,网络连接也是一个拦截器(CallServerInterceptor),他是最后一个被调用的,负责将request写入网络流中,并从网络流中读取服务器返回的信息写入Response中返回给客户端。

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();
    //发送请求的时间戳
    long sentRequestMillis = System.currentTimeMillis();
    //写入请求头信息
    httpCodec.writeRequestHeaders(request);
     //发送header数据
    httpCodec.writeRequestHeaders(request);
    Response.Builder responseBuilder = null;
    //根据是否支持100-continue,发送body数据
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        realChain.eventListener().requestBodyStart(realChain.call());
        long contentLength = request.body().contentLength();
        CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
        realChain.eventListener()
            .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
      } else if (!connection.isMultiplexed()) {
        // HTTP2多路复用,不需要关闭socket,不管!.
        streamAllocation.noNewStreams();
      }
    }
    //结束请求
    httpCodec.finishRequest();
    if (responseBuilder == null) {
     realChain.eventListener().responseHeadersStart(realChain.call());
      //读取响应头信息
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        //发送请求的时间
        .sentRequestAtMillis(sentRequestMillis)
        //接收到响应的时间
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

      int code = response.code();
      //response处理
      if (code == 100) {
      responseBuilder = httpCodec.readResponseHeaders(false);
      response = responseBuilder
              .request(request)
              .handshake(streamAllocation.connection().handshake())
              .sentRequestAtMillis(sentRequestMillis)
              .receivedResponseAtMillis(System.currentTimeMillis())
              .build();

      code = response.code();
    }
    realChain.eventListener()
            .responseHeadersEnd(realChain.call(), response);
    if (forWebSocket && code == 101) {
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    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 ,利用 HttpCodec 发出请求到服务器并且解析生成 Response 。
首先调用 httpCodec.writeRequestHeaders(request); 将请求头写入到缓存中(直到调用 flushRequest() 才真正发送给服务器)。然后马上进行第一个逻辑判断。

    //根据是否支持100-continue,发送body数据
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        realChain.eventListener().requestBodyStart(realChain.call());
        long contentLength = request.body().contentLength();
        CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
        realChain.eventListener()
            .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
      } else if (!connection.isMultiplexed()) {
        // HTTP2多路复用,不需要关闭socket,不管!.
        streamAllocation.noNewStreams();
      }
    }

整个if都和一个请求头有关: Expect: 100-continue 。这个请求头代表了在发送请求体之前需要和服务器确定是否愿意接受客户端发送的请求体。所以 permitsRequestBody 判断为是否会携带请求体的方式(POST),如果命中 if,则会先给服务器发起一次查询是否愿意接收请求体,这时候如果服务器愿意会响应100(没有响应体,responseBuilder 即为nul)。这时候才能够继续发送剩余请求数据。
但是如果服务器不同意接受请求体,那么我们就需要标记该连接不能再被复用,调用 noNewStreams() 关闭相关的 Socket。

    if (responseBuilder == null) {
     realChain.eventListener().responseHeadersStart(realChain.call());
      //读取响应头信息
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        //发送请求的时间
        .sentRequestAtMillis(sentRequestMillis)
        //接收到响应的时间
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

这时 responseBuilder 的情况即为:

  • 1、POST方式请求,请求头中包含 Expect ,服务器允许接受请求体,并且已经发出了请求体, responseBuilder为null;
  • 2、POST方式请求,请求头中包含 Expect ,服务器不允许接受请求体, responseBuilder 不为null
  • 3、POST方式请求,未包含 Expect ,直接发出请求体, responseBuilder 为null;
  • 4、POST方式请求,没有请求体, responseBuilder 为null;
  • 5、GET方式请求, responseBuilder 为null;

对应上面的5种情况,读取响应头并且组成响应 Response ,注意:此 Response 没有响应体。同时需要注意的是,如果服务器接受 Expect: 100-continue 这是不是意味着我们发起了两次 Request ?那此时的响应头是第一次查询服务器是否支持接受请求体的,而不是真正的请求对应的结果响应。所以紧接着:

      int code = response.code();
      //response处理
      if (code == 100) {
      responseBuilder = httpCodec.readResponseHeaders(false);
      response = responseBuilder
              .request(request)
              .handshake(streamAllocation.connection().handshake())
              .sentRequestAtMillis(sentRequestMillis)
              .receivedResponseAtMillis(System.currentTimeMillis())
              .build();

      code = response.code();
    }

如果响应是100,这代表了是请求 Expect: 100-continue 成功的响应,需要马上再次读取一份响应头,这才是真正的请求对应结果响应头。

    if (forWebSocket && code == 101) {
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
    return response;

forWebSocket 代表websocket的请求,我们直接进入else,这里就是读取响应体数据。然后判断请求和服务器是不是都希望长连接,一旦有一方指明 close ,那么就需要关闭 socket 。而如果服务器返回204/205,一般情况而言不会存在这些返回码,但是一旦出现这意味着没有响应体,但是解析到的响应头中包含 Content-Lenght 且不为 0,这表响应体的数据字节长度。此时出现了冲突,直接抛出协议异常

二、 RealInterceptorChain 拦截器链

  • 当发送一个请求的时候,实质OkHttp会通过一个拦截器的链来执行OkHttp的请求。
  • 这就是所谓的拦截器链,执行 RetryAndFollowUpInterceptor => 执行 BridgeInterceptor => 执行 CacheInterceptor => 执行 ConnectInterceptor => 执行 CallServerInterceptor => 响应到
    ConnectInterceptor => 响应到 CacheInterceptor => 响应到 BridgeInterceptor => 响应到 RetryAndFollowUpInterceptor
public final class RealInterceptorChain implements Interceptor.Chain {
  private final List<Interceptor> interceptors;
  //在RetryAndFollowUpInterceptor中new的
  private final StreamAllocation streamAllocation;
  //在ConnectInterceptor中new的
  private final HttpCodec httpCodec;
  //在ConnectInterceptor中new的
  private final RealConnection connection;
  //标识应该取拦截器链表里面的第几个拦截器
  private final int index;  //通过index + 1
  private int calls;  //通过call++
  private final Request request;

  public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
      HttpCodec httpCodec, RealConnection connection, int index, Request request) {
    this.interceptors = interceptors;
    this.connection = connection;
    this.streamAllocation = streamAllocation;
    this.httpCodec = httpCodec;
    this.index = index;
    this.request = request;
  }

  @Override public Connection connection() {
    return connection;
  }

  public StreamAllocation streamAllocation() {
    return streamAllocation;
  }

  public HttpCodec httpStream() {
    return httpCodec;
  }

  @Override public Request request() {
    return request;
  }
  //执行继续拦截操作
  @Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
  }
/**
  * 依次取出拦截器链表中的每个拦截器去获取Response
  */
  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
     // 1、迭代拦截器集合
    if (index >= interceptors.size()) throw new AssertionError();
     //2、记录本方法调用次数,创建一次实例,call加1
    calls++;

    //如果已经为该Request创建了stream,就不再继续创建了
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    //  如果已经为该Request创建了stream,那该方法只能调用一次
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

   //创建新的拦截器链对象, 并将计数器+1
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    //取出下一个拦截器
    Interceptor interceptor = interceptors.get(index);
    //执行拦截器的intercept方法获取结果,并将新的拦截器链对象传入
    Response response = interceptor.intercept(next);

    // 确保该方法只能调用一次
    if (httpCodec != 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");
    }

    return response;
  }
}

三、总结

在这个拦截器中就是完成HTTP协议报文的封装与解析。

OkHttp网络请求的整体接口图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容