美文网首页
Android OkHttp 源码阅读笔记(三)

Android OkHttp 源码阅读笔记(三)

作者: BlueSocks | 来源:发表于2023-12-12 22:14 被阅读0次

    OkHttp 源码阅读笔记(三)

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

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

    RetryAndFollowUpInterceptor

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

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

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

    链接错误的重试

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

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

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

    • 配置中必须支持重试
      默认值是 true,可以在构建 OkHttpClient 的时候自定义。

    • 没有发送过 Request 相关内容,如果已经发送过必须是 OneShot
      这个貌似和 Http 2 有关。

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

    • 还有可以重试的 Route
      是否有重试 Route 的判断后面看。

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

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

    • 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()

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

    相关文章

      网友评论

          本文标题:Android OkHttp 源码阅读笔记(三)

          本文链接:https://www.haomeiwen.com/subject/skqbgdtx.html