Android开源框架系列-OkHttp4.11.0(kotlin版)- 拦截器分析之RetryAndFollowUpInterceptor

前言

在上一篇Android开源框架系列-OkHttp4.11.0-请求转发详细分析中我们分析了okhttp的请求转发逻辑,今天进入拦截器的源码分析,okhttp有五大拦截器,今天要分析的是重试重定向拦截器-RetryAndFollowUpInterceptor。

intercept

okhttp的五大拦截器通过责任链层层调用,首先来到的是RetryAndFollowUpInterceptor的intercept方法。

kotlin 复制代码
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
  //一个while无限循环,所以它只能由方法内部主动退出,否则会无限进行下去,这也是重试和重定向的基础。
  while (true) {
    //这一行比较关键,它是在为后边的拦截器执行做准备,查看1
    call.enterNetworkInterceptorExchange(request, newExchangeFinder)

    var response: Response
    var closeActiveExchange = true
    try {
      try {
        //看到这里可以知道,RetryAndFollowUpInterceptor只是做了一个ExchangeFinder对象的创建之后
        //就进入其他拦截器去执行了,RetryAndFollowUpInterceptor既然是重试重定向的功能,那么就表示他
        //是在响应回来之后才发挥作用的。我们假设realChain.proceed已经执行完成,可以进行下一步了
        response = realChain.proceed(request)
        newExchangeFinder = true
      } catch (e: RouteException) {
        //代码执行到这里表示由于路由问题,请求并没有发送出去,此时就要根据具体的情况来判断有没有重试的必要,查看2
        if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
        } 
        continue
      } catch (e: IOException) {
        // 代码执行到这里,表示发生了io异常,有可能请求已经发送出去了,但是由于服务器没有正常响应导致异常,查看2
        if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
        } 
        continue
      }

      // 代码第一次执行到这里,一定是空的,先不看
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                .body(null)
                .build())
            .build()
      }

      val exchange = call.interceptorScopedExchange
      //检查是否需要重定向,如果需要则返回一个新的request,查看5
      val followUp = followUpRequest(response, exchange)
      //返回null表示无法重定向(或重试)
      if (followUp == null) {
        if (exchange != null && exchange.isDuplex) {
          call.timeoutEarlyExit()
        }
        closeActiveExchange = false
        return response
      }
      ...
      request = followUp
      priorResponse = response
    } finally {
      call.exitNetworkInterceptorExchange(closeActiveExchange)
    }
  }
}

1.enterNetworkInterceptorExchange

ExchangeFinder对象创建之后并不会在RetryAndFollowUpInterceptor拦截器中使用,我们先了解有这么一个对象存在即可。

kotlin 复制代码
fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
  //传入的newExchangeFinder为true,所以这里new了一个ExchangeFinder对象出来
  if (newExchangeFinder) {
    this.exchangeFinder = ExchangeFinder(
        //连接池,此时连接池对象RealConnectionPool已经创建存在了
        connectionPool,
        createAddress(request.url),
        this,
        eventListener
    )
  }
}

2.recover

recover方法负责判断是否满足重新请求的条件。

kotlin 复制代码
private fun recover(
  e: IOException,
  call: RealCall,
  userRequest: Request,
  requestSendStarted: Boolean
): Boolean {
  // 如果client已经明确的设置了不允许重试(全局性设置),则返回false
  if (!client.retryOnConnectionFailure) return false
  // requestSendStarted表示请求是否已经发送过了,当e为RouteException的时候,requestSendStarted为false,表示没有发出,就跳过这个判断,当e为IOException并且不是ConnectionShutdownException
  //(ConnectionShutdownException为okhttp内定义的一种异常,只有http2协议会抛出)
  //的时候,requestSendStarted为true,requestIsOneShot查看3
  if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
  // isRecoverable查看4
  if (!isRecoverable(e, requestSendStarted)) return false
  // 如果此请求设置了失败后不允许重试(单个请求设置),则返回false
  if (!call.retryAfterFailure()) return false
  //走到这里说明满足重试的条件,返回true
  return true
}

