探索OkHttp系列 (二) 错误重试与重定向

前言

在上一篇文章「探索OkHttp系列 (一) 请求的发起与响应」,我们介绍了请求的发起与响应的整个过程,在介绍请求响应的时候,最关键的点是拦截器机制责任链模式,关于责任链模式在请求的响应中是如何运用的,我们已经在上篇文章讲述了,但是上篇文章没有去详细地介绍各个拦截器的作用,从这篇文章开始,我们就一一地介绍各个拦截器。

拦截器的工作流程如下:

image-20211121121406472

如果用户没有加入自定义的拦截器,那么 RetryAndFollowUpInterceptor 就是我们的责任链中最先被调用的拦截器,我们这篇文章就从RetryAndFollowUpInterceptor拦截器开始介绍,该拦截器作用是进行错误重试重定向

Http协议中的重定向

我们先了解下Http协议中的重定向知识。

Http协议中,当客户端请求的资源的位置在服务器中被转移,服务器就会发送一个响应报文,该响应报文的Response Code3XX,表示客户端需要「重定向」请求,并且服务端会在该响应报文的Response HeaderLocation字段中放入新的URL,这样客户端就可以向该 Location 字段所指定的 URL 重新请求从而得到需要的数据。

该过程如下:

image-20211120134352948

关于重定向状态码3XX的详细资料 可以查看:HTTP状态码 - 维基百科.

重定向分类:

  • 永久重定向:表示重定向操作是永久的,原URL应不再被使用,优先选用新的URL
  • 临时重定向:有时候请求的资源无法从其标准地址访问,但是却可以从另外的地方访问。在这种情况下可以使用临时重定向。搜索引擎不会记录该新的、临时的链接。
  • 特殊重定向(状态码304,其实与重定向无关):判断资源缓存是否有效等。

重定向带来的相关问题:

  1. 性能耗损:多了一次请求与应答
  2. 循环跳转:可能出现重定向循环跳转

RetryAndFollowUpInterceptor

intercept

我们从RetryAndFollowUpInterceptorintercept方法开始,来探究RetryAndFollowUpInterceptor的工作机制。

RetryAndFollowUpInterceptor::intercept

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    // 获取请求Request
    var request = chain.request
    val call = realChain.call
    // 重定向的次数
    var followUpCount = 0
    // 上一次请求返回的Response  
    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  
          response = realChain.proceed(request)
          newExchangeFinder = true
        } catch (e: RouteException) {
          // The attempt to connect via a route failed. The request will not have been sent.
          // 路由连接失败  
          // 检测是否可以从失败中恢复,如果可以恢复就返回true,就会在下面continue,进行重试;
          // 不可以恢复就抛出异常             
          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.
          // IO过程中,抛出异常
          // 检测是否可以从失败中恢复,如果可以恢复就返回true,就会在下面continue,进行重试;
          // 不可以恢复就抛出异常          
          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.
        // 如果上一次请求priorResponse存在,就在本次Response中设置上一次的priorResponse,
        // 设置的priorResponse的body为空 
        if (priorResponse != null) {
          response = response.newBuilder()
              .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
              .build()
        }

        val exchange = call.interceptorScopedExchange
        // 根据Response Code,尝试获取「重试」或「重定向」对应的Request  
        val followUp = followUpRequest(response, exchange)
		// 获取不到Request,则停止计时,并且返回Response
        if (followUp == null) {
          if (exchange != null && exchange.isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }

        val followUpBody = followUp.body
        // followUpBody.isOneShot():该方法默认返回false,如果用户希望Request的Body最多传输
        // 一次,那么该方法就需要返回true。
        // 这里的意思是:如果用户只希望Request的Body最多传输一次,那么这里就不再进行请求,而是直接
        // 返回Response。
        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
        // 记录本次请求拿到的Response  
        priorResponse = response
      } finally {
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
  }

接着我们看看followUpRequest方法,是如何 尝试获取「重试」或「重定向」对应的Request

followUpRequest

RetryAndFollowUpInterceptor::followUpRequest

  @Throws(IOException::class)
  private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
    val route = exchange?.connection?.route()
    // 获取响应码  
    val responseCode = userResponse.code
	// 获取请求方法
    val method = userResponse.request.method
    // 根据不同的响应码,做不同的处理  
    when (responseCode) {
      // Response Code : 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)
      }

      // Response Code : 401 
      // 身份认证  
      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)

      // Response Code:308,307,300,301,302,303  
      // 重定向  
      HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
        return buildRedirectRequest(userResponse, method)
      }

      // Response Code : 408 
      // 请求超时  
      HTTP_CLIENT_TIMEOUT -> {
        // 408的响应码在实践中很少见,出现这个响应码,我们可以向服务器重复发送这个请求(无需对该
        // 请求做任何的修改)  
          
        // 若客户端不希望在「连接失败」后重试,那么就返回null  
        if (!client.retryOnConnectionFailure) {
          return null
        }

        val requestBody = userResponse.request.body
        // 如果请求带有body,并且该body仅允许发送一次,那么就返回null  
        if (requestBody != null && requestBody.isOneShot()) {
          return null
        }
          
        val priorResponse = userResponse.priorResponse
        // 如果存在priorResponse,并且priorResponse的响应码也是HTTP_CLIENT_TIMEOUT,
        // 那么也返回null,不再进行重试
        if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null
        }

        // 如果Response当中有 Retry-After 这个Header,那么就返回null;
        // 如果Response当中没有 Retry-After 这个Header,就不进入If语句。 
        if (retryAfter(userResponse, 0) > 0) {
          return null
        }

        return userResponse.request
      }

      // Response Code : 503  
      // 表示服务器暂时处于超负载或维护状态,可能会包含 RetryAfter 这个Header来告诉客户端
      // 何时能够进行访问  
      HTTP_UNAVAILABLE -> {
        ...  
        if (...) {
          return userResponse.request
        }

        return null
      }

      // Response Code : 421  
      HTTP_MISDIRECTED_REQUEST -> {
        if (...) {
          return null
        }
		...
        return userResponse.request
      }

      else -> return null
    }
  }

