Android重学系列 OkHttp源码解析(二)

前言

阅读过上一篇对网络编程的概述一文后,应该对网络编程有一个大体的概念了。从本文开始,将会开始对OkHttp的源码开始进行解析。

OkHttp是由square开发的网络请求哭,它是当前Android开发中使用率高达近100%的网络请求库。而且在Android源码中也内置了这个库作为官方的网络请求。甚至在一小部分后端也开始使用了。

关于前置知识,可以阅读我写的上篇OKHttp系列解析(一) Okio源码解析 以及Android重学系列 Android网络编程 总览

正文

老规矩,先来看看OkHttp是如何使用的。

public class GetExample {
  OkHttpClient client = new OkHttpClient();

  String run(String url) throws IOException {
    Request request = new Request.Builder()
        .url(url)
        .build();

    try (Response response = client.newCall(request).execute()) {
      return response.body().string();
    }
  }

  public static void main(String[] args) throws IOException {
    GetExample example = new GetExample();
    String response = example.run("https://raw.github.com/square/okhttp/master/README.md");
    System.out.println(response);
  }
}

就用官方的例子来看看,实际上就是通过Get请求github中README文本。

这个过程涉及了如下几个重要角色:

  • OkHttpClient Okhttp用于请求的执行客户端
  • Request 通过构造者设计模式,构建的一个请求对象
  • Call 是通过 client.newCall 生成的请求执行对象,当执行了execute之后才会真正的开始执行网络请求
  • Response 是通过网络请求后,从服务器返回的信息都在里面。内含返回的状态码,以及代表响应消息正文的ResponseBody。

再来看看POST是怎么请求的:

public static final MediaType JSON
    = MediaType.get("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(json, JSON);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

和Get很相似,只是这个过程在Request中添加了RequestBody对象。这个RequestBody对象保存一个json字符串,并且设置MediaType为"application/json; charset=utf-8"也就是指正文格式,可以让客户端识别到应该怎么解析这串字符串。

当然还有put,delete等,和post流程十分相似就没有必要再展示了。

我们就以此为铺垫来看看整个OkHttp是怎么处理网络请求的。下面的原来来自最新的okHttp 4.1版本解析

OkHttp的概况

整个OkHttp设计的很有层次性,OKHttp把整个网络请求逻辑拆成7个拦截器,设计成责任链模式的处理。我们可以把Okhttp的网络请求大致分为如下几层,如下图:


OkHttp设计基础框架.png
  • 1.retryAndFollowUpInterceptor 重试拦截器
  • 2.BridgeInterceptor 建立网络桥梁的拦截器,主要是为了给网络请求时候,添加各种各种必要参数。如Cookie,Content-type
  • 3.CacheInterceptor 缓存拦截器,主要是为了在网络请求时候,根据返回码处理缓存。
  • 4.ConnectInterceptor 链接拦截器,主要是为了从链接池子中查找可以复用的socket链接。
  • 5.CallServerInterceptor 真正执行网络请求的逻辑。
  • 6.Interceptor 用户定义的拦截器,在重试拦截器之前执行
  • 7.networkInterceptors 用户定义的网络拦截器,在CallServerInterceptor(执行网络请求拦截器)之前运行。

每个拦截器的处理逻辑可以拆分为两个部分,请求部分(处理Request)可应答部分(处理Response )。

其中最后两点是允许用户进行自定义的。本文将会专门讲述前三点的设计和思想。

OkHttp 执行流程

我们来看看整个流程都做了什么?

实际上OkHttp 执行客户端可以通过建造者模式构建出来的,每一个成员变量代表了Okhttp中的一种能力,先来看看okhttp.Builder中的成员变量中都有写什么。

 class Builder constructor() {
    internal var dispatcher: Dispatcher = Dispatcher()
    internal var connectionPool: ConnectionPool = ConnectionPool()
    internal val interceptors: MutableList<Interceptor> = mutableListOf()
    internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()
    internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory()
    internal var retryOnConnectionFailure = true
    internal var authenticator: Authenticator = Authenticator.NONE
    internal var followRedirects = true
    internal var followSslRedirects = true
    internal var cookieJar: CookieJar = CookieJar.NO_COOKIES
    internal var cache: Cache? = null
    internal var dns: Dns = Dns.SYSTEM
    internal var proxy: Proxy? = null
    internal var proxySelector: ProxySelector? = null
    internal var proxyAuthenticator: Authenticator = Authenticator.NONE
    internal var socketFactory: SocketFactory = SocketFactory.getDefault()
    internal var sslSocketFactoryOrNull: SSLSocketFactory? = null
    internal var x509TrustManagerOrNull: X509TrustManager? = null
    internal var connectionSpecs: List<ConnectionSpec> = DEFAULT_CONNECTION_SPECS
    internal var protocols: List<Protocol> = DEFAULT_PROTOCOLS
    internal var hostnameVerifier: HostnameVerifier = OkHostnameVerifier
    internal var certificatePinner: CertificatePinner = CertificatePinner.DEFAULT
    internal var certificateChainCleaner: CertificateChainCleaner? = null
    internal var callTimeout = 0
    internal var connectTimeout = 10_000
    internal var readTimeout = 10_000
    internal var writeTimeout = 10_000
    internal var pingInterval = 0
    internal var minWebSocketMessageToCompress = RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE
    internal var routeDatabase: RouteDatabase? = null
...
}
  • Dispatcher Okhttp 请求分发器,是整个OkhttpClient的执行核心

  • ConnectionPool Okhttp链接池,不过会把任务委托给RealConnectionPool处理

  • interceptors: MutableList<Interceptor> 这是代表了okhttp所有的网络拦截器

  • networkInterceptors: MutableList<Interceptor> 这种特殊的拦截器,会生效在CallServerInterceptor真正执行网络请求的拦截器之前执行。

  • eventListenerFactory: EventListener.Factory 这是一个Event的监听器,可以监听到如callStart开始执行,proxySelectStart 代理选择开始,proxySelectEnd代理选择结束,dnsStartdns开始查找ip映射,dnsEnd查找ip完毕,connectStart开始链接服务器,secureConnectStart开始安全链接服务器,secureConnectEnd安全链接服务器结束,connectEnd链接结束等周期

  • authenticator: Authenticator 当出现401时候,就会使用这个对象进行身份校验。如果配置过登录信息,则会根据信息生成新的Request重新请求

  • followRedirects 是否允许重定向

  • cookieJar: CookieJar 这是okhttp 对cookie持久化的接口

  • cache: Cache 这部分实际上是okhttp持久化缓存的接口

  • dns: Dns 这部分也就是前文说过的,用于通过URL地址反过来查找ip地址的对象,默认为DnsSystem.在这里面ip地址是使用java对象InetAddress来表示。

  • proxy: ProxyProxySelector Okhttp中请求的代理对象以及自动代理选择器。proxyAuthenticator: Authenticator 代理服务的401权限校验处理中心.

  • socketFactory: SocketFactory 默认的socket链接池

  • sslSocketFactoryOrNull: SSLSocketFactory 用于https的socket链接工厂

  • X509TrustManager 用于信任https证书的对象,编码格式为X.509。换句话说,就是在TLS握手期间进行校验,如果校验失败则链接失败

  • connectionSpecs: List<ConnectionSpec> ConnectionSpec实际上是用于指定http请求时候的socket链接,如果是Https则是构建TLS链接时候向服务端说明的TLS版本,密码套件的类。

  • protocols: List<Protocol> Okhttp支持的自定义请求协议集合,内含http1.1,http2.0

  • hostnameVerifier: HostnameVerifier。也是https中的校验,只是是在握手之后,对host进行校验。

  • CertificatePinner 在SSL过程锁定证书,只允许设置在这个类中的证书才能正常的链接。

  • CertificateChainCleaner 这是一个证书链清理器。证书链是指使用一组收到信任的证书构成的根证书链,在TLS握手过程中,会清理链路中的证书。

  • connectTimeout 链接超时时间

  • readTimeout 读取超时时间,writeTimeout写入超时时间

  • RealWebSocket管理Okhttp内部所有的websocket对象

  • RouteDatabase 这是okhttp学习那些链接不上去的黑名单地址,如果出现请求这些黑名单就会想办法。如果出现了链接过程出现异常,之后会想办法找到可以替代的路由。

大致上我们需要聊的内容都在这里面了。有了对okhttp的大体的功能之后,我们先来看看Okhttp的运行远离。

Okhttp 分发请求入口 newCall

先来看看Okhttp如果需要构造一个可以请求的对象需要调用如下方法

  override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

生成一个RealCall对象。其中第三个参数说迷宫这个请求是否是websocket。

得到RealCall对象之后,一般有两种选择进行网络请求:

  • 1.如官方给出的方法,调用excute方法,把执行设置在RealCall的Request对象。很少使用excute的方式,开发中更多的下面这种方式。

  • 2.使用方法enqueue 把请求分发按照队列方式顺序消费执行。

RealCall excute

  override fun execute(): Response {
    check(executed.compareAndSet(false, true)) { "Already Executed" }

    timeout.enter()
    callStart()
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)
    }
  }

  private fun callStart() {
    this.callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()")
    eventListener.callStart(this)
  }

