二、OkHttp_重试和重定向和Bridge拦截器

拦截器

1、RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor会在失败时进行恢复,并根据需要跟随重定向。如果请求被取消,它可能会抛出 IOException 异常。

通过类的介绍得知其作用:

  • 处理网络请求的重试:当网络请求失败时,RetryAndFollowUpInterceptor 可以根据策略进行自动重试。它会检测请求是否可以安全地重试,并在必要时重新发送请求。

  • 跟进重定向:当服务器返回重定向响应时,RetryAndFollowUpInterceptor 可以自动跟随重定向,并发送新的请求到重定向的目标位置。它负责处理重定向响应并更新请求的目标 URL。

  • 处理请求的取消:如果网络请求被取消,RetryAndFollowUpInterceptor 可能会抛出 IOException 异常,表示请求已被中止。它会在适当的时候检查请求的取消状态,并中断请求的执行。

  • 实现失败恢复机制:RetryAndFollowUpInterceptor 在网络请求过程中,会根据不同的失败情况尝试进行恢复。它可以处理连接失败、通信问题、超时等情况,并根据策略尝试重新发送请求以实现恢复。

class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {

  @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>()
    // 死循环,不断请求接口,除非触发return操作
    while (true) {
        // 每次循环,都会为RealCall的成员变量exchangeFinder 设置新的ExchangeFinder对象
        // 如果发生请求过程中异常,则不设置新的exchangeFinder
        //  this.exchangeFinder = ExchangeFinder(...)
      call.enterNetworkInterceptorExchange(request, newExchangeFinder)

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

        try {
            // 通过责任链模式,请求一次接口
          response = realChain.proceed(request)
          // 为重定向请求的RealCall,设置新的exchangeFinder
          newExchangeFinder = true
        } catch (e: RouteException) {
          // 尝试通过链接 route失败,Request未被发送出去
          // RouteException:表明通过单个路由连接存在问题。可能用其他协议进行了多次尝试,但都没有成功。
          // recover() :表示是否进行再次请求:
          // 1、应用层 拒绝重试 ,返回 false : if (!client.retryOnConnectionFailure) return false
          // 2、无法再次发送请求体。返回 false : if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
          // 3、致命的异常。返回 false : InterruptedIOException、SSLHandshakeException、SSLPeerUnverifiedException
          // 4、没有更多的 routers 可以尝试 : if (!call.retryAfterFailure()) return false
          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
        }
        // ---------------------↑↑↑↑↑↑以上就是请求错误的重试逻辑↑↑↑↑↑↑↑↑--------------
        // 如果已经存在重定向响应,则 该 response不用应该有 body
        if (priorResponse != null) {
          response = response.newBuilder()
              .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
              .build()
        }
        
        val exchange = call.interceptorScopedExchange
        // 重定向的核心逻辑方法,通过之前请求的response 获得 新的重定向Request
        // 1、通过 response.code 判断是否符合重定向(30X,)、Unauthorized(401)...
        // 2、例如30X的重定向,则会获取 响应头中的 "Local"字段获取新的URL
        val followUp = followUpRequest(response, exchange)
        // 不存在重定向请求,直接返回 response
        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()
        // 累计重定向次数 大于20(默认)次 抛出异常
        if (++followUpCount > MAX_FOLLOW_UPS) {
          throw ProtocolException("Too many follow-up requests: $followUpCount")
        }
        // 重置 request 和 response
        request = followUp
        priorResponse = response
        // 继续下一次新的重定向请求
      } finally {
          // 退出网络拦截器 交换流
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
  }

总结:请求失败后,会重试3次请求? 从源码中怎么体现出来的呢??? 啊 啊 啊 。。。。

2、BridgeInterceptor

BridgeInterceptor主要是将应用程序代码转化为网络代码的桥梁。

  • 首先它从用户请求构建网络请求,
  • 然后继续调用网络,
  • 最后从网络响应构建用户响应。
class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val userRequest = chain.request()
    val requestBuilder = userRequest.newBuilder()

    val body = userRequest.body
    // 对设置body的请求进行处理。Post、PUT、PATCH等
    if (body != null) {
      val contentType = body.contentType()
      if (contentType != null) {
          // Content-Type 用于指示请求或响应的实体的媒体类型,描述了实体是什么类型的文档,以及如何解析它
          // 可以指定实体是HTML文档、JSON数据、XML文档、图像文件等。
          // 该头部的值通常由媒体类型(例如 text/html,application/json等)和字符集(例如 utf-8)组成。
        requestBuilder.header("Content-Type", contentType.toString())
      }
        // 请求头 Content-Length 表示请求体的字节数。
        // 它是一个十进制数,通常用于 POST 和 PUT 请求,用于指定请求体的大小。
      val contentLength = body.contentLength()
      if (contentLength != -1L) {
        requestBuilder.header("Content-Length", contentLength.toString())
        // Transfer-Encoding 用于指定消息主体的传输编码方式。
        // 它通常用于支持以流的形式传输响应主体,而无需预先知道其总大小。
        requestBuilder.removeHeader("Transfer-Encoding")
      } else {
          // 常见的传输编码方式包括 chunked 和 compress。
          // 其中 chunked 表示在传输响应主体时会将其分成多个块,每个块的大小在发送前不需要知道,
          // 以此来支持传输较大的响应。
        requestBuilder.header("Transfer-Encoding", "chunked")
        requestBuilder.removeHeader("Content-Length")
      }
    }
     
