OkHttp的源码分析(二)——重试、重定向拦截器分析

本文详细解读了OkHttp网络请求中的重连和重定向拦截器,涉及异常处理、策略判断与限制,以及如何根据HTTP状态码决定是否重定向。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        上一篇文章中,我们了解学习了OkHttp的网络请求的主流程,本文主要分析OkHttp的不同拦截器的源码以及涉及到的功能作用。

目录

一、整体代码流程

二、重连

三、重定向

四、总结


一、整体代码流程

直接看RetryAndFollowUpInterceptor的核心方法

@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) {
      call.enterNetworkInterceptorExchange(request, newExchangeFinder)

      var response: Response
      var closeActiveExchange = true
      try {
        if (call.isCanceled()) {
          throw IOException("Canceled")
        }

        try {
          response = realChain.proceed(request)
          newExchangeFinder = true
        } catch (e: RouteException) {
          // The attempt to connect via a route failed. The request will not have been sent.
          if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
            throw e.firstConnectException.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e.firstConnectException
          }
          newExchangeFinder = false
          continue
        } catch (e: IOException) {
          // An attempt to communicate with a server failed. The request may have been sent.
          if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newExchangeFinder = false
          continue
        }

        // Attach the prior response if it exists. Such responses never have a body.
        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)
      }
    }
  }

整体流程:

1)创建一个死循环,如果直接得到服务器的返回,就不做重试流程,去看是否需要重定向;需要重定向的话,通过followUpRequest()获得一个新的Request,获得Response

2)如果没有得到服务器返回,通过异常捕获,去尝试重连,2次,3次,不断continue

注意,在最后有这样一行代码,这里是重定向的限制,最大值默认是20次,超过会抛出异常

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

另外,在流程开始,enterNetworkInterceptorExchange()初始化了ExchangeFinder对象,ExchangeFinder在ConnectInterceptor中会被用到,用来获取新的连接RealInterceptorChain,不是用之前重试失败留下的连接

二、重连

看重试的核心方法recover():

private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure) return false

    // We can't send the request body again.
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false

    // No more routes to attempt.
    if (!call.retryAfterFailure()) return false

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

通过上面的intercept方法我们知道,recover()方法如果返回false,会抛出异常,否则,进入continue流程,继续尝试重连。

retryOnConnectionFailure 如果是false,直接返回false,那样就不会重连了,retryOnConnectionFailure 是OkHttpclient的属性变量,可以在初始化的时候设置,不去做重连

private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
    val requestBody = userRequest.body
    return (requestBody != null && requestBody.isOneShot()) ||
        e is FileNotFoundException
}

这里的关键是requsetIsOneShot方法,如果request.isOneShot方法返回true,或者异常是文件没有找到,这个时候就不会重拾了,isOneShot是RequestBody的方法,默认返回false,如果不需要重试,自定义一个RequestBody方法,重写这个isOneShot方法返回true即可

继续向下看该方法

  private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
    // If there was a protocol problem, don't recover.
    if (e is ProtocolException) {
      return false
    }

    // If there was an interruption don't recover, but if there was a timeout connecting to a route
    // we should try the next route (if there is one).
    if (e is InterruptedIOException) {
      return e is SocketTimeoutException && !requestSendStarted
    }

    // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
    // again with a different route.
    if (e is SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e.cause is CertificateException) {
        return false
      }
    }
    if (e is SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      return false
    }
    // An example of one we might want to retry with a different route is a problem connecting to a
    // proxy and would manifest as a standard IOException. Unless it is one we know we should not
    // retry, we return true and try a new route.
    return true
}

可以看到,这里就是判断异常的类型,有些异常是不做重试的,看下具体哪些异常

  • ProtocolException 协议异常不做重连
  • InterruptedIOException IO中断异常,其中除了连接超时异常,其他不做重连
  • SSLHandshakeException SSL握手异常,如果是鉴权失败,不做重连
  • SSLPeerUnverifiedException 证书过期或失效,不做重连
all.retryAfterFailure()

这里主要是判断有没有可用的线路来尝试重连,没有的话返回false,如果有的话,就尝试换线路重连

主流程中的另外一个catch中的recover方法流程和上面的是一个方法,不做分析了,接下来看重试部分

