OKHttp3--重试及重定向拦截器RetryAndFollowUpInterceptor源码解析【六】

系列

OKHttp3–详细使用及源码分析系列之初步介绍【一】
OKHttp3–流程分析 核心类介绍 同步异步请求源码分析【二】
OKHttp3–Dispatcher分发器源码解析【三】
OKHttp3–调用对象RealCall源码解析【四】
OKHttp3–拦截器链RealInterceptorChain源码解析【五】
OKHttp3–重试及重定向拦截器RetryAndFollowUpInterceptor源码解析【六】
OKHttp3–桥接拦截器BridgeInterceptor源码解析及相关http请求头字段解析【七】
OKHttp3–缓存拦截器CacheInterceptor源码解析【八】
OKHttp3-- HTTP缓存机制解析 缓存处理类Cache和缓存策略类CacheStrategy源码分析 【九】

RetryAndFollowUpInterceptor

上一篇文章解析了RealInterceptorChain,紧接着该篇文章来解析拦截器链中的第一个拦截器RetryAndFollowUpInterceptor(重试及重定向拦截器)

核心功能

我们应该怎么理解这个拦截器呢?

其实从名字也可以大概知道它的意思,主要有两个方面:

  • 请求失败后重新尝试连接:从Retry这个单词理解,但是在OKHttp中并不是所有的请求失败后(即返回码不是200)都会去重新连接,而是在发生 RouteException 或者 IOException 后再根据一些策略进行一些判断,如果可以恢复,就重新进请求
  • 继续请求:FollowUp本意是跟进的意思,主要有以下几种类型可以继续发起请求
    • 407/401:未进行身份认证,需要对请求头进行处理后再发起新的请求
    • 408:客户端请求超时,如果 Request 的请求体没有被 UnrepeatableRequestBody 标记,会继续发起新的请求
    • 308/307/303/302/301/300:需要进行重定向,发起新的请求
      比如:
      client向server发送一个请求,要求获取一个资源
      但是server收到请求后发现这个资源在另一个位置,然后server就在返回的response的头部header的location字段中存入该资源新的地址url,并设置响应码为30x(常用状态码有301,303,也有临时码302,307)
      client收到响应后,根据响应码判断这是一个重定向的响应,就去解析url,最后再次发出请求获取资源

其中FollowUp的次数受到限制,OKHTTP内部限制次数为20次以内,避免消耗过多资源,20次是一个综合考虑的结果

Chrome遵循21次重定向; Firefox,curl和wget遵循20; Safari跟随16; HTTP / 1.0建议5

RetryAndFollowUpInterceptor.intercept

既然是拦截器,那就必须要走intercept方法

  /**
  * 拦截器的拦截方法,主要作用是失败重连和重定向
  */
  @Override 
  public Response intercept(Chain chain) throws IOException {
    // 获取请求对象
    Request request = chain.request();
   /**
   * 实例化一个StreamAllocation对象,是一个管理类,字面理解是分配流,分配与服务器数据传输的流
   * 是用来建立HTTP请求所需的网络设施组件,比如说HttpCodec(跟服务端进行数据传输的流 HttpStream)、连接服务器的RealConnection等
   * 它还提供了调用RealConnection的connect()方法与服务器建立连接的方法,提供了断开连接的方法release(),提供了对路由的判断等等
   * 在这个拦截器里没有用到,真正使用的地方是在ConnectInterceptor
   */
    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);

    // 重连次数(包括重定向次数)
    int followUpCount = 0;
    // 上一个重试得到的响应
    Response priorResponse = null;
    // 死循环
    while (true) {

      // 如果RealCall调用了cancel,即取消请求,那就释放资源,抛出异常结束请求
      if (canceled) {
        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
      // followUpRequest方法见下方
      Request followUp = followUpRequest(response);

      // 如果为null,那就没必要重新请求,说明已经有了合适的Response,直接返回
      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }

      //关闭,忽略任何已检查的异常
      closeQuietly(response.body());

      //检测重连次数是否超过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,在下一次循环中使用
      priorResponse = response;

      // 本次循环结束,进入下一个循环,重新连接
    }
  }

该方法里还涉及到几个小方法的调用,见下方