    if (userRequest.header("Host") == null) {
        // 请求头 Host 表示 HTTP 请求的目标服务器的主机名和端口号。这个请求头在 HTTP/1.1 协议中是必需的,
        // 并且必须包含在所有请求中。它的作用是告诉服务器请求的目标主机地址和端口号。
      requestBuilder.header("Host", userRequest.url.toHostHeader())
    }

    if (userRequest.header("Connection") == null) {
        // 请求头 Connection 是用来指定与服务器建立连接的选项,主要包括以下几个常用选项:
        // -keep-alive:表示请求完成后,仍然保持连接状态,以便下次请求可以复用这个连接。
        // -close:表示请求完成后立即关闭连接。
        // 对于HTTP/1.1 协议,默认的 Connection 值是 keep-alive,表示在完成请求后仍然保持连接状态。
        // 而对于HTTP/1.0 协议,默认的 Connection 值是 close,表示完成请求后关闭连接。
      requestBuilder.header("Connection", "Keep-Alive")
    }

    // 当我们在请求头中添加 "Accept-Encoding: gzip" 字段时,我们也需要负责解压缩传输流。
    // 这是因为 "gzip" 是一种常用的压缩算法,
    // 如果服务端响应的内容使用了 gzip 压缩,那么就需要客户端进行解压缩才能正确地处理数据。
    // 在请求头中指定了支持 gzip 压缩,服务器就有可能使用 gzip 进行压缩,客户端也需要相应地对压缩过的数据进行解压缩。
    var transparentGzip = false
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      // 服务器可以根据 Accept-Encoding 的值,将响应数据进行相应的压缩后再返回给客户端,以减少网络传输流量,提高传输效率。
      // 常见的编码方式有 gzip、deflate 等
      transparentGzip = true
      requestBuilder.header("Accept-Encoding", "gzip")
    }
    // 通过URL获取cookie
    // Cookie是一种用于存储会话信息或用户状态的机制。当客户端发送请求时,
    // 如果请求头中包含了Cookie信息,那么服务端就能通过这些信息识别出请求来自哪个用户,并且可以根据这些信息返回对应的响应。 
    val cookies = cookieJar.loadForRequest(userRequest.url)
    if (cookies.isNotEmpty()) {
        // 在请求头里面添加cookie
        // 简单的样例:Cookie: session_id=1234567890abcdef; user_id=123
      requestBuilder.header("Cookie", cookieHeader(cookies))
    }
    // 用于标识客户端发送请求时所使用的浏览器或客户端类型、操作系统、软件版本等信息。
    // 服务器可以根据 User-Agent 的值来判断客户端类型,从而进行相应的处理和响应。
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", userAgent)
    }
     // 获取到请求 响应response
    val networkResponse = chain.proceed(requestBuilder.build())
    // 解析cookie并保存 
    cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)
    // ??? 为什么重新构建一个ResponseBuild
    val responseBuilder = networkResponse.newBuilder()
        .request(userRequest)
    // 解压缩传输流
    if (transparentGzip &&
        "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
        // 如果响应头和状态 表示 该响应具有(可能是0长度的)正文,则返回true。
        networkResponse.promisesBody()) {
      val responseBody = networkResponse.body
      if (responseBody != null) {
        val gzipSource = GzipSource(responseBody.source())
        // 去除 Content-Encoding 、Content-Length
        val strippedHeaders = networkResponse.headers.newBuilder()
            .removeAll("Content-Encoding")
            .removeAll("Content-Length")
            .build()
        responseBuilder.headers(strippedHeaders)
        val contentType = networkResponse.header("Content-Type")
        responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
      }
    }
    // 将重建的response 创建
    return responseBuilder.build()
  }

小结:Bridge拦截器就是将用户的请求转换包装转化成网络请求,主要对请求头、请求体、请求方法的解析转换,默认添加了一些请求头!

其次对response根据Header的描述进行解析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,针对你的问题,我可以给出以下的基于 OkHttp重试拦截器实现: ```java public class RetryInterceptor implements Interceptor { private final int maxRetryCount; private final long retryInterval; public RetryInterceptor(int maxRetryCount, long retryInterval) { this.maxRetryCount = maxRetryCount; this.retryInterval = retryInterval; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = null; IOException exception = null; int tryCount = 0; while (tryCount <= maxRetryCount) { try { response = chain.proceed(request); if (response.isSuccessful()) { return response; } } catch (IOException e) { exception = e; } tryCount++; if (tryCount <= maxRetryCount) { try { Thread.sleep(retryInterval); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } } } if (exception != null) { throw exception; } throw new IOException("Request failed after " + maxRetryCount + " attempts."); } } ``` 在这个拦截器中,我们可以通过传入最大重试次数和重试间隔时间来配置拦截器。当请求失败时,会在一定时间后重新尝试请求,直到达到最大重试次数或请求成功为止。如果最大重试次数用完后仍然失败,则抛出异常。 使用这个拦截器需要在 OkHttpClient 中添加: ```java OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new RetryInterceptor(3, 1000)) .build(); ``` 这样就可以在 OkHttp 的请求中添加重试功能了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值