OkHttp 源码阅读笔记(三)

OkHttp 源码阅读笔记(三)

第一篇文章中介绍了 OkHttp 的同步调用和异步调用,Dispatcher 的任务调度器工作方式和 RealInterceptorChain 拦截器链的工作方式:OkHttp 源码阅读笔记(一)

第二篇文章中介绍了 OkHttp 如何从缓存中获取链接,如何创建链接以及 ConnectionPool 的工作原理:OkHttp 源码阅读笔记(二)

本篇文章是系列文章的第三篇,主要介绍几种系统拦截器的工作原理,在理解他们的工作原理前最好是对 HTTP 协议有一些理解,当然一边看源码,一边去网上查 HTTP 协议的相关功能也是没有问题的😄,这取决于你自己。

RetryAndFollowUpInterceptor

它的名字已经很直白了,它主要做两件事,链接错误的重试和重定向处理。

  • 链接错误的重试

    我们在前一篇文章中有了解到一个域名可能是有多个 Proxy 和 多个 IP Address 来完成链接,一种链接的方式在 OkHttp 中称为 RouteExchangeFinder 每获取到一个 Route 后,会通过 RealConnection#isHealthy() 方法来判断链接是否健康,如果不健康,就自动去获取下一个 Route 了。是啊,ExchangeFinder 会自动去获取,但是这里有一个前提,那就是不抛出异常的条件下。如果抛出了链接过程相关的异常就会被 RetryAndFlowUpInterceptor 所捕获到(比如 DNSTCP 或者 TLS 相关的错误),RetryAndFlowUpInterceptor 会判断是否有还有其他的 Route,如果有的话,会触发 ExchangeFinder 再次去查找或者创建可用的链接,直到试过所有的 Route 后,最后抛出异常(如果只有一个 Route 就直接抛出异常,我们的大部分情况都是这样)。

  • 重定向处理

    这个感觉没有什么好说的,就是 HTTP 协议中的重定向处理。

链接错误的重试

以下的代码我只保留了和链接错误重试的相关逻辑:

Kotlin 复制代码
  @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
    // 是否需要构建一个新的 ExchangeFinder
    var newExchangeFinder = true
    var recoveredFailures = listOf<IOException>()
    while (true) {
      // 触发 RealInterceptor 创建 ExchangeFinder(需要 newExchangeFinder 为 true)
      call.enterNetworkInterceptorExchange(request, newExchangeFinder)

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

        try {
          // 触发拦截器链
          response = realChain.proceed(request)
          newExchangeFinder = true
        } catch (e: RouteException) {
          // 在创建链接过程中发生的错误都是 RouteException
          // The attempt to connect via a route failed. The request will not have been sent.
          // recover 方法来判断当前的错误是否可以重试
          if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
            // 不可重试
            throw e.firstConnectException.withSuppressed(recoveredFailures)
          } else {
            // 可以重试
            recoveredFailures += e.firstConnectException
          }
          // 重试时不会创建新的 ExchangeFinder
          newExchangeFinder = false
          continue
        } catch (e: IOException) {
          // IOException 和上面的 RouteException 是类似的,只是某些参数不同。  
          // An attempt to communicate with a server failed. The request may have been sent.
          if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newExchangeFinder = false
          continue
        }

        // ...
      } finally {
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
  }

ExchangeFinder#find() 方法中抛出的异常全部都是 RouteException,然后会通过 recover() 方法来判断是否要继续重试下一个 Route,如果不重试就直接抛出异常,如果需要重试就进入下次循环,注意这里把 newExchangeFinder 设置成了 false,这样就不会创建一个新的 ExchangeFinder 了,然后 ExchangeFinder 就可以尝试用下一个 Route 来创建可用的链接。

我们再来看看 recover() 的实现:

Kotlin 复制代码
  private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    // The application layer has forbidden retries.
    // 设置是否允许重试
    if (!client.retryOnConnectionFailure) return false

    // 判断是否已经开始发送 Request 中的内容了,如果已经开始就必须是 OneShot
    // We can't send the request body again.
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
    
    // 判断是否是致命错误
    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false
    
    // 判断是否还有 Route 可以尝试
    // No more routes to attempt.
    if (!call.retryAfterFailure()) return false

    // For failure recovery, use the same route selector with a new connection.
    return true
  }

