OkHttp 系列四:重试重定向拦截器

762 阅读6分钟

重试重定向拦截器

  • 本文概述:

    • 本文重点探究了OkHttp 五大默认拦截器中的重试重定向拦截器,介绍了其工作角色、整体流程、深入源码重点探究了OkHttp 重试重定向拦截器如何发起重试、重试逻辑、重定向规则

工作角色:

  • 第一个接到用户请求的拦截器,最后一个接到服务端响应的拦截器

  • 并不会对请求做过多的处理,重点在响应

    • 根据请求过程是否出现异常
    • 根据响应中的状态码判断是否需要重定向

整体流程:

  • 就是一个while (true):帮助我们去重试

    • 重试没有次数限制
  • 重定向:次数限制20 次

      companion object {
        /**
         * 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.
         */
        private const val MAX_FOLLOW_UPS = 20
      }
    

重试相关:OkHttp 内部是可以去重试的,但规则苛刻

  • 实例化newExchangeFinder 对象:获取连接

    • 这个对象不是链接,这是获取连接的工具
    • 从连接池中去或者新建
    • 这个对象是在ConnectInterceptor 中使用
    //ExchangeFinder: 获取连接 (ConnectInterceptor中使用)
    call.enterNetworkInterceptorExchange(request, newExchangeFinder)
    
    //实例化对象
    if (newExchangeFinder) {
        this.exchangeFinder = ExchangeFinder(
            connectionPool,
            createAddress(request.url),
            this,
            eventListener
        )
    }
    
  • 检查用户是否取消了本次请求

    • 取消了 ---> 回调CallBack (在异步请求时就会回调一个CallBack )
    try {
        if (call.isCanceled()) {
            throw IOException("Canceled")
        }
    
  • 将request 请求对象交给责任链中的下一个拦截器

  • 在此拦截器中回去处理多个异常

    • 路线异常:OkHttp 内部的自定义异常

      • Sokect 链接建立失败等
    • I/O 异常:

      • HTTP2才会有ConnectionShutdownException 代表连接中断
      • 如果是因为IO异常,那么requestSendStarted=true (若是HTTP2的连接中断异常仍然为false)
    • 一旦发生异常:查看catch 语句

      • 执行recover
      if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
          throw e.withSuppressed(recoveredFailures)
      }
      
  • 重试逻辑:recover

    image-20220730195922173

    1. 判断是否具有重试权限:默认是有的

      • OkHttp 没有重试权限就不重试了
      • 在实例化OkHttpClient 对象时,可以指定是否需要OkHttp 帮助重试
      // 不允许OkHttp 内部进行重试 
      var okHttpClient = OkHttpClient.Builder()
      .retryOnConnectionFailure(false)
      
    2. 判断请求是否发出去并且requestIsOneShot 成立

      • OkHttp 想要上传文件但是文件找不到,也不重试

      • requestIsOneShot

        return (requestBody != null && requestBody.isOneShot()) ||
        e is FileNotFoundException
        
        • 请求体不为空

        • 请求体是否可以使用多次(默认是false ,只允许使用一次)

        • 异常是否属于FileNotFoundException

          • 通过Socket 上传文件给服务端,但文件找不到
    3. 发生指定异常时不会重试

      • 发生协议异常:服务端响应204 (没有响应体),但响应头字段Content-Length 不为0 (响应体长度不为0 ) ---> 协议逻辑矛盾

      • 发生IO 中断异常:OkHttp 不会重试

        • 但如果发生这个异常的原因是超时而导致连接失败(SockectTimeOut )的,那么OkHttp 会重试
      • 发生证书异常:证书格式、解析失败,OkHttp 不会重试

      • 证书验证失败:OkHttp 也不会重试

    4. 判断是否拥有更多的路线

      • 没有更多的路线 ---> OkHttp 不会去重试

      • 什么情况下会导致本次请求存在多条路线?

        • 给OkHttpClient 设置了代理

        • DNS 服务器返回多个IP 地址

          • 比如一个域名解析出了ip1,ip2;处理ip1 失败,那么我就重试ip2

重定向规则:

  • 整体概述:

    • 根据响应判断是否需要重定向
    • 源码细节一:重定向条件一

       将本次响应作为参数放入followUpRequest
           ---> 需要重试,返回一个新的Request 对象
               --->followUp 不为空
                   ---> 此时进行重定向 
           --->不需要重试,返回空
               --->followUp 为空
                   ---> 此时不进行重定向 (本次请求已经结束,将response 返回出去)
       val followUp = followUpRequest(response, exchange)
      
    • 源码细节二:重定向条件二

       //如果重试次数超过25 次,也不会重定向 ---> 抛出协议异常
       if (++followUpCount > MAX_FOLLOW_UPS) {
           throw ProtocolException("Too many follow-up requests: $followUpCount")
       }
      
    • 源码细节三:响应保存

       //本次重定向的request 去执行请求,会将本次的响应记录到临时变量中;
       //重定向结束后:会将执行重定向之前的上一次请求的结果记录当这一次重定向之后的结果中去
       request = followUp
       //将本次的响应记录到临时变量中
       priorResponse = response
      
  • followUpRequest 是如何工作的?

    • 概述:不仅仅做了重定向判断,我们根据服务端的不同响应码去做配置;

    • 执行细节:根据响应码 + when 进行处理(分为6 种情况)

      image-20220730201501565

    • 响应407:两步

      • 第一步:在实例化OkHttpClient 时配置了Http 代理(与Sokect 代理有什么区别)

         //配置Http 代理
         .proxy()
        
      • 第二步:对Http 代理授权

         //配置另外一个:对Http 代理进行授权
         .proxyAuthenticator(object: Authenticator{
             override fun authenticate(route: Route?, response: Response): Request? {
                 return response.request.newBuilder().addHeader("Proxy-Authorization","xxxx").build()
             }
         })
        
    • 响应的是401:还是两步,第二步不一样

       .authenticator()
      
    • 响应的是3XX :需要重定向

      • 此时服务端回传Location:Path(这个Path 就是本次重定向需要访问的地址 )
      • 根据Path 搞一个新的Request 对象,将其返回出去,进行重定向;
    • 响应的是408:请求超时

      • 三个条件才会发起重试(不是重定向)
      • 用户允许重试(默认成立)

      • 第一个请求返回408,第二次还是返回408,那么就不会进行重试了;

      • 服务端回传408 的同时在响应头中包含了Retry - After

        • Retry - After 的值:多久之后去重试(再次发起请求)

          • 值为0,或者就没有这个字段 ---> OkHttp 马上发起重试

            • 默认给了一个0 值
          • 值不为0 ---> OkHttp 直接return OkHttp 不进行重试了

            • 等这段时间到了(服务端已经明确告诉客户端,你在什么时候才来请求我),OkHttp 才去重试
    • 响应的是503 :服务不可用

      • 满足两个条件才能去重试
      • 虽然本次是503,但上一次请求返回的不是503,那么就可以去重试

      • 服务端必须明确响应Retry - After 且值为0,客户端立即去重试

        • 如果服务端给的非0 :不重试

        • 如果服务端没有给这个字段:

          • 默认值给的是Int 最大值,此时不会去重试
    • 响应的是421:

      • 当客户端对服务端的连接数超过了服务端定义的限制,此时客户端会自动用另外一个连接对象对服务端再次发起请求