RetryAndFollowUpInterceptor.recover

  /**
  * 判断当与服务器通信失败时,连接能否进行恢复
  * 返回true,表示可以进行恢复
  * 返回false 表示不能恢复,即不能重连
  */
  private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
    //根据抛出的异常,做出连接、连接路线的一些处理,并且释放连接,关闭连接
    streamAllocation.streamFailed(e);
   
    // 判断开发者是否禁用了失败重连
    // 在构建OKHttpClient的时候可以通过build进行配置
    //如果禁用,那就返回false,不进行重连
    if (!client.retryOnConnectionFailure()) return false;

    // 如果不是连接关闭异常,且请求体被UnrepeatableRequestBody标记,那不能恢复
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    // 根据异常判断是否可以重连
    if (!isRecoverable(e, requestSendStarted)) return false;

    // 判断还有没有多余线路进行连接
    // 如果没有,返回false
    if (!streamAllocation.hasMoreRoutes()) return false;

    // 走到这里说明可以恢复连接,尝试重连
    return true;
  }

RetryAndFollowUpInterceptor.hasMoreRoutes

   public boolean hasMoreRoutes() {
       return route != null || routeSelector.hasNext();
   }  

前面三个条件都通过了,就需要进行最后一步检验,判断是否有可用的路由,所以只要route为null且RouteSelector中没有可选择的路由了,那就返回false,不能进行重连

其中RouteSelector 封装了选择可用路由进行连接的策略,因为同一个请求会存在多个IP,多个代理的情况;比如 DNS 对域名解析后会返回多个 IP,在一个IP失败后,尝试另一个IP,而不是同一个代理或 IP 反复重试;而没有可选择的路由意味着

  • 没有下一个 IP
  • 没有下一个代理
  • 没有下一个延迟使用的 Route(之前有失败过的路由,会在这个列表中延迟使用)

RetryAndFollowUpInterceptor.isRecoverable

  /**
  * 根据异常类型判断能否恢复连接
  */
  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // 出现协议异常,不能恢复
    if (e instanceof ProtocolException) {
      return false;
    }

    // 如果是中断异常,即IO连接中断
    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;
  }

RetryAndFollowUpInterceptor.followUpRequest

  /**
  * 根据网络请求的响应码Code,判断当前请求是否还需要进一步添加身份验证或者重定向,
  * 如果需要则构建新的Request返回,用于重新发送请求,否则返回null
  */
  private Request followUpRequest(Response userResponse) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    Connection connection = streamAllocation.connection();
    Route route = connection != null
        ? connection.route()
        : null;
    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();
        // 如果代理协议不是HTTP协议,那就抛出异常
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        //在请求头中添加 “Proxy-Authorization”授权后可以尝试重新连接
        return client.proxyAuthenticator().authenticate(route, userResponse);

      // 401 未认证 需要身份验证
      case HTTP_UNAUTHORIZED:
        //在请求头中添加 “Authorization” 可以尝试重新连接
        return client.authenticator().authenticate(route, userResponse);
      // 308 永久重定向 
      case HTTP_PERM_REDIRECT:
      //  307 临时重定向
      case HTTP_TEMP_REDIRECT:
        // 如果接收到的状态码是307或者308去请求除GET或者HEAD以外的方法,用户代理不得自动重定向请求
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      // 300  响应存在多种选择,需要客户端做出其中一种选择
      case HTTP_MULT_CHOICE:
      // 301 请求的资源路径永久改变
      case HTTP_MOVED_PERM:
      // 302 请求资源路径临时改变
      case HTTP_MOVED_TEMP:
      // 303 服务端要求客户端使用GET访问另一个URI
      case HTTP_SEE_OTHER:
        // 如果开发者不允许重定向,那就返回null
        if (!client.followRedirects()) return null;
        // 从头部取出location 
        String location = userResponse.header("Location");
        if (location == null) return null;
        // 从location 中取出HttpUrl 
        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;

        // 大多数重定向不包含请求体
        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, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      // 408 客户端请求超时
      case HTTP_CLIENT_TIMEOUT:
        // 408在实际开发中很少见,但是像HAProxy这样的服务器使用这个响应代码
        // 请求体是否被UnrepeatableRequestBody标记,如果被标记,就不能进行重连
        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
          return null;
        }
        return userResponse.request();

      default:
        return null;
    }
  }

RetryAndFollowUpInterceptor.sameConnection

  /**
  * 比较已经完成的请求的Url和经过followUpRequest()构建的新Request的Url
  * 判断它们的host、port、scheme协议是否一致
  * 如果不一致则以followUpRequest()返回的Request为准,释放掉旧的StreamAllocation,创建新的StreamAllocation重新向服务器发送请求
  */
  private boolean sameConnection(Response response, HttpUrl followUp) {
    HttpUrl url = response.request().url();
    return url.host().equals(followUp.host())
        && url.port() == followUp.port()
        && url.scheme().equals(followUp.scheme());
  }