必须要同时满足以下四个条件才允许重试:

  • 配置中必须支持重试

    默认值是 true,可以在构建 OkHttpClient 的时候自定义。

  • 没有发送过 Request 相关内容,如果已经发送过必须是 OneShot

    这个貌似和 Http 2 有关。

  • 不是致命错误

    致命错误的判断后面再看。

  • 还有可以重试的 Route

    是否有重试 Route 的判断后面看。

这里看看 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
  }

致命错误的判断比较简单,大家自己看看源码就好了。

继续看看 RealCall#retryAfterFailure() 方法的实现:

Kotlin 复制代码
fun retryAfterFailure() = exchangeFinder!!.retryAfterFailure()

然后继续调用了 ExchangeFinder#retryAfterFailure() 方法:

Kotlin 复制代码
  fun retryAfterFailure(): Boolean {
    // 不同种类的失败记录都是 0,就表示没有失败过,直接返回 false
    if (refusedStreamCount == 0 && connectionShutdownCount == 0 && otherFailureCount == 0) {
      return false // Nothing to recover from.
    }

    // 如果有重试的 Route
    if (nextRouteToTry != null) {
      return true
    }
    
    // 重新查找重试的 Route
    val retryRoute = retryRoute()
    if (retryRoute != null) {
      // Lock in the route because retryRoute() is racy and we don't want to call it twice.
      nextRouteToTry = retryRoute
      return true
    }

    // If we have a routes left, use 'em.
    // 如果 Section 中还有 Route,返回 true
    if (routeSelection?.hasNext() == true) return true

    // If we haven't initialized the route selector yet, assume it'll have at least one route.
    // 如果没有初始化 RouteSelector,返回 true
    val localRouteSelector = routeSelector ?: return true

    // If we do have a route selector, use its routes.
    // 判断 RouteSelector 中是否还有 Selection
    return localRouteSelector.hasNext()
  }

如果有读我的上一篇文章,就能理解上面提到的 RouteSelectorSelection,他们都是用来管理 Route 的。

重定向处理

我们忽略掉链接重试的逻辑,只看和重定向相关的逻辑:

Kotlin 复制代码
 @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>()
    while (true) {
      call.enterNetworkInterceptorExchange(request, newExchangeFinder)

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

        try {
          response = realChain.proceed(request)
          newExchangeFinder = true
        } catch (e: RouteException) {
          // ...
        } catch (e: IOException) {
          // ...
        }

        // Attach the prior response if it exists. Such responses never have a body.
        // 如果这已经是第二次请求了(或者 2 次以上),会把上次的重定向 Response 存放在新的 Response 对象中
        if (priorResponse != null) {
          response = response.newBuilder()
              .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
              .build()
        }
        // 获取 Exchange 对象
        val exchange = call.interceptorScopedExchange
        // 获取重定向的 Request,如果为空就表示不需要重定向
        val followUp = followUpRequest(response, exchange)

        if (followUp == null) {
          // 不需要重定向,直接返回结果
          if (exchange != null && exchange.isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }

        val followUpBody = followUp.body
        if (followUpBody != null && followUpBody.isOneShot()) {
          // 这部分逻辑和 Http 2相关,跳过。 
          closeActiveExchange = false
          return response
        }
        
        // 如果是重定向的 Reponse,需要把它的 body 流关闭,提前释放对应的链接,避免泄漏。
        response.body?.closeQuietly()

        if (++followUpCount > MAX_FOLLOW_UPS) {
          // 达到最大的重定向次数,直接抛出异常,最大为 20 次
          throw ProtocolException("Too many follow-up requests: $followUpCount")
        }
        // 替换成重定向的 Request
        request = followUp
        // 将当前的 Response 保存下来,下次再请求时保存到后面的 Response 中
        priorResponse = response
      } finally {
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
  }

是否执行重定向的关键方法是 followUpRequest(),它的返回值是后续重定向请求的新的 Request,如果返回值为空就表示不需要重定向,反之就需要。最大的重定向次数是 20 次,超过次数就会直接抛出异常,这里注意和链接异常重试过程做一下比较,重定向是需要重新创建 ExchangeFinder 对象的,因为重定向后的地址可能会改变域名,所以原来的网络链接的相关 Route 就可能变为不可用。

我们继续看看 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 -> {
        val selectedProxy = route!!.proxy
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
        }
        // 通过 `proxyAuthenticator` 获取认证后的 Request
        return client.proxyAuthenticator.authenticate(route, userResponse)
      }
      
      // HTTP 认证,通过 `authenticator` 获取认证后的 Request
      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
      
      // 普通重定向
      HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
        // 构建重定向的 Request
        return buildRedirectRequest(userResponse, method)
      }
      
      // HTTP 的超时
      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
        }
        // 直接返回之前同样的 Request 去重试
        return userResponse.request
      }
      // 服务不可用
      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
      }
      // Http2 相关逻辑,跳过
      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
    }
  }

上面的重定向逻辑看似代码挺多,其实很简单,我们来整理一下:

  • HTTP_PROXY_AUTH(407)

    代理认证,有的代理是需要认证信息的,看源码是只支持 HTTP 类型的代理,需要通过 proxyAuthenticator(需要自定义) 来获取写入了认证信息的 Request

  • HTTP_UNAUTHORIZED(401)
    Http 的请求需要认证,和上面的代理认证类似,不过是通过 authenticator (需要自定义)来获取认证后的 Request

  • HTTP_PERM_REDIRECT(308), HTTP_TEMP_REDIRECT(307), HTTP_MULT_CHOICE(300), HTTP_MOVED_PERM(301), HTTP_MOVED_TEMP(302), HTTP_SEE_OTHER(303)

    以上都是普通的重定向,他们都会通过 buildRedirectRequest() 方法来构建新的 Request(后续再看源码)。

  • HTTP_CLIENT_TIMEOUT(408)

    超时错误,首先判断是否允许重试(可以配置,默认允许),然后判断之前是否已经重试过超时的这种场景(为 true 跳过),判断 Response 中是否有返回限制重试的时间(大于0)(有限制跳过),最后执行重试

  • HTTP_UNAVAILABLE(503)

    处理逻辑和 HTTP_CLITEN_TIMEOUT 差不多,但是必须指定重试时间为 0 才会重试。

我们再来看看通用的重定向 Request 创建方法 buildRedirectRequest()

Kotlin 复制代码
  private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
    // Does the client allow redirects?
    // 是否允许重定向
    if (!client.followRedirects) return null
    // 从 Response 中获取新连接的字符串
    val location = userResponse.header("Location") ?: return null
    // Don't follow redirects to unsupported protocols.
    // 将字符串转换成 HttpUrl 对象
    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
    // 判断配置 http 协议和 https 协议之间是否允许重定向
    if (!sameScheme && !client.followSslRedirects) return null

    // Most redirects don't include a request body.
    val requestBuilder = userResponse.request.newBuilder()
    // 以下是 RequestBody 的处理,我省略了
    if (HttpMethod.permitsRequestBody(method)) {
      // ...
    }

    // When redirecting across hosts, drop all authentication headers. This
    // is potentially annoying to the application layer since they have no
    // way to retain them.
    // 如果前后不是同一个请求,移除认证的 Header。
    if (!userResponse.request.url.canReuseConnectionFor(url)) {
      requestBuilder.removeHeader("Authorization")
    }
    // 替换原有的 Request 的 url 然后构建一个新的 Request。
    return requestBuilder.url(url).build()
  }

首先判断配置是否支持重定向,默认为支持(不支持直接返回);判断 Reaponse Header 中是否有重定向的 Location(没有直接返回);判断重定向前后的协议是否发生改变,如果发生改变了,通过配置判断是否支持协议改变后的重定向,默认支持(不支持直接返回),所谓的协议改变就是,前面是 http 协议,重定向后是 https 协议,或者前面是 https 协议,重定向后是 http 协议;如果前后的请求 url 不一致,就移除认证的 Header;最后替换旧的 Request 中的 url 为重定向的 url,最后构建一个 Request 返回。

BridgeInterceptor

BridgeInterceptor 负责对原有的某些 Request HeaderResponse Header 作出一些修正和添加一些默认的值,还负责对 Cookie 的处理,Cookie 的保存和获取的相关类是 CookieJar,默认 OkHttp 是没有实现的,我们可以在代码中可以自己实现。如果有人不理解 Cookie,可以去网上找找别的资料,我这里就不介绍了。

直接看 BridgeInterceptor 源码:

Kotlin 复制代码
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
    if (body != null) {
      // 设置 ContentType
      val contentType = body.contentType()
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString())
      }

      // 设置 ContentLength
      val contentLength = body.contentLength()
      if (contentLength != -1L) {
        requestBuilder.header("Content-Length", contentLength.toString())
        requestBuilder.removeHeader("Transfer-Encoding")
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked")
        requestBuilder.removeHeader("Content-Length")
      }
    }
    // 如果没有 Host,设置 Host
    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", userRequest.url.toHostHeader())
    }

    // 如果没有 Connection,设置 Connection,默认是 Keep-Alive
    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive")
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    // 如果没有 Accept-Encoding,设置默认支持的传输编码方式 `gzip`
    var transparentGzip = false
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true
      requestBuilder.header("Accept-Encoding", "gzip")
    }
    // 通过 CookieJar 获取当前 url,需要的 Cookies
    val cookies = cookieJar.loadForRequest(userRequest.url)
    if (cookies.isNotEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies))
    }
    
    // 如果没有设置 User-Agent,添加默认 User-Agent。
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", userAgent)
    }

    val networkResponse = chain.proceed(requestBuilder.build())
    
    // 将 ReponseHeader 中的 Cookie 相关的数据写入到 `CookieJar` 中
    cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)

    val responseBuilder = networkResponse.newBuilder()
        .request(userRequest)
    
    // 如果 ResponseBody 中是使用 gzip 编码
    if (transparentGzip &&
        "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
        networkResponse.promisesBody()) {
      val responseBody = networkResponse.body
      if (responseBody != null) {
        // 将原来的 ResponseBody 使用 GzipSource 来封装,通过它就可以直接解码 gzip 格式的数据
        val gzipSource = GzipSource(responseBody.source())
        // 移除 Response Header 中的 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")
        // 将 ResponseBody 替换成上面的带有 GzipSource 的 RealResponseBody
        responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
      }
    }

    return responseBuilder.build()
  }

  /** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */
  private fun cookieHeader(cookies: List<Cookie>): String = buildString {
    cookies.forEachIndexed { index, cookie ->
      if (index > 0) append("; ")
      append(cookie.name).append('=').append(cookie.value)
    }
  }
}

BridgeInterceptor 的代码可以说非常简单,代码非常清晰。可以分为两部分,对 Request Header 的处理和对 Response HeaderResponse Body 的处理。

如果 Request Body 不为空的话,通过 Request Body 获取 Content TypeContent Length,然后把他们写入到 Request Header 中;如果 Request Header 中没有设置 Host 的话,将当前请求的 url 格式化后写入到 Request Header,具体格式化的代码感兴趣自己可以看看;如果 Request Header 没有设置 Connection,设置为 Keep-Alive,这个参数是 Http 1.1 中定义的,也就是请求完成后不要关闭链接,后续的请求还可以复用这个链接;如果 Request Header 中没有设置 Accept-EncodingRange,设置为 gzip,也就是表明客户端支持的编码格式;从 CookieJar 中获取要对该次请求 url 需要设置的 Cookies,然后把它们写入到 Request Header 中;如果 Request Header 中没有设置 User-Agent,设置默认的 OkHttpUser-Agent;到这里 Reqeust Header 的处理就完成了,然后就是发起请求,等待 Response

获取到 Response 后,首先将 Response Header 中和 Cookies 相关的内容解析后放入 CookieJar 中(具体如何解析,我就没有贴代码了,大家可以自己去看看。);然后检查它的 Response Body 的编码方式,如果是 gzip,然后会把原来的 Response BodySource 使用 GzipSource 来封装,GzipSourceOkIo 中的对象,通过它可以将原来 gzip 编码的 Source 解码成明文。然后移除 Resonse Header 中的 Content-EncodingContent-Length,最后构建一个新的 Response

最后

本来想的是一口气把所有的系统 Interceptor 内容全部介绍完,然后发现写的东西越写越多,如果一次的内容太多你阅读累,我写的也累,所以剩下的内容下篇文章再介绍。

相关推荐
编程洪同学27 分钟前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
氤氲息3 小时前
Android 底部tab,使用recycleview实现
android
Clockwiseee3 小时前
PHP之伪协议
android·开发语言·php
小林爱3 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
小何开发4 小时前
Android Studio 安装教程
android·ide·android studio
开发者阿伟5 小时前
Android Jetpack LiveData源码解析
android·android jetpack
weixin_438150995 小时前
广州大彩串口屏安卓/linux触摸屏四路CVBS输入实现同时显示!
android·单片机
CheungChunChiu5 小时前
Android10 rk3399 以太网接入流程分析
android·framework·以太网·eth·net·netd
木头没有瓜6 小时前
ruoyi 请求参数类型不匹配,参数[giftId]要求类型为:‘java.lang.Long‘,但输入值为:‘orderGiftUnionList
android·java·okhttp
键盘侠0076 小时前
springboot 上传图片 转存成webp
android·spring boot·okhttp