首先回调上面设置的EventListenter的callStart方法,接着调用Dispatcherexecute方法,把当前的RealCall传入,最后调用getResponseWithInterceptorChain 阻塞获取从网络请求响应后的结果,最后调用Dispatcherfinished 方法

RealCall enqueue

  override fun enqueue(responseCallback: Callback) {
    check(executed.compareAndSet(false, true)) { "Already Executed" }

    callStart()
    client.dispatcher.enqueue(AsyncCall(responseCallback))
  }

整个流程就很简单了,先给当前的RealCall的executed原子类设置为true后,回调callStart方法,最后调用把外面监听响应数据的接口responseCallback封装成AsyncCall后作为参数传入 Dispatcherenqueue方法。

我们主要来看enqueue方法的设计,execute的使用场景确实不多。

AsyncCall

先来看看AysncCall都做了什么?

  internal inner class AsyncCall(
    private val responseCallback: Callback
  ) : Runnable {
    @Volatile var callsPerHost = AtomicInteger(0)
      private set

    fun reuseCallsPerHostFrom(other: AsyncCall) {
      this.callsPerHost = other.callsPerHost
    }

    val host: String
      get() = originalRequest.url.host

    val request: Request
        get() = originalRequest

    val call: RealCall
        get() = this@RealCall

    /**
     * Attempt to enqueue this async call on [executorService]. This will attempt to clean up
     * if the executor has been shut down by reporting the call as failed.
     */
    fun executeOn(executorService: ExecutorService) {
      client.dispatcher.assertThreadDoesntHoldLock()

      var success = false
      try {
        executorService.execute(this)
        success = true
      } catch (e: RejectedExecutionException) {
        val ioException = InterruptedIOException("executor rejected")
        ioException.initCause(e)
        noMoreExchanges(ioException)
        responseCallback.onFailure(this@RealCall, ioException)
      } finally {
        if (!success) {
          client.dispatcher.finished(this) // This call is no longer running!
        }
      }
    }

    override fun run() {
      threadName("OkHttp ${redactedUrl()}") {
        var signalledCallback = false
        timeout.enter()
        try {
          val response = getResponseWithInterceptorChain()
          signalledCallback = true
          responseCallback.onResponse(this@RealCall, response)
        } catch (e: IOException) {
          if (signalledCallback) {
            // Do not signal the callback twice!
            Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)
          } else {
            responseCallback.onFailure(this@RealCall, e)
          }
        } catch (t: Throwable) {
          cancel()
          if (!signalledCallback) {
            val canceledException = IOException("canceled due to $t")
            canceledException.addSuppressed(t)
            responseCallback.onFailure(this@RealCall, canceledException)
          }
          throw t
        } finally {
          client.dispatcher.finished(this)
        }
      }
    }
  }