RouteException

该异常最终是由RealConnection.connect和StreamAllocation.newStream这两个方法抛出的,而newStream方法又是由ConnectInterceptor的intercept方法内部调用的(newStream方法最终会调用connect方法);connect()方法是与服务器建立连接,newStream()是获取流,所以在连接拦截器中抛出也是正常的

要注意抛出这个异常意味着请求还没有发出去,只在连接阶段出问题了,就是打开Socket失败了,比如连接超时抛出的 SocketTimeoutException,包裹在 RouteException 中

RouteException 是 OkHttp 自定义的异常,是一个包裹类,包裹住了建连失败中发生的各种 Exception

IOException

该异常抛出说明Request很有可能发出且在读取Response的过程中,TCP连接已经建立

主要发生在 CallServerInterceptor 中,通过建立好的通道,发送请求并且读取响应的环节

总结

该类的几个重要方法都分析完了,是时候来总结下该拦截器的工作逻辑了

  • 获取请求对象Request和实例化streamAllocation ,streamAllocation 真正被使用是在ConnectInterceptor拦截器里,在本拦截器只是进行资源释放等工作
  • 进入while循环
    1. 如果用户取消了该请求,即关闭了Socket连接,那就抛出IO异常结束循环

    2. 调用下一个拦截器的proceed方法获取响应Response,如果该方法没有抛出异常,就修改releaseConnection 为false,即不需要重连

    3. 如果后续拦截器在执行过程中抛出RouteException,会通过recover方法进行判断是否进行连接重试,判断条件如下:

      • 如果开发者禁用了失败重连,那就不能重试
      • 如果不是ConnectionShutdownException(连接关闭异常),且请求体被UnrepeatableRequestBody标记,那不能恢复
      • 如果没有多余线路进行连接,那就不能恢复
      • 根据异常类型进行判断能否恢复:
        * 出现ProtocolException(协议异常),不能恢复;主要发生在 RealConnection 中通过代理隧道构建HTTPS连接次数超过 21 次
        * 如果是InterruptedIOException(中断异常),这里分为两步:A 如果是建立连接时发生该异常,即建立TCP连接超时,那返回true,recover方法继续执行;B 如果是连接已经建立了,读取响应发生该异常,那返回false,recover方法直接return,不能恢复
        * 如果是SSLHandshakeException(SSL握手异常)且证书出现问题,就不能进行恢复;比如证书制作错误
        * 如果是SSLPeerUnverifiedException(SSL握手未授权异常),不能进行恢复,比如证书校验失败,访问网站的证书不在你可以信任的证书列表中
    4. 如果后续拦截器在执行过程中抛出IOException,也要通过recover方法进行判断是否进行连接重试,同上

    5. 在finally代码块中判断,如果releaseConnection为true,说明后续拦截器抛出了其它异常,那就释放所有资源,结束请求

    6. 走到这一步,说明一次网络请求已经完成,但并不说明这个请求是成功的,需要通过followUpRequest方法对响应码进行判断,是否需要进行身份验证或者重定向,如果需要就构建新的Request,如下:

      • 407/401:未进行身份认证,需要对请求头进行处理后再发起新的请求
      • 408:客户端请求超时,如果 Request 的请求体没有被 UnrepeatableRequestBody 标记,会继续发起新的请求
      • 308/307/303/302/301/300:需要进行重定向,发起新的请求
    7. 如果上一步判断不需要身份验证,重定向,没有超时,那就结束循环,返回response

    8. 检测重连次数是否超过20次,如果超过就抛出异常,不再进行连接检测重连次数是否超过20次,如果超过就抛出异常,不再进行连接

    9. 如果重连前的Request与重新构建的Request有不同的连接,那就释放之前的StreamAllocation,重新实例化;如果相同但是本次请求的流没有关闭,那就抛出异常

    10. 进入下次循环,重新连接

其中最重要的两步一定要注意:

  • 当抛出RouteException或者发生IOException时,拦截器会根据用户的设置和异常分析,决定当前请求是否可以重连
  • 当发送网络请求完成并获取到Response后,对响应码进行判断是否需要身份验证、重定向、是否超时,如果需要就构建新的Request重新发起请求

参考文章:
https://blog.csdn.net/firefile/article/details/75346937

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页