美文网首页
OkHttp源码阅读(四) —— 拦截器之RetryAndFol

OkHttp源码阅读(四) —— 拦截器之RetryAndFol

作者: Sherlock丶Aza | 来源:发表于2021-04-11 08:21 被阅读0次

  上一篇我们了解了OkHttp的拦截器链是如何形成连式结构,并且如何工作的,接下来开始逐个的分析学习OkHttp内置的几个拦截器。首先第一个拦截器:重试和重定向拦截器RetryAndFollowUpInterceptor

简介

  在拦截器链的链式结构中,除了用户自定义的拦截器,位于最上层的拦截器就是该拦截器了

主要功能

连接失败重试(Retry)

  在发生 RouteException 或者 IOException 后,会捕获建联或者读取的一些异常,根据一定的策略判断是否是可恢复的,如果可恢复会重新创建 StreamAllocation 开始新的一轮请求

继续发起请求(FollowUp)

主要有这几种类型

  1. 3xx 重定向
  2. 401,407 未授权,调用 Authenticator 进行授权后继续发起新的请求
  3. 408 客户端请求超时,如果 Request 的请求体没有被 UnrepeatableRequestBody 标记,会继续发起新的请求

代码实现

  拦截器的工作核心主要是intercpt方法,所以我们直接看intercpt方法

intercpt方法

直接上代码,里边有详细的注释

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    /**初始化StreamAllcation类,这个类十分重要, 用来管理connections、stream、routerSelector的类
     * 在RetryAndFollowUpInterceptor中并没有用到初始化StreamAllcation类的核心功能,只是初始化和释放资源
     * 而已,它的作用会在ConnectInterceptor拦截器中体现,当前只需要知道它是用来建立连接的就可以了
     */
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;
    /**计数器,记录重连次数*/
    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      /**判断当前请求是否已经取消,是:释放链接资源**/
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response;
      /**是否要释放链接,这个标记位后边会频繁使用*/
      boolean releaseConnection = true;
      try {
        /**将创建链接和请求最为参数调用下一组拦截器链,最终的得到想要的response*/
        response = realChain.proceed(request, streamAllocation, null, null);
        /**请求交给拦截器链后,就不需要释放链接了**/
        releaseConnection = false;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        /**路由异常RouteException*/
        /**检测路由异常是否能重新连接*/
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          /**不可以重新链接,直接抛出异常*/
          throw e.getLastConnectException();
        }
        /**如果可以重新链接,就不需要释放链接了*/
        releaseConnection = false;
        /**如果可以重新链接,跳出本次循环,继续网络请求*/
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        /**IO异常*/
        /**检测IO异常是否能进行重新链接,不可以的话直接抛出该异常*/
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        /**可以重新连接的话 不必释放资源*/
        releaseConnection = false;
        /**如果可以重新链接,跳出本次循环,继续网络请求*/
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        /**最后释放所有资源*/
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

      // Attach the prior response if it exists. Such responses never have a body.
      /**判断priorResponse是否为空,priorResponse的赋值在方法最后边*/
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }
      /**响应码判断,代码执行到这里说明请求已经成功了,但是服务器返回的响应码不一定是200
       * 这里还需要对该请求进行检测,主要是通过响应码进行判断
       */
      Request followUp = followUpRequest(response, streamAllocation.route());

      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);
      }

      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }
      /** 如果重试的host等发生改变,如代理,重定向等情况,重新实例化StreamAllocation*/
      if (!sameConnection(response, followUp.url())) {
        streamAllocation.release();
        streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(followUp.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
      } else if (streamAllocation.codec() != null) {
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }

      request = followUp;
      priorResponse = response;
    }
  }

该方法里边有个重要的角色初始化工作StreamAllcation,这个类重要性主要体现在ConnectInterceptor,后面会详细介绍,右上边代码可以看出,重新建立链接的逻辑判断大部分都是在异常处理当中,IOExceptionRouteException,IOException是编译时异常,需要在编译时期就要捕获或者抛出。RouteException是运行时异常,不需要显式的去驳货或者抛出。无论是RouteException还是IOException,再处理是都调用了recover方法进行判断

RouteException异常的重连机制

/**
   * Report and attempt to recover from a failure to communicate with a server. Returns true if
   * {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only
   * be recovered if the body is buffered or if the failure occurred before the request has been
   * sent.
   */

  /**
   * 不在继续连接的情况:
   * 1. 应用层配置不在连接,默认为true
   * 2. 请求Request出错不能继续使用
   * 3. 是否可以恢复的
   *   3.1、协议错误(ProtocolException)
       3.2、中断异常(InterruptedIOException)
       3.3、SSL握手错误(SSLHandshakeException && CertificateException)
       3.4、certificate pinning错误(SSLPeerUnverifiedException)
   * 4. 没用更多线路可供选择
   */
  private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);
    // 1. 应用层配置不在连接,默认为true
    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure()) return false;
    // 2. 请求Request出错不能继续使用
    // We can't send the request body again.
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
    // 3.是否可以恢复的,在isRecoverable()方法中会判断注释3.1~3.4的异常
    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false;
    // 4. 没用更多线路可供选择
    // No more routes to attempt.
    if (!streamAllocation.hasMoreRoutes()) return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }

通过 recover 方法检测该 RouteException 是否能重新连接; 可以重新连接,那么就不要释放连接 releaseConnection = false; continue进入下一次循环,进行网络请求; 不可以重新连接就直接走 finally 代码块释放连接。

IOException异常重连机制

IOException 异常的检测实际上和 RouteException 是一样的,只是传入 recover 方法的第二个参数为 false 而已,表示该异常不是 RouteException ,这里就不分析了。

followUpRequest

/**
   * Figures out the HTTP request to make in response to receiving {@code userResponse}. This will
   * either add authentication headers, follow redirects or handle a client request timeout. If a
   * follow-up is either unnecessary or not applicable, this returns null.
   */
  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) {
      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:
        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
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      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);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        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();
        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");
          }
        }

        // When redirecting across hosts, drop all authentication headers. This
        // is potentially annoying to the application layer since they have no
        // way to retain them.
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

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

      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()) {
          // The application layer has directed us not to retry the request.
          return null;
        }

        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
          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();

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

followUpRequest主要是对响应码进行了检测判断,里边各个响应码对应的数值和意义在HttpURLConnection类中都有定义和说明,在这就不赘述了。

重试次数的判断

  在 RetryAndFollowUpInterceptor 内部有一个 MAX_FOLLOW_UPS 常量,它表示该请求可以重试多少次,在 OKHTTP 内部中是不能超过 20 次,如果超过 20 次,那么就不会再请求了。

private static final int MAX_FOLLOW_UPS = 20; 
if (++followUpCount > MAX_FOLLOW_UPS) { 
streamAllocation.release(); 
throw new ProtocolException("Too many follow-up requests: " + followUpCount); 
} 

总结

  RetryAndFollowUpInterceptor拦截器是最上层的内置拦截器,它的核心功能就是初始化StreamAllocation和判断重试机制的判断。大致业务流程如下:

  1. 初始化StreamAllocation
  2. 开启循环,执行下一个调用链(拦截器),等待返回结果(Response)
  3. 如果发生错误,判断是否继续请求,否:退出(抛出异常)
  4. 检查响应是否符合要求,是:返回
  5. 关闭响应结果
  6. 判断是否达到最大限制数,是:退出
  7. 检查是否有相同连接,是:释放,重建连接
  8. 重复以上流程

相关文章

网友评论

      本文标题:OkHttp源码阅读(四) —— 拦截器之RetryAndFol

      本文链接:https://www.haomeiwen.com/subject/utzykltx.html