深入解读OKhttp五大拦截器之RetryAndFollowUpInterceptor

加油吧,打工人,不相信命运,只相信自己!

简介

Okhttp的封装了一些列请求所需要的参数,不管是同步请求还是异步请求最终都会经过五大拦截器的处理才能得到服务器返回的请求结果。本篇文章主要讲解Okhttp五大拦截器的重试重定向拦截器的作用。

RetryAndFollowUpInterceptor拦截器作为OKhttp的第一个默认拦截器,主要作用是当客户端网络请求失败时或目标响应的位置发生改变时调用。

一,网络失败重试

kotlin 复制代码
 val realChain = chain as RealInterceptorChain //请求链
    var request = chain.request //网络请求
    val call = realChain.call //call对象
    var followUpCount = 0 //重试次数初始为0
    var priorResponse: Response? = null //以前的返回值
    var newExchangeFinder = true
    var recoveredFailures = listOf<IOException>()
    //默认进入while死循环
    while (true) {
  
     //获取在链接池中网络链接
      call.enterNetworkInterceptorExchange(request, newExchangeFinder)
  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) { //IO异常
          // An attempt to communicate with a server failed. The request may have been sent.
            //如果是因为IO异常,那么requestSendStarted=true 
          if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newExchangeFinder = false
          continue
        }

        request = followUp
        priorResponse = response
      } finally {
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
 }
}

从源码中可以看出 执行RetryAndFollowUpInterceptor拦截器时,默认进入while死循环,表示网络请求失败了可以一直重试,直到realChain.proceed(request)返回服务端数据。

那什么时候中断重试呢,总不能一直进行网络重试吧,从try-catch中可以看到当发生RouterException和IOException时,通过recover函数判断来抛出异常来中断while循环。所以recover函数才是决定是否重试的关键。

下面分析recover函数是如何决定网络请求是否需要重试的。

kotlin 复制代码
  private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    // The application layer has forbidden retries.
    //1. okhttpclient配置不重试参数 
    if (!client.retryOnConnectionFailure) return false

    // We can't send the request body again.
    // 2. 不重试:
    // 条件1.如果是IO异常(非http2中断异常)表示请求可能发出
    // 条件2、如果请求体只能被使用一次(默认为false)
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

    // This exception is fatal.
    // 3.异常不重试:协议异常、IO中断异常(除Socket读写超时之外),ssl认证异常
    if (!isRecoverable(e, requestSendStarted)) return false

    // No more routes to attempt.
    //4. 是否有更多的请求路线
    if (!call.retryAfterFailure()) return false

    // For failure recovery, use the same route selector with a new connection.
    return true
  }
  1. client.retryOnConnectionFailure 为构建okhttpClient时的配置参数,默认为true。
  2. requestSendStarted 表示网络请求已经发送出去了。requestIsOneShot 用户请求是否只执行一次。
kotlin 复制代码
 private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
    val requestBody = userRequest.body
    // 1. 请求体不为null
    // 2.默认为false
     //3.文件不存在.比如上传文件时本地文件被删除了
    return (requestBody != null && requestBody.isOneShot()) ||
        e is FileNotFoundException
  }
  1. isRecoverable() 发生如下四种类型异常不会重试:协议异常;中断异常时请求已经发出时;证书异常;证书验证异常。
kotlin 复制代码
  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
  }
  1. retryAfterFailure 返回false时不重试。当没有更多的重试路线时,不能进行重试。

综述所示,网络请求停止重试的条件一共有四种。

  • 用户初始化Okhttp时的参数配置
  • 网络请求被中断发生中断异常且客户端的网络请求,请求连接超时且还没有发送出去。
  • 协议异常,数字证书SSL异常和验证失败异常
  • 没有多余的重试请求路线

二, 网络重定向

当网络请求成功后,得到服务端的返回值response,客户端还需要对response进行一些验证。

kotlin 复制代码
if (priorResponse != null) {
          //将上次的请求结果作为这边请求response的参数
          response = response.newBuilder()
              .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
              .build()
        }
     
        val exchange = call.interceptorScopedExchange
        //判断是否构建重定向请求
        val followUp = followUpRequest(response, exchange)
        //如果不需要重定向请求
        if (followUp == null) {
          //如果连接是全双工 websocket 则退出超时。
          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()
        //失败重定向的次数最大为20次
        if (++followUpCount > MAX_FOLLOW_UPS) {
          throw ProtocolException("Too many follow-up requests: $followUpCount")
        }

        request = followUp
        priorResponse = response
      } finally {
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }

当客户端拿到服务端的响应时,还需要进行响应的response进行验证来决定是否需要重定向。从源码中可以看出主要是通过followUpRequest()判断。

下面深入源码查看一些followUpRequest是如何判断的。

kotlin 复制代码
 private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
    //连接路由 
    val route = exchange?.connection?.route()
    //响应码
    val responseCode = userResponse.code
    //请求方法
    val method = userResponse.request.method
    when (responseCode) {
     // 407 代理身份验证
      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)
      }
      //401 身份认证
      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
      // 30... 临时重定向
      HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
        return buildRedirectRequest(userResponse, method)
      }
      // 408   客户端连接超时
      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
      }
      // 503 服务不可用
      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
      }
      //421 连接发生错误
      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
    }
  }

从源码分析中可以得知,当客户端发生连接异常,或服务端不可用时 时才会禁止客户端进行重定向。如果请求发生重定向,最大重定向的次数只有20次。

通过对RetryAndFollowUpInterceptor重试重定向拦截器的源码分析,可以更好的帮助理解网络连接请求的实现里,对日常工作中解决网络问题及优化提供帮助。

相关推荐
selt7916 小时前
Redisson之RedissonLock源码完全解析
android·java·javascript
Yao_YongChao7 小时前
Android MVI处理副作用(Side Effect)
android·mvi·mvi副作用
非凡ghost8 小时前
JRiver Media Center(媒体管理软件)
android·学习·智能手机·媒体·软件需求
席卷全城8 小时前
Android 推箱子实现(引流文章)
android
齊家治國平天下8 小时前
Android 14 系统中 Tombstone 深度分析与解决指南
android·crash·系统服务·tombstone·android 14
致Great8 小时前
Nano Banana提示语精选
人工智能·gpt·chatgpt·开源·agent
chxii8 小时前
开源项目 lottery_results
开源
maycho12310 小时前
MATLAB环境下基于双向长短时记忆网络的时间序列预测探索
android
十六年开源服务商10 小时前
WordPress站内SEO优化最佳实践指南
大数据·开源
思成不止于此10 小时前
【MySQL 零基础入门】MySQL 函数精讲(二):日期函数与流程控制函数篇
android·数据库·笔记·sql·学习·mysql