AysncCall是一个Runnable对象。一旦线程开始执行的时候就会运行其中的run方法。而executeOn 方法这是允许任何的线程池执行该AsyncCall对象,接着调用其中的run方法。

在run方法中依次执行了如下步骤:

    1. getResponseWithInterceptorChain 执行Okhttp中所有的拦截器,并获得对象response。
  • 2.调用responseCallback的onResponse 方法把response对象回调出去。
  • 3.如果遇到IOException异常则返回responseCallback.onFailure
  • 4.其他异常则调用cancel 方法取消请求后,回调responseCallback.onFailure
  • 5.调用Dispatcher的finished 方法结束执行。

Dispatcher enqueue

  private val readyAsyncCalls = ArrayDeque<AsyncCall>()
  private val runningAsyncCalls = ArrayDeque<AsyncCall>()
  private val runningSyncCalls = ArrayDeque<RealCall>()

  internal fun enqueue(call: AsyncCall) {
    synchronized(this) {
      readyAsyncCalls.add(call)

      if (!call.call.forWebSocket) {
        val existingCall = findExistingCallWithHost(call.host)
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
      }
    }
    promoteAndExecute()
  }

  private fun findExistingCallWithHost(host: String): AsyncCall? {
    for (existingCall in runningAsyncCalls) {
      if (existingCall.host == host) return existingCall
    }
    for (existingCall in readyAsyncCalls) {
      if (existingCall.host == host) return existingCall
    }
    return null
  }

这里有三个队列十分重要:

  • 1.readyAsyncCalls 异步执行的准备队列
  • 2.runningAsyncCalls 正在异步执行队列
  • 3.runningSyncCalls 正在同步执行队列

清楚这三个队列的功能后,下面就很好理解了:

  • 1.首先把当前的AsyncCall 添加到readyAsyncCalls 预备执行队列中。这是一个 ArrayDeque对象,这是一个双端队列,可以作为栈使用。这个队列看起来像一个链表,实质上内部是一个数组(每一次扩容为原来的2倍,并且初始值为8,内用head和tail标示整个队列的范围,只允许操作头部和尾部).

  • 2.其次通过findExistingCallWithHost 查找是否有host相同的AsyncCall 在runningAsyncCallsreadyAsyncCalls,存在则调用reuseCallsPerHostFrom复用这个AsyncCall,也就是复用这个请求配置,没必要重新构建全新的对象。

  • 3.promoteAndExecute 开始通过线程池执行保存在队列中的AsyncCall

promoteAndExecute

  @get:Synchronized
  @get:JvmName("executorService") val executorService: ExecutorService
    get() {
      if (executorServiceOrNull == null) {
        executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
            SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
      }
      return executorServiceOrNull!!
    }

@get:Synchronized var maxRequests = 64
    set(maxRequests) {
      require(maxRequests >= 1) { "max < 1: $maxRequests" }
      synchronized(this) {
        field = maxRequests
      }
      promoteAndExecute()
    }

  @get:Synchronized var maxRequestsPerHost = 5
    set(maxRequestsPerHost) {
      require(maxRequestsPerHost >= 1) { "max < 1: $maxRequestsPerHost" }
      synchronized(this) {
        field = maxRequestsPerHost
      }
      promoteAndExecute()
    }


  private fun promoteAndExecute(): Boolean {
    this.assertThreadDoesntHoldLock()

    val executableCalls = mutableListOf<AsyncCall>()
    val isRunning: Boolean
    synchronized(this) {
      val i = readyAsyncCalls.iterator()
      while (i.hasNext()) {
        val asyncCall = i.next()

        if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
        if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

        i.remove()
        asyncCall.callsPerHost.incrementAndGet()
        executableCalls.add(asyncCall)
        runningAsyncCalls.add(asyncCall)
      }
      isRunning = runningCallsCount() > 0
    }

    for (i in 0 until executableCalls.size) {
      val asyncCall = executableCalls[i]
      asyncCall.executeOn(executorService)
    }

    return isRunning
  }

SynchronousQueue是一个单对单的消费者生产者模式数据结构,必须要要有一个Request对应一个Data模式的数据结构。更多的Request会进入阻塞等待,知道有Data来匹配。

这里面设置一个默认的线程池,其中线程的调度队列就是通过SynchronousQueue处理成一对一的消费者生产者模式。

而在一次请求中一次性消费AsyncCall最大的数量默认为64,且这个AysncCall复用Host的次数要小于5次。

最后就会添加到executableCalls和runningAsyncCalls 加入到执行队列中。最后遍历一次executableCalls中AsyncCall的executeOn,执行其中的run方法。

那么我们需要看看整个Okhttp执行的核心方法getResponseWithInterceptorChain.

getResponseWithInterceptorChain

  @Throws(IOException::class)
  internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        call = this,
        interceptors = interceptors,
        index = 0,
        exchange = null,
        request = originalRequest,
        connectTimeoutMillis = client.connectTimeoutMillis,
        readTimeoutMillis = client.readTimeoutMillis,
        writeTimeoutMillis = client.writeTimeoutMillis
    )

    var calledNoMoreExchanges = false
    try {
      val response = chain.proceed(originalRequest)
      if (isCanceled()) {
        response.closeQuietly()
        throw IOException("Canceled")
      }
      return response
    } catch (e: IOException) {
      calledNoMoreExchanges = true
      throw noMoreExchanges(e) as Throwable
    } finally {
      if (!calledNoMoreExchanges) {
        noMoreExchanges(null)
      }
    }
  }

首先构造一个可变的 Interceptor集合。这个集合的顺序其实就是指代了整个okhttp拦截器的执行顺序。

整个流程如图:


OkHttp设计基础框架.png

注意如果是websocket的话,就不会执行用户自定定义的NetworkInterceptor。

然后使用RealInterceptorChain包裹所有的拦截器后,执行RealInterceptorChain.proceed方法执行Request。

RealInterceptorChain 拦截器管理器

  internal fun copy(
    index: Int = this.index,
    exchange: Exchange? = this.exchange,
    request: Request = this.request,
    connectTimeoutMillis: Int = this.connectTimeoutMillis,
    readTimeoutMillis: Int = this.readTimeoutMillis,
    writeTimeoutMillis: Int = this.writeTimeoutMillis
  ) = RealInterceptorChain(call, interceptors, index, exchange, request, connectTimeoutMillis,
      readTimeoutMillis, writeTimeoutMillis)

  override fun proceed(request: Request): Response {
    check(index < interceptors.size)

    calls++

...

    // Call the next interceptor in the chain.
    val next = copy(index = index + 1, request = request)
    val interceptor = interceptors[index]

    @Suppress("USELESS_ELVIS")
    val response = interceptor.intercept(next) ?: throw NullPointerException(
        "interceptor $interceptor returned null")
...
    return response
  }

这个过程可以看到实际上又copy可一个RealInterceptorChain 对象,不过index 下标增加了1,此时就会继续执行对应下标的Interceptor。

换句话说,每当一个拦截器走完一个Request的处理流程就会生成一个新的RealInterceptorChain并且下标+1,时候下一个拦截器的intercept方法。不断的迭代下去。

这种思路,在我写的OkRxCache的库中有使用,很实用。能够把复杂且层级结构分明的逻辑拆分出来,做到可组装的效果。

那么就来看看第一个拦截器retryAndFollowUpInterceptor 做了什么?

retryAndFollowUpInterceptor 重试拦截器

整个拦截器可以划分为2个部分进行理解,以方法realChain.proceed作为分割线,上部分为请求逻辑,下部分为应答处理逻辑

retryAndFollowUpInterceptor 处理请求

  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) {

          if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
            throw e.firstConnectException.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e.firstConnectException
          }
          newExchangeFinder = false
          continue
        } catch (e: IOException) {

          if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newExchangeFinder = false
          continue
        }

    ...
    }
  }
  • 1.调用RealCall的enterNetworkInterceptorExchange方法实例化一个ExchangeFinder在RealCall对象中。
  • 2.执行RealCall的proceed 方法,进入下一个拦截器,进行下一步的请求处理。
  • 3.如果出现路由异常,则通过recover方法校验,当前的链接是否可以重试,不能重试则抛出异常,离开当前的循环。
recover 校验链接是否可以重试
  private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure) return false

    // 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

    // No more routes to attempt.
    if (!call.retryAfterFailure()) return false

    // For failure recovery, use the same route selector with a new connection.
    return true
  }
  • 1.如果Okhttp 的retryOnConnectionFailure为false,禁止重试则返回false

  • 2.requestIsOneShot 如果requestIsOneShot校验的是RequestBody的isOneShot是否是true, isOneShot默认是false。说明一个请求正文可以多次请求(多次请求的情况如408 客户端超时;401和407 权限异常可以通过头部进行满足;503 服务端异常,但是头部的retry-After为0可以进行重试)。

  • 3.isRecoverable 校验当前的异常是否是可恢复的异常。ProtocolException 协议异常返回false;InterruptedIOException io读写异常同时是socket链接超时异常可以重试;SSLHandshakeException https握手时候的异常同时是校验异常CertificateException会返回false; SSLPeerUnverifiedException证书校验异常则返回false。

retryAndFollowUpInterceptor 处理应答

   if (priorResponse != null) {
          response = response.newBuilder()
              .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
              .build()
        }

        val exchange = call.interceptorScopedExchange
        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()) {
          closeActiveExchange = false
          return response
        }

        response.body?.closeQuietly()

        if (++followUpCount > MAX_FOLLOW_UPS) {
          throw ProtocolException("Too many follow-up requests: $followUpCount")
        }

        request = followUp
        priorResponse = response
      } finally {
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
  • 1.每一次循环都会获取上次应答数据作为本次重定向或者权限询问的参数。
  • 2.followUpRequest 根据当前的响应体,更新请求体中的内容。
  • 3.如果当前的仇视次数超过了20次,就会抛出异常,跳出循环。
private const val MAX_FOLLOW_UPS = 20

其实整个重试拦截器最为核心的内容就是followUpRequest方法。

followUpRequest 根据应答重试请求处理

  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")
        }
        return client.proxyAuthenticator.authenticate(route, userResponse)
      }

      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 -> {
        return buildRedirectRequest(userResponse, method)
      }

      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
        }

        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
      }

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

这个过程处理了几个HttpCode状态码:

    public static final int HTTP_PROXY_AUTH = 407;
    public static final int HTTP_UNAUTHORIZED = 401;
    public static final int HTTP_CLIENT_TIMEOUT = 408;
    public static final int HTTP_UNAVAILABLE = 503;
    const val HTTP_TEMP_REDIRECT = 307
    const val HTTP_PERM_REDIRECT = 308
    const val HTTP_MISDIRECTED_REQUEST = 421

下面我们一个个的解析:

状态码407 代理需要校验身份
        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)

如果状态代码是407,此时会校验当前路由的代理模式是不是Http协议,不是则抛出异常,没有使用代理时候接受到了407。如果是,则通过Authenticator对响应体进行校验。

此时默认是设置没有任何行为的代理权限校验器。如果设置了就会调用Authenticator进行校验,这里面可以设置根据路由和响应体获取到对应的校验处理。可以来看看OkHttp内置的一个JavaNetAuthenticator 做了什么?

  @Throws(IOException::class)
  override fun authenticate(route: Route?, response: Response): Request? {
    val challenges = response.challenges()
    val request = response.request
    val url = request.url
    val proxyAuthorization = response.code == 407
    val proxy = route?.proxy ?: Proxy.NO_PROXY

    for (challenge in challenges) {
      if (!"Basic".equals(challenge.scheme, ignoreCase = true)) {
        continue
      }

      val dns = route?.address?.dns ?: defaultDns
      val auth = if (proxyAuthorization) {
        val proxyAddress = proxy.address() as InetSocketAddress
        Authenticator.requestPasswordAuthentication(
            proxyAddress.hostName,
            proxy.connectToInetAddress(url, dns),
            proxyAddress.port,
            url.scheme,
            challenge.realm,
            challenge.scheme,
            url.toUrl(),
            Authenticator.RequestorType.PROXY
        )
      } else {
        Authenticator.requestPasswordAuthentication(
            url.host,
            proxy.connectToInetAddress(url, dns),
            url.port,
            url.scheme,
            challenge.realm,
            challenge.scheme,
            url.toUrl(),
            Authenticator.RequestorType.SERVER
        )
      }

      if (auth != null) {
        val credentialHeader = if (proxyAuthorization) "Proxy-Authorization" else "Authorization"
        val credential = Credentials.basic(
            auth.userName, String(auth.password), challenge.charset)
        return request.newBuilder()
            .header(credentialHeader, credential)
            .build()
      }
    }

    return null // No challenges were satisfied!
  }

核心其实很简单,就是通过Authenticator.requestPasswordAuthentication获得一个PasswordAuthentication对象。从这个对象中获取对应代理设置的权限账号密码,并且设置到Authorization或者Proxy-Authorization头部key中,重试时候会带上这个头部放到新的请求中。

状态码 401 请求要求用户的身份认证
return client.authenticator.authenticate(route, userResponse)

说明此时需要通过Authenticator校验身份,可以发送自己的用户名和密码过去进行校验。

状态码300,301,302,303,307,308
return buildRedirectRequest(userResponse, method)

状态码30X 系列一般是发生了资源变动处理的行为。如重定向跳转等。

  • 300 是指有多种选择。请求的资源包含多个位置
  • 301 请求的资源已经永久移动了 会自动重定向
  • 302 临时移动,资源是临时转移了,客户端可以沿用原来的url
  • 303 查看其他地址,可301类似
  • 307 临时重定向,GET请求的重定向
  • 308 和307类似也是临时重定向
  private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
    if (!client.followRedirects) return null

    val location = userResponse.header("Location") ?: return null
    val url = userResponse.request.url.resolve(location) ?: return null

    val sameScheme = url.scheme == userResponse.request.url.scheme
    if (!sameScheme && !client.followSslRedirects) return null

    val requestBuilder = userResponse.request.newBuilder()
    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) {
        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")
      }
    }

    if (!userResponse.request.url.canReuseConnectionFor(url)) {
      requestBuilder.removeHeader("Authorization")
    }

    return requestBuilder.url(url).build()
  }
  • 1.先从响应头,取出Locationkey对应的值,而这个值就是重定向之后的url路径。

  • 2.并且校验这个请求是否是GET或者HEAD.

    • 2.1.如果不是,则请求方式不是PROPFIND 且状态码不是307或者308状态码,则强制设置请求方式为GET
    • 2.2.否则则判断请求状态码是307或者308,或者请求方式是PROPFIND,那么继承之前设置的请求体。
  • 3.不是307或者308且不是PROPFIND,则清掉Header中的Content-Length,Transfer-Encoding,Content-Type

  • 4.如果之前的请求和本次请求的host(主机)和port(端口)一致,则不需要Authorization校验身份。

状态码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) {
          return null
        }

        if (retryAfter(userResponse, 0) > 0) {
          return null
        }

        return userResponse.request

状态码408不常见,但是在HAProxy中会比较常见。这种情况说明我们可以重复的进行没有修改过的请求(甚至是非幂等请求)。

HAProxy 是一种高可用,负载均衡的,基于TCP和HTTP的应用程序代理。十分合适负载十分大的服务器。如github,stackflow等都集成了。它实现了事件驱动,单一进程模型支持十分大的(超越一个进程因内存模型而限制的线程数目)。

实际上还是那老一套的,基于系统调用epoll,select,poll等实现。

这一段代码的逻辑校验了如下逻辑:

  • 1.当前okhttp是否允许重试
  • 2.请求体是否允许重复发送
  • 3.是否已经重试了,且重试的状态是否还是408
  • 4.通过retryAfter获取响应头部信息Retry-After(头部存在该key,则设置为key的内容否则设置为0.不存在该key设置为INT的最大数值)。拿到重试时间后判断是否大于0,大于0说明此时返回一个空的请求对象,Okhttp将不会处理抛给业务层自己处理。
状态码 503 由于服务器的异常导致无法完成客户端的请求
        val priorResponse = userResponse.priorResponse
        if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
          return null
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          return userResponse.request
        }
        return null

如果上一次的请求已经是503了,就没必要重复请求了。且如果Retry-After 设置为0,说明需要立即重复请求,才会重新请求,其他情况下只会放弃请求。

状态码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
      }

这种情况下,只要requestBody允许重复发送,且host发生了变化,则重新返回请求体重新请求。这种情况下,一般是指Http 2.0协议。这种情况下,只要链接链接的得失同一个服务器,且RealConntection是合法的,如果服务器返回了421状态码,可以复用这个流。

BridgeInterceptor 桥接拦截器

BridgeInterceptor 处理请求体头部

  @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) {
      val contentType = body.contentType()
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString())
      }

      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")
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", userRequest.url.toHostHeader())
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive")
    }

    var transparentGzip = false
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true
      requestBuilder.header("Accept-Encoding", "gzip")
    }

    val cookies = cookieJar.loadForRequest(userRequest.url)
    if (cookies.isNotEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies))
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", userAgent)
    }

    val networkResponse = chain.proceed(requestBuilder.build())
...
  }

在请求到下一个拦截器之前,做了如下的事情:

  • 1.设置头部的Content-Type.说明内容类型是什么
  • 2.如果contentLength大于等于0,则设置头部的Content-Length(说明内容大小是多少);否则设置头部的Transfer-Encodingchunked(说明传输编码为分块传输)
  • 3.如果Host不存在,设置头部的Host(在Http 1.1之后出现,可以通过同一个URL访问到不同主机,从而实现服务器虚拟服务器的负载均衡。如果1.1之后不设置就会返回404)。
  • 4.如果Connection不存在,设置头部的ConnectionKeep-Alive(代表链接状态需要保持活跃)
  • 5.如果Accept-EncodingRange为空,则强制设置Accept-Encodinggzip(说明请求将会以gzip方式压缩)
  • 6.从CookieJar的缓存中取出cookie设置到头部的Cookie
  • 7.如果User-Agent为空,则设置User-Agent到头部

Cookie是什么?Cookie是http协议中用于追踪用户会话的机制。注意Http协议是无状态的(但是底层构成http协议的tcp协议是有状态用于控制数据的正确性)。一个用户所有的请求都是同属一个会话。在http协议中,服务器为了得知客户端的身份会给每一个客户端分配一个cookie,同时在服务器有一个sessionID进行对应。都会保存在服务器的Map中。正是有这个上下文才会正确的知道该用户的的请求状态。

BridgeInterceptor 处理响应体


    cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)

    val responseBuilder = networkResponse.newBuilder()
        .request(userRequest)

    if (transparentGzip &&
        "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
        networkResponse.promisesBody()) {
      val responseBody = networkResponse.body
      if (responseBody != null) {
        val gzipSource = GzipSource(responseBody.source())
        val strippedHeaders = networkResponse.headers.newBuilder()
            .removeAll("Content-Encoding")
            .removeAll("Content-Length")
            .build()
        responseBuilder.headers(strippedHeaders)
        val contentType = networkResponse.header("Content-Type")
        responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
      }
    }

    return responseBuilder.build()
  • 1.读取响应头上的信息,把cookie'保存到CookieJar中
  • 2.如果此时是Content-Encoding是gzip的内容压缩格式,则获取当前的响应体的内容,并根据 Content-Type把压缩格式还原,重新设置到响应体中。

CacheInterceptor 缓存拦截器

CacheInterceptor 缓存拦截器处理请求

  override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()

    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    val networkRequest = strategy.networkRequest
    val cacheResponse = strategy.cacheResponse

    cache?.trackResponse(strategy)
    val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE

    if (cacheCandidate != null && cacheResponse == null) {
      // The cache candidate wasn't applicable. Close it.
      cacheCandidate.body?.closeQuietly()
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(HTTP_GATEWAY_TIMEOUT)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build().also {
            listener.satisfactionFailure(call, it)
          }
    }

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build().also {
            listener.cacheHit(call, it)
          }
    }

    if (cacheResponse != null) {
      listener.cacheConditionalHit(call, cacheResponse)
    } else if (cache != null) {
      listener.cacheMiss(call)
    }

    var networkResponse: Response? = null
    try {
      networkResponse = chain.proceed(networkRequest)
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        cacheCandidate.body?.closeQuietly()
      }
    }

...
  }
  • 1.先根据请求体的状态通过CacheStrategy获取到是否需要缓存,从而获得本次网络请求的请求体以及缓存的响应体,关键就是
val cacheCandidate = cache?.get(chain.request())
 CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
  • 2.如果两者都获取不到,此时会返回协议http 1.1,错误代码为504,消息是禁止了网络请求但是缓存响应不存在的应答消息体。

  • 3.如果只是网络请求不存在,而缓存的响应体存在,则根据缓存的响应构造出响应体对象,返回cacheHit会爱到并返回。

  • 4.如果网络请求存在,则正常的进入下一个拦截器中。

Cache get 从LRUCache中获取缓存
@JvmStatic
    fun key(url: HttpUrl): String = url.toString().encodeUtf8().md5().hex()

  internal fun get(request: Request): Response? {
    val key = key(request.url)
    val snapshot: DiskLruCache.Snapshot = try {
      cache[key] ?: return null
    } catch (_: IOException) {
      return null // Give up because the cache cannot be read.
    }

    val entry: Entry = try {
      Entry(snapshot.getSource(ENTRY_METADATA))
    } catch (_: IOException) {
      snapshot.closeQuietly()
      return null
    }

    val response = entry.response(snapshot)
    if (!entry.matches(request, response)) {
      response.body?.closeQuietly()
      return null
    }

    return response
  }
  • 1.首先把url路径转化成utf-8,并且md5一下拿到摘要,并调用hex获取摘要的16进制的字符串,这个字符串就是LRUCache的key

  • 2.通过key拿到Cache中DiskLruCache.Snapshot对象,每一个Snapshot就是LRUCache的缓存单位。每一个缓存单位中都缓存一个文件,在cache的get操作方法重写中,会读取数据为Okio的Source

  operator fun get(key: String): Snapshot? {
    initialize()

    checkNotClosed()
    validateKey(key)
    val entry = lruEntries[key] ?: return null
    val snapshot = entry.snapshot() ?: return null

    redundantOpCount++
    journalWriter!!.writeUtf8(READ)
        .writeByte(' '.toInt())
        .writeUtf8(key)
        .writeByte('\n'.toInt())
    if (journalRebuildRequired()) {
      cleanupQueue.schedule(cleanupTask)
    }

    return snapshot
  }
  • 3.把数据转化为Respone 响应体