在该方法中,根据不同的响应码可创建不同的请求,其中「重定向」对应的Request的创建,是调用了buildRedirectRequest方法,我们查看该方法。

buildRedirectRequest

RetryAndFollowUpInterceptor::buildRedirectRequest

  private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
    // 客户端不支持重定向,返回null
    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.
    val sameScheme = url.scheme == userResponse.request.url.scheme
    if (!sameScheme && !client.followSslRedirects) return null

    // 大多数重定向不包含Request Body  
    val requestBuilder = userResponse.request.newBuilder()
    if (HttpMethod.permitsRequestBody(method)) {
      val responseCode = userResponse.code
      val maintainBody = HttpMethod.redirectsWithBody(method) ||
          responseCode == HTTP_PERM_REDIRECT ||
          responseCode == HTTP_TEMP_REDIRECT
      if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
        requestBuilder.method("GET", null)
      } else {
        val requestBody = if (maintainBody) userResponse.request.body else 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 (!userResponse.request.url.canReuseConnectionFor(url)) {
      requestBuilder.removeHeader("Authorization")
    }

    // 返回重定向请求  
    return requestBuilder.url(url).build()
  }

小结

下面对RetryAndFollowUpInterceptor的内部实现进行总结

  1. 调用拦截器链的proceed方法,获取下一个拦截器返回的Response

  2. 若下面的拦截器在处理请求的过程中抛出「路由连接失败」、「服务器通信失败」的异常,就检测是否可以从失败中恢复,如果可以,就进行请求重试,如果不可以,就抛出异常

  3. 在本次的Response中设置上一次请求的Response,且上一次请求的Responsebody为空

  4. 根据本次响应的Response Code,查看是否有需要「重试」或「重定向」的Request

  5. 若第4步的Request不存在,那么就停止计时,并且返回Response;若第4步的Request存在,并且带有Request Body,用户还要求该Request Body仅能传输一次,那么也是直接返回Response;否则往下走

  6. 判断「重试」、「重定向」的次数是否超过最大值,若超过最大值,那么就抛出异常,否则往下走

  7. 记录「重试」、「重定向」对应的Request,以及本次请求返回的Response

  8. 进行下一次请求

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值