三、重定向

  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 -> {
        // 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
        }

        val requestBody = userResponse.request.body
        if (requestBody != null && requestBody.isOneShot()) {
          return null
        }
        val priorResponse = userResponse.priorResponse
        if (priorResponse != null && 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
      }

      HTTP_UNAVAILABLE -> {
        val priorResponse = userResponse.priorResponse
        if (priorResponse != null && 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
      }

      HTTP_MISDIRECTED_REQUEST -> {
        // OkHttp can coalesce HTTP/2 connections even if the domain names are different. See
        // RealConnection.isEligible(). If we attempted this and the server returned HTTP 421, then
        // we can retry on a different connection.
        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
    }
}

如果不允许重定向,直接返回null,看followUpRequest如何获取新的request,可以看到里面有很多responseCode,我们逐个看下里面的内容

  • HTTP_PROXY_AUTH = 407

        客户端使用了Http代理服务器,在请求头中加上Proxy-Authorization,让代理服务器进行重定向

  • HTTP_UNAUTHORIZED = 401

        身份认证,需要在请求头中加Authorization

  • HTTP_PERM_REDIRECT = 308,  HTTP_TEMP_REDIRECT = 307, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER

  private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
    // Does the client allow redirects?
    if (!client.followRedirects) return null

    // 如果请求头中没有Location , 不去重定向
    val location = userResponse.header("Location") ?: return null
    // Don't follow redirects to unsupported protocols.
    val url = userResponse.request.url.resolve(location) ?: return null

    // If configured, don't follow redirects between SSL and non-SSL.
    //这里英文已经注释了,其实就是Scheme就是http或https,如果涉及Scheme的切换,要看是否配置允                
    //许,默认允许
    val sameScheme = url.scheme == userResponse.request.url.scheme
    if (!sameScheme && !client.followSslRedirects) return null

    // Most redirects don't include a request body.
    val requestBuilder = userResponse.request.newBuilder()
    //判断方法是不是(method == "GET" || method == "HEAD")
    if (HttpMethod.permitsRequestBody(method)) {
      val responseCode = userResponse.code
      val maintainBody = HttpMethod.redirectsWithBody(method) ||
          responseCode == HTTP_PERM_REDIRECT ||
          responseCode == HTTP_TEMP_REDIRECT
      //判断方法是不是PROPFIND,不是PROPFIND方法请求,请求方法都要改成是get,并且不能有请求体
      if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
        requestBuilder.method("GET", null)
      } else {
        //PROPFIND方法请求,添加请求体
        val requestBody = if (maintainBody) userResponse.request.body else null
        requestBuilder.method(method, requestBody)
      }
      // 不是PROPFIND 的请求,请求头中关于请求体的数据删掉
      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 (!userResponse.request.url.canReuseConnectionFor(url)) {
      requestBuilder.removeHeader("Authorization")
    }

    return requestBuilder.url(url).build()
}

上面buildRedirectRequest()的主要部分已经注释,代码流程比较简单

HTTP_CLIENT_TIMEOUT = 408

        客户端请求超时,前面两行代码在重试逻辑中分析过了,直接在代码上注释

val priorResponse = userResponse.priorResponse
// 上次响应码如果已经是超时,直接返回,不需要走重定向
if (priorResponse != null && 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
}
  • HTTP_UNAVAILABLE = 503

        服务不可用,和408类似,服务器如果告诉了Retry-After,则立即返回Request

  • HTTP_MISDIRECTED_REQUEST = 421

// OkHttp can coalesce HTTP/2 connections even if the domain names are different. See
// RealConnection.isEligible(). If we attempted this and the server returned HTTP 421, then
// we can retry on a different connection.

直接看代码注释,大意就是:“即使域名不同,OkHttp 也可以合并 HTTP/2 连接。请参阅 RealConnection.isEligible()。 如果我们尝试这样做并且服务器返回 HTTP 421,那么我们可以在不同的连接上重试”。总结成一句话,421响应码可以在一个不同的连接上重试请求。

四、总结

  1. 重连主要是在catch异常中完成,异常是RouteExceptionIOException
  2. 重连涉及判断协议异常、IO中断异常,SSL握手异常,证书过期或失效,重试判断的是HTTP响应码,比如代理服务器、请求超时、身份认证失效等,其中307和308请求头不是GET或者HEAD不做重定向
  3. 重定向的最大次数默认是20次

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ezview_uniview

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值