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

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

推荐阅读更多精彩内容