OkHttp源码之深度解析(四)——RetryAndFollowUpInterceptor详解:重试机制

OkHttp源码之深度解析(四)——RetryAndFollowUpInterceptor详解:重试机制

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情

前言

OkHttp源码系列文章:

RetryAndFollowUpInterceptor是OkHttp内置的重试和重定向拦截器,主要就是负责请求失败重试和网络重定向。在实际情况来说应用发起网络请求有可能因为各种原因导致请求失败,而RetryAndFollowUpInterceptor中有一个重试机制,如果请求失败时符合重试的条件,则会再次进行请求以增大网络请求的成功率。本文将详细分析RetryAndFollowUpInterceptor的源码,探究OkHttp重试机制的原理。

PS:本文基于OkHttp3版本4.9.3

intercept方法

RetryAndFollowUpInterceptor是拦截器责任链中的第一个拦截器(不考虑添加自定义的应用拦截器的情况),拦截器相关的源码在之前的文章中已经详细分析过了,作为一个拦截器RetryAndFollowUpInterceptor内部最重要的就是intercept方法,触发后续的拦截器、实现失败重试和重定向等等都是在intercept方法中做的,先来看看intercept方法的源码:

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    var request = chain.request
    val call = realChain.call
    var followUpCount = 0
    var priorResponse: Response? = null
    var newExchangeFinder = true
    var recoveredFailures = listOf<IOException>()
    while (true) {
      ......
    }
  }
复制代码

intercept方法里声明了一些成员变量,其中:

  • followUpCount:用于统计重定向的次数
  • priorResponse:用于记录上一次重试获得的Response
  • newExchangeFinder:判断是否请求成功的标志位
  • recoveredFailures:用于记录异常信息