3.requestIsOneShot

kotlin 复制代码
private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
  val requestBody = userRequest.body
  //如果请求体设置了只请求一次,或者请求一次之后响应的是文件未找到,则没有必要继续重试
  return (requestBody != null && requestBody.isOneShot()) ||
      e is FileNotFoundException
}

4.isRecoverable

kotlin 复制代码
private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
  // 协议异常,重试没有用,不必重试
  if (e is ProtocolException) {
    return false
  }

  // 超时类型的异常可以重试
  if (e is InterruptedIOException) {
    return e is SocketTimeoutException && !requestSendStarted
  }

  // 如果是因为证书问题导致握手失败,没有重试的必要
  if (e is SSLHandshakeException) {
    if (e.cause is CertificateException) {
      return false
    }
  }
  if (e is SSLPeerUnverifiedException) {
    //证书校验失败
    return false
  }
  // 其他情况则尝试重试
  return true
}

5.followUpRequest

followUpRequest主要是通过响应码来判断是否需要进行重定向

kotlin 复制代码
@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) {
    HTTP_PROXY_AUTH -> {
      //查看6
      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)
    }
    //查看7
    HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
    //查看7,重定向相应码,300、301、302、303、307、308
    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这个响应码较少见,但还是有一部分服务器使用这个响应码,当服务器返回408时,我们
      //可以不做任何修改,再次发起请求
      if (!client.retryOnConnectionFailure) {
        //设置了失败后不重试,返回
        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) {
        // 加入已经进行过一次408失败后的重试,不再继续重试,返回
        return null
      }
      //服务端通过响应头Retry-After提醒客户端多久之后再重试,则直接返回,等待下次
      if (retryAfter(userResponse, 0) > 0) {
        return null
      }
     
      return userResponse.request
    }

    HTTP_UNAVAILABLE -> {
      //503是服务端出错,可以尝试重试,但如果尝试一次又失败了,则直接返回
      val priorResponse = userResponse.priorResponse
      if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
        return null
      }
      //假如服务端通过响应头Retry-After给了一个0,则表示可以立即重试
      if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
        return userResponse.request
      }
      //都不满足不重试
      return null
    }

    HTTP_MISDIRECTED_REQUEST -> {
      // OkHttp可以合并HTTP/2连接,即使域名不同。见RealConnection.isEligible()。
      //如果我们尝试这样做,服务器返回HTTP 421,那么
      //我们可以在其他连接上重试。
      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
  }
}

6.HTTP_PROXY_AUTH(407)

尝试使用代理服务器时,代理服务器会向你请求进行身份验证,此时服务器会返回407,我们需要构建一个新的request去请求,并添加上代理服务器需要的身份验证的凭证,这个身份凭证就是通过 Proxy-Authorization请求头的方式添加,其格式为:

typescript 复制代码
Proxy-Authorization: <type> <credentials>

其中type表示身份验证类型,常见的身份验证类型就是基本验证:Basic,其他身份验证机制见这里。 credentials表示将用户名和密码用冒号拼接(aladdin:opensesame),然后将拼接生成的字符串使用 base64编码方式进行编码。 如:

javascript 复制代码
Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

我们通过okhttp怎么设置这个请求头?

less 复制代码
.proxyAuthenticator(new Authenticator() {
    @Nullable
    @Override
    public Request authenticate(@Nullable Route route, @NonNull Response response) throws IOException {
        String credential = Credentials.basic("代理服务用户名", "代理服务密码"); 
        return response.request().newBuilder()
            .header("Proxy-Authorization", credential)
            .build();
        }
    }
})

7.HTTP_UNAUTHORIZED(401)