CacheStrategy compute 计算获取请求对应的缓存
    fun compute(): CacheStrategy {
      val candidate = computeCandidate()

      // We're forbidden from using the network and the cache is insufficient.
      if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
        return CacheStrategy(null, null)
      }

      return candidate
    }

    /** Returns a strategy to use assuming the request can use the network. */
    private fun computeCandidate(): CacheStrategy {
      // No cached response.
      if (cacheResponse == null) {
        return CacheStrategy(request, null)
      }

      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps && cacheResponse.handshake == null) {
        return CacheStrategy(request, null)
      }

      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }

      val requestCaching = request.cacheControl
      if (requestCaching.noCache || hasConditions(request)) {
        return CacheStrategy(request, null)
      }

      val responseCaching = cacheResponse.cacheControl

      val ageMillis = cacheResponseAge()
      var freshMillis = computeFreshnessLifetime()

      if (requestCaching.maxAgeSeconds != -1) {
        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
      }

      var minFreshMillis: Long = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }

      var maxStaleMillis: Long = 0
      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
      }

      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        val builder = cacheResponse.newBuilder()
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
        }
        val oneDayMillis = 24 * 60 * 60 * 1000L
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
        }
        return CacheStrategy(null, builder.build())
      }

      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      val conditionName: String
      val conditionValue: String?
      when {
        etag != null -> {
          conditionName = "If-None-Match"
          conditionValue = etag
        }

        lastModified != null -> {
          conditionName = "If-Modified-Since"
          conditionValue = lastModifiedString
        }

        servedDate != null -> {
          conditionName = "If-Modified-Since"
          conditionValue = servedDateString
        }

        else -> return CacheStrategy(request, null) // No condition! Make a regular request.
      }

      val conditionalRequestHeaders = request.headers.newBuilder()
      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      return CacheStrategy(conditionalRequest, cacheResponse)
    }

从LRUCache中获取到缓存的应答数据,将会做如下的Header的校验。

先来看看CacheStagty的初始化:

    init {
      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
        val headers = cacheResponse.headers
        for (i in 0 until headers.size) {
          val fieldName = headers.name(i)
          val value = headers.value(i)
          when {
            fieldName.equals("Date", ignoreCase = true) -> {
              servedDate = value.toHttpDateOrNull()
              servedDateString = value
            }
            fieldName.equals("Expires", ignoreCase = true) -> {
              expires = value.toHttpDateOrNull()
            }
            fieldName.equals("Last-Modified", ignoreCase = true) -> {
              lastModified = value.toHttpDateOrNull()
              lastModifiedString = value
            }
            fieldName.equals("ETag", ignoreCase = true) -> {
              etag = value
            }
            fieldName.equals("Age", ignoreCase = true) -> {
              ageSeconds = value.toNonNegativeInt(-1)
            }
          }
        }
      }
    }

分别取出了缓存头部中的:

  • 1.Date 缓存应答的发送到客户端的时间
  • 2.Expires 代表应答可以存活时间
  • 3.Last-Modified 代表服务器用来校验客户端的请求是否是最新的时间,不是则返回304
  • 4.ETag 用于记录当前请求页面状态时效的token
  • 5.Age 代理服务器用自己去缓存应答的时候,该头部代表从诞生到现在多长时间

获得这些基础数据后,上面的compute就是根据这5个标志位进行计算。

  • 1.cacheResponseAge 计算这个缓存真正缓存时间方式:

接受消耗的时间 = max(Response抵达客户端的时间 - Response从服务端发出的时间(Date字段),Age字段,0)
Response来回时间 = Response抵达客户端的时间 - 客户端发送的时间
缓存时间 = 当前时间 - Response抵达客户端的时间
当前应答真实缓存时间 = 接受消耗的时间 + Response来回时间 + 缓存时间

  • 2.computeFreshnessLifetime 遵循如下的逻辑计算缓存有效性时间段

优先取出CacheControl的maxAge 字段,存在则返回
其次取出expires 字段,expires - (Date字段 或者 Response抵达客户端时间)
最后取出lastModified字段, (Date字段 或者 Response抵达客户端时间) - lastModified(上次修改)

这样计算差值就能大致获得这个缓存比较精确的有效时间。

  • 3.cacheResponseAge 获取到的缓存的时间和 computeFreshnessLifetime计算出来的缓存时效性,以及在okhttp设置的最小缓存minFreshSeconds时效,通过下面简单的计算:

cacheResponseAge + minFreshSeconds > computeFreshnessLifetime

就能知道当前缓存是否有效,从而决定是否返回一个CacheResponse上去。

CacheInterceptor 缓存拦截器进行网络请求后的处理

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (networkResponse?.code == HTTP_NOT_MODIFIED) {
        val response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers, networkResponse.headers))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis)
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis)
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build()

        networkResponse.body!!.close()

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache!!.trackConditionalCacheHit()
        cache.update(cacheResponse, response)
        return response.also {
          listener.cacheHit(call, it)
        }
      } else {
        cacheResponse.body?.closeQuietly()
      }
    }

    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()

    if (cache != null) {
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        val cacheRequest = cache.put(response)
        return cacheWritingResponse(cacheRequest, response).also {
          if (cacheResponse != null) {
            // This will log a conditional cache miss only.
            listener.cacheMiss(call)
          }
        }
      }

      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }

    return response
  • 1.如果当前缓存响应体存在,且当前网络请求返回的状态代码为304。在http协议中说明此时请求没有发生变化,那么就会根据缓存响应体构造一个全新的响应体,并更新当前的是时间戳,最后更新了到缓存中,并返回到上层。

  • 2.不为304情况,且缓存策略是允许刷新的,还是会把当前成功的响应体保存到Cache中最后返回。

来看看如何判断那些响应体可以进行缓存:

    fun isCacheable(response: Response, request: Request): Boolean {
      // Always go to network for uncacheable response codes (RFC 7231 section 6.1), This
      // implementation doesn't support caching partial content.
      when (response.code) {
        HTTP_OK,
        HTTP_NOT_AUTHORITATIVE,
        HTTP_NO_CONTENT,
        HTTP_MULT_CHOICE,
        HTTP_MOVED_PERM,
        HTTP_NOT_FOUND,
        HTTP_BAD_METHOD,
        HTTP_GONE,
        HTTP_REQ_TOO_LONG,
        HTTP_NOT_IMPLEMENTED,
        StatusLine.HTTP_PERM_REDIRECT -> {
          // These codes can be cached unless headers forbid it.
        }

        HTTP_MOVED_TEMP,
        StatusLine.HTTP_TEMP_REDIRECT -> {
          // These codes can only be cached with the right response headers.
          // http://tools.ietf.org/html/rfc7234#section-3
          // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
          if (response.header("Expires") == null &&
              response.cacheControl.maxAgeSeconds == -1 &&
              !response.cacheControl.isPublic &&
              !response.cacheControl.isPrivate) {
            return false
          }
        }

        else -> {
          // All other codes cannot be cached.
          return false
        }
      }

      // A 'no-store' directive on request or response prevents the response from being cached.
      return !response.cacheControl.noStore && !request.cacheControl.noStore
    }
  }
  • 1.302,307状态码状态下,需要判断获取头部中的Expires数值,获取不存在。并且Cache-Control头部中max-age为-1,且publicprivate都存在的时候。

Expires和max-age 说明了有效期为无限长,public说明http通信的过程中,包括请求的发起方、代理缓存服务器都可以进行缓存。private说明http通信过程中只有请求方可以缓存。

  • 2.如果状态码为200,203,204,300,301,404,405,410,414,501,308都可以缓存。

  • 2.其他情况都返回false,不允许缓存

总结

到这里就完成了对Okhttp头三层协议的解析,能看到实际上头三层主要处理的是Http协议中状态码所对应的行为。

Http响应状态码大致上可以分为如下几种情况:

2XX 代表请求成功

200,203,204 代表请求成功,可以对响应数据进行缓存

30X 代表资源发生变动或者没有变动

  • 300 是指有多种选择。请求的资源包含多个位置,此时请求也可以看作成功,此时也会进行缓存起来。此时也会记录下需要跳转Header中的Location,并重新设置为全新的跳转url。记住这个过程是先执行了缓存拦截器后,再执行跳转拦截器。

  • 301 请求的资源已经永久移动了 会自动重定向。此时还是一样会缓存当前的结果后,尝试获取Location的url 进行重定向(Http 1.0内容),不允许重定向时候改变请求方式(如get转化成post)

  • 302 代表临时移动的资源,所以没有特殊处理并不会缓存结果,因为这个响应数据很可能时效性很短;但是如果设置了Cache-ControlExpires这些缓存时效头部就会进行缓存,接着会获取Location的url 进行重定向(Http 1.0内容),不允许重定向时候改变请求方式(如get转化成post)

  • 303 代表查看其他资源,而这个过程可以不视作一个正常的响应结果,也因为允许改变请求方式;因此也不会进行缓存,接着会获取Location的url 进行重定向.

  • 304 代表资源没有发生变动,且缓存策略是允许刷新的。那么就说明服务器这段时间内对这个请求的应答没有变化,客户端直接从缓存获取即可。此时客户端就会从缓存拦截器中的缓存对象获取缓存好的响应信息。

  • 307 同302 也是一个临时移动资源的标志位,不同的是这是来自Http 1.1协议。为什么出现一个一样的呢?因为302在很多浏览器的实现是允许改变请求方式,因此307强制规定不允许改变

  • 308 同301 是一个永久移动的资源路径,来自Http 1.1.原因也是因为强制规范不允许改变请求方式,但是允许进行缓存。

4XX 客户端异常或者客户端需要特殊处理

  • 401 请求要求用户的身份认证。这个过程就会获取设置在Authenticator 中的账号密码,添加到头部中重试这个请求。

  • 403 代表拒绝访问,okhttp不会做任何处理直接返回

  • 404 代表客户端请求异常,说明这个url的请求状态有问题,okhttp也会进行缓存学习,下一次再一次访问的时候就会直接返回异常。

  • 405 代表当前请求的方式出错了,这个请求不支持这种请求方式

  • 407 和401类似 不过在这里面代表的是使用代理的Authenticator. authenticate 进行账号密码的校验

  • 408 服务器等待客户端发送请求超时处理 状态码408不常见,但是在HAProxy中会比较常见。这种情况说明我们可以重复的进行没有修改过的请求(甚至是非幂等请求),从头部中获取对应的key,从而决定是否立即重试

  • 410 代表资源已经不可用了,此时okhttp也会学习,缓存这个结果直到超过缓存时效。

  • 414 代表请求的URL长度超出了服务器可以处理的长度。很少见这种情况,这种也是数据一种异常,所以okhttp也会获取摘要学习

  • 421 代表客户端所在的ip地址到服务器的连接数超过了服务器最大的连接数。此时还是有机会进行重新请求,因为在Http 2.0协议中允许流的复用。

5XX 服务端异常

  • 500 服务端出现了无法处理的错误,直接报错了。 这种情况不会做处理,直接抛出错误即可

  • 501 服务端此时不支持请求所需要的功能,服务器无法识别请求的方法,并且无法支持对任何资源的请求。 这种错误okhhtp可以缓存学习,因为是服务器的web系统需要升级了。

  • 503 服务器过载,暂时不处理。一般会带上Retry-After 告诉客户端延时多少时间之后再次请求。然而okhttp不会做延时处理,而是交给开发者处理,他只会处理Retry-After 为0的情况,也就是立即处理

  • 504 一般是指网关超时,注意如果okhttp禁止了网络请求和缓存也会返回504

retryAndFollowUpInterceptor

主要处理了如下几个方向的问题:

  • 1.异常,或者协议重试(408客户端超时,权限问题,503服务暂时不处理,retry-after为0)
  • 2.重定向
  • 3.重试的次数不能超过20次。

BridgeInterceptor

主要是把Cookie,Content-type设置到头部中。很多时候,初学者会疑惑为什么自己加的头部会失效,就是因为在Application拦截器中处理后,又被BridgeInterceptor 覆盖了。需要使用networkInterceptor

CacheInterceptor

主要是处理304等响应体的缓存。通过DiskLruCache缓存起来。

到这里前三层属于对Http协议处理的拦截器就完成了,接下来几层就是okhttp如何管理链接的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容