Intercept方法里面除了几个成员变量就只剩下一个while循环了,可以推断出重试和重定向的逻辑就在循环体内,具体看看这个while循环是怎么写的:

    while (true) {
      call.enterNetworkInterceptorExchange(request, newExchangeFinder) //⑴

      var response: Response
      var closeActiveExchange = true //是否重置Exchange的标志位
      try {
        if (call.isCanceled()) {
          throw IOException("Canceled")
        }

        try {
          response = realChain.proceed(request) //⑵
          newExchangeFinder = true
        } catch (e: RouteException) {
          //⑶
          if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
            throw e.firstConnectException.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e.firstConnectException
          }
          newExchangeFinder = false
          continue
        } catch (e: IOException) {
          //⑷
          if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newExchangeFinder = false
          continue
        }

        //⑸
        if (priorResponse != null) {
          response = response.newBuilder()
              .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
              .build()
        }

        val exchange = call.interceptorScopedExchange
          
        //⑹
        val followUp = followUpRequest(response, exchange)
        if (followUp == null) {
          if (exchange != null && exchange.isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }

        //⑺
        val followUpBody = followUp.body
        if (followUpBody != null && followUpBody.isOneShot()) {
          closeActiveExchange = false
          return response
        }

        response.body?.closeQuietly()

        //⑻
        if (++followUpCount > MAX_FOLLOW_UPS) {
          throw ProtocolException("Too many follow-up requests: $followUpCount")
        }

        request = followUp
        priorResponse = response
      } finally {
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
复制代码

代码有点长,我们来逐步分析它的逻辑:

  • ⑴处如果newExchangeFinder为true,RealCall的enterNetworkInterceptorExchange方法内部会构建一个ExchangeFinder,这个ExchangeFinder是获取连接的工具,后面的连接拦截器ConnectInterceptor会用到;

  • 如果请求已经被取消了则抛出异常;

  • ⑵处调用RealInterceptorChain的proceed方法启动下一个拦截器,如果是后面的任意一个拦截器抛出了异常都会在这里捕获到,然后进入下面的catch判断是否尝试重连;

  • 当路由连接失败捕获到RouteException的时候就会进入⑶处,这时请求还没发出去,会进入recover方法中判断此异常能不能恢复,如果能恢复的话就会记录这次的异常信息,并进行重试;

  • 当与服务器通信失败捕获到IOException的时候就会进入⑷处,这时请求有可能已经发出去了,同样是跟⑶处一样会调用recover方法,返回true则记录异常并重试;

  • ⑸处尝试关联上一个Response,priorResponse只有在成功获取到后续拦截器返回的响应或者重试过才会有值,如果priorResponse不为空的话更新Response,并且这个Response的body会是空的;

  • 获取RealCall的interceptorScopedExchange,这个Exchange会在连接拦截器ConnectInterceptor进行拦截的时候创建,在ConnectInterceptor启动之前Exchange会是null;

  • ⑹处调用followUpRequest方法进行重定向并获取重定向返回的Request实例followUp,这个followUp将作为下一次重试的Request,而followUpRequest方法中会根据responseCode来判断是否需要进行重定向,如果返回的followUp不为空的话则说明进行了重定向处理,否则说明不需要重定向,结束本次请求直接返回Response,后文会对followUpRequest方法详细分析;

  • ⑺处如果重定向后的请求体不为空并且是只允许请求一次的话,则直接返回Response不再进行重试;

  • ⑻处检查重定向次数的合法性,支持的最大次数默认是20,注释中说明不同浏览器推荐的最大重试次数也不同,比如Chrome是21,Firefox是20,Safari是16,另外HTTP/1.0推荐是5次:

    /**
     * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
     * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
     */
    复制代码

    不过OkHttp将最大次数写死为20了,如果超出了限制的最大次数则会抛出异常。

  • 如果上述的步骤没有终止循环,则更新Request和priorResponse然后开始重试,进入下一轮的轮询进行请求,最后调用RealCall的exitNetworkInterceptorExchange方法,传入的closeActiveExchange当需要进行重定向的时候为true,这时exitNetworkInterceptorExchange方法会将RealCall的interceptorScopedExchange重新置空,也就是说当发生重定向的时候会重置Exchange为null,即便是在这之前网络拦截器已经跟服务器建立起了连接,每次重试前都会把先连接断开再重试。

分析完intercept方法可以知道,RetryAndFollowUpInterceptor是通过while(true)+try catch的方式去实现异常自动重连的,当遍历拦截器链时出现异常而且异常可恢复的时候,RetryAndFollowUpInterceptor则会循环调用拦截器链RealInterceptorChain的proceed方法,直到拿到Response或者发生超出重定向的最大限制等异常则终止循环结束请求工作。那到底什么异常是可恢复的?什么异常又是不允许重试的呢?具体就在recover方法里面。

recover方法:重试判定规则

老规矩先贴上recover方法的源码:

  private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    if (!client.retryOnConnectionFailure) return false

    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

    if (!isRecoverable(e, requestSendStarted)) return false

    if (!call.retryAfterFailure()) return false

    return true
  }
复制代码

根据这段代码我们可以知道,在以下几种情况下recover方法会返回false,即异常不可恢复不会进行重试:

  • 用户配置OkHttpClient的时候设置了retryOnConnectionFailure为false(默认配置是true),即用户手动关闭了重试机制,连接失败的时候不允许重试;

  • 请求已经发送出去了并且只允许发送一次;

  • isRecoverable方法判断为不可重试的异常类型,点进去isRecoverable方法里:

      private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
        if (e is ProtocolException) {
          return false
        }
    
        if (e is InterruptedIOException) {
          return e is SocketTimeoutException && !requestSendStarted
        }
    
        if (e is SSLHandshakeException) {
          if (e.cause is CertificateException) {
            return false
          }
        }
          
        if (e is SSLPeerUnverifiedException) {
          return false
        }
    
        return true
      }
    复制代码

    可以看到,isRecoverable里检测了以下的异常:

    1. ProtocolException:协议异常,通常是因为用户的请求或者服务器的响应没有按照HTTP协议来定义数据,如果是这种异常则判定为不能重试(数据格式不对再重试也没有用);
    2. InterruptedIOException:中断异常,如果是属于Socket连接超时而引起的中断并且此时请求还没发送出去,则判定为可以重试,尝试连接到其他路由,否则不能重试;
    3. SSLHandshakeException:SSL握手异常,如果是因为CertificateException引起的异常,即证书验证出问题,则判定为不能重试;
    4. SSLPeerUnverifiedException:SSL未验证异常,可能是因为没有SSL证书或者证书的数据不对,这种异常也判定为不能重试;

    如果属于上述不可恢复的异常,则isRecoverable返回false,不会进行重试。

  • 没有其他可重试的路由。DNS解析域名后可能返回多个IP,如果一个IP失败了可以尝试重定向到另一个IP,要是没有其他可用的IP进行连接,也就没有重试的必要了。

如果经过recover方法判定异常可以恢复允许重试的话,接下来就该followUpRequest方法出场,去判断是否需要进行重定向了。

followUpRequest方法:重定向判定规则

先贴上followUpRequest方法的源码:

  private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
    val route = exchange?.connection?.route()
    val responseCode = userResponse.code

    val method = userResponse.request.method
    when (responseCode) {
      HTTP_PROXY_AUTH -> {
        val selectedProxy = route!!.proxy
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
        }
        return client.proxyAuthenticator.authenticate(route, userResponse)
      }

      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)

      HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
        return buildRedirectRequest(userResponse, method)
      }

      HTTP_CLIENT_TIMEOUT -> {
        if (!client.retryOnConnectionFailure) {
          return null
        }

        val requestBody = userResponse.request.body
        if (requestBody != null && requestBody.isOneShot()) {
          return null
        }
        val priorResponse = userResponse.priorResponse
        if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
          return null
        }

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

        return userResponse.request
      }

      HTTP_UNAVAILABLE -> {
        val priorResponse = userResponse.priorResponse
        if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
          return null
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          return userResponse.request
        }

        return null
      }

      HTTP_MISDIRECTED_REQUEST -> {
        val requestBody = userResponse.request.body
        if (requestBody != null && requestBody.isOneShot()) {
          return null
        }

        if (exchange == null || !exchange.isCoalescedConnection) {
          return null
        }

        exchange.connection.noCoalescedConnections()
        return userResponse.request
      }

      else -> return null
    }
  }