和HTTP_PROXY_AUTH类似,401也是一种需要验证的机制。状态码401 Unauthorized代表客户端错误,指的是由于缺乏目标资源要求的身份验证凭证,发送的请求未得到满足。此时需要在请求头中添加Authorization,Authorization请求头的添加方式和Proxy-Authorization类似,Authorization请求头用于提供服务器验证用户代理身份的凭据,允许访问受保护的资源。

Basic 身份验证格式如下,YWxhZGRpbjpvcGVuc2VzYW1l是将用户名和密码通过一个冒号(aladdin:opensesame)相结合,然后将生成的字符串编码为base64后的结果。

makefile 复制代码
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

我们通过okhttp怎么设置这个请求头?

less 复制代码
.authenticator(new Authenticator() {
    @Nullable
    @Override
    public Request authenticate(@Nullable Route route, @NonNull Response response) throws IOException {
        String credential = Credentials.basic("jesse", "password1");
        return response.request().newBuilder()
            .header("Authorization", credential)
            .build();
    }
})

Proxy-Authorization和Authorization的区别?

他们之间的区别在于Proxy-Authorization用于客户端与代理服务器之间的认证,而Authorization是用于客户端和服务器之间的认证。

8.HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER

3xx 重定向状态码告诉客户端使用替代位置访问客户端感兴趣的资源。当资源被移动后,服务器可发送一个重定向状态码和一个可选的 Location 首部告诉客户端资源已被移走,以及现在哪里可以找到该资源,这样客户端就可以在不打扰使用者的情况在新的位置获取资源了。

看看这个重定向请求是如何构建的?

kotlin 复制代码
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
  // 不允许重定向,直接返回
  if (!client.followRedirects) return null
  //从响应头中取出Location,表示的就是资源被转移后的位置
  val location = userResponse.header("Location") ?: return null
  // resolve是将取出的url解析成HttpUrl对象,这样host、scheme等就都拿到了
  val url = userResponse.request.url.resolve(location) ?: return null

  // 如果重定向url的协议和原url的协议不同,并且设置了协议变化不能重定向,则返回空
  //followSslRedirects:是否遵循从HTTPS到HTTP和从HTTP到HTTPS的重定向,默认可以。
  val sameScheme = url.scheme == userResponse.request.url.scheme
  if (!sameScheme && !client.followSslRedirects) return null

  // 大部分重定向请求是不带body的,但是也存在非get类型的请求,含有body,修改请求头
  val requestBuilder = userResponse.request.newBuilder()
  //permitsRequestBody见9,不是get也不是head的情况下,带有body
  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) {
      //除了PROPFIND类型的请求外,都转为get请求
      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")
    }
  }

  // 加入重定向url的host、port、scheme都相同,就不用带Authorization进行验证了
  if (!userResponse.request.url.canReuseConnectionFor(url)) {
    requestBuilder.removeHeader("Authorization")
  }
  //将请求构建出来
  return requestBuilder.url(url).build()
}

9.permitsRequestBody

kotlin 复制代码
@JvmStatic 
fun permitsRequestBody(method: String): Boolean = !(method == "GET" || method == "HEAD")

总结

至此我们就分析完了okhttp重试重定向拦截器的逻辑,下一篇会继续进行BridgeInterceptor的分析。

相关推荐
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
大耳猫4 小时前
主动测量View的宽高
android·ui
帅次6 小时前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛7 小时前
Android中Crash Debug技巧
android
工业互联网专业11 小时前
Python毕业设计选题:基于Django+uniapp的公司订餐系统小程序
vue.js·python·小程序·django·uni-app·源码·课程设计
程序员小海绵【vincewm】12 小时前
【设计模式】结合Tomcat源码,分析外观模式/门面模式的特性和应用场景
设计模式·tomcat·源码·外观模式·1024程序员节·门面模式
kim565912 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼12 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ12 小时前
Android Studio使用c++编写
android·c++
csucoderlee13 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio