Android OkHttp 源码阅读笔记(三)

2023-12-12  本文已影响0人  BlueSocks

OkHttp 源码阅读笔记(三)

第一篇文章中介绍了 OkHttp 的同步调用和异步调用,Dispatcher 的任务调度器工作方式和 RealInterceptorChain 拦截器链的工作方式:Android OkHttp 源码阅读笔记(一)。
第二篇文章中介绍了 OkHttp 如何从缓存中获取链接,如何创建链接以及 ConnectionPool 的工作原理:Android OkHttp 源码阅读笔记(二)。

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

RetryAndFollowUpInterceptor

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

链接错误的重试

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

  @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() 的实现:

  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
  }

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

这里看看 isRecoverable() 方法的实现:

  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() 方法的实现:

fun retryAfterFailure() = exchangeFinder!!.retryAfterFailure()

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

  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 的。

重定向处理

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

 @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() 方法中是如何判断是否需要执行重定向的吧。

  @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
    }
  }

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

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

  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 源码:

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 内容全部介绍完,然后发现写的东西越写越多,如果一次的内容太多你阅读累,我写的也累,所以剩下的内容下篇文章再介绍。

上一篇下一篇

猜你喜欢

热点阅读