复制代码

总的来说followUpRequest方法会根据响应码来判断是否需要重定向,如果需要重定向则会返回一个Request用于新一轮的请求,否则返回null,具体的逻辑如下所示:

响应码说明重定向判断
HTTP_PROXY_AUTH(407)代理认证如果路由代理不是HTTP类型则抛出异常,否则执行proxyAuthenticator.authenticate方法进行代理认证获取Request(默认是返回null,可以自定义Authenticator重写authenticate方法)
HTTP_UNAUTHORIZED(401)未授权执行authenticator.authenticate进行授权认证获取Request(默认是返回null,可以自定义Authenticator重写authenticate方法)
HTTP_PERM_REDIRECT(308)、
HTTP_TEMP_REDIRECT(307)、
HTTP_MULT_CHOICE(300)、
HTTP_MOVED_PERM(301)、
HTTP_MOVED_TEMP(302)、
HTTP_SEE_OTHER(303)
重定向响应进入buildRedirectRequest方法中处理:
1、如果用户配置OkHttpClient的时候设置了followRedirects为false,则不需要重定向,直接返回null;
2、如果配置了不允许在SSL和非SSL之间进行重定向,则直接返回null;
3、在请求方法不是GET也不是HEAD的前提下:
①响应码不是307、308的时候将除了PROPFIND请求之外的都改成GET请求,并把请求体置空;
②如果但凡满足响应码为307、308或请求方法为PROPFIND中的一个,则获取当前响应的requestBody作为重定向的请求体;
③如果响应码不是307、308并且请求方式不是PROPFIND,则移除请求头中的Transfer-Encoding、Content-Length和Content-Type;
4、如果是跨主机重定向,则移除请求头中的Authorization;
5、走完上述的逻辑之后构建Request并返回。
HTTP_CLIENT_TIMEOUT(408)请求超时1、如果用户配置OkHttpClient的时候设置了retryOnConnectionFailure为false,则不需要重定向,直接返回null;
2、如果当前Response的请求体不为空且只允许请求一次,则不需要重定向,直接返回null;
3、如果上一次请求的Response不为空而且也请求超时(响应码也是408),则放弃重试,直接返回null;
4、调用retryAfter方法对响应头进行解析,如果返回的Retry-After大于0则不需要重定向,直接返回null;
5、不属于上述情况则重定向,返回当前Response的Request实例。
HTTP_UNAVAILABLE(503)服务不可用1、如果上一次请求的Response不为空而且返回的响应码也是503,则放弃重试,直接返回null;
2、如果返回的Retry-After等于0则返回当前Response的Request实例;
3、不属于上述情况则直接返回null,不需要重定向。
HTTP_MISDIRECTED_REQUEST(421)请求有误1、如果当前Response的请求体不为空且只允许请求一次,则不需要重定向,直接返回null;
2、如果Exchange为null(即跟服务器的连接还没建立)或者连接池中的连接跟当前与服务器的连接已合并,则不需要重定向,直接返回null;
3、不属于上述情况则重定向,返回当前Response的Request实例。

上述响应码以外的情况都一律返回null,不需要重定向。

重试机制流程

最后用一个流程图来总结一下OkHttp的重试机制的大致流程:

flowchart TB
    interceptor(RetryAndFollowUpInterceptor)
    proceed(chain.proceed)
    isSuccess{成功?}
    exception(RouteException/IOException)
    
    subgraph 重试判定
    检测client是否允许失败重试-->
    检测请求是否已经发送出去了并且只允许发送一次-->
    检测异常类型是否可恢复-->
    检测是否存在其他可重试路由
    end
    
    isRetry{是否重试?}
    
    subgraph 重定向判定
    根据响应码判定是否重定向-->
    重定向次数检测
    end
    
    isRedirect{是否重定向?}
    checkFollowUpBody{重定向后的请求体不为空且只允许请求一次?}
    saveResult(记录本次重定向返回的Request和当前Response)
    finish(结束)
    
    interceptor-->proceed-->isSuccess-->|否|exception-->重试判定-->isRetry-->|是|重定向判定-->isRedirect-->|否|finish
    isSuccess-->|是|finish
    isRetry-->|否|finish
    checkFollowUpBody-->|是|finish
    isRedirect-->|是|checkFollowUpBody-->|否|saveResult-->|重试|proceed
分类:
Android
标签: