Analysis of OkHttp source code (very thin and very long)

Posted by akrocks_extreme on Thu, 16 Dec 2021 18:05:44 +0100

preface

This article is a detailed analysis of the OkHttp open source library. If you feel you don't know enough about OkHttp, you want to learn more. I believe this article will be helpful to you.

This article contains a detailed analysis of the request process, the interpretation of major interceptors and a summary of my reflection. The article is very long. You are welcome to communicate and discuss it together.

usage method

It is very simple to create a OkHttpClient object, a Request object, and then use them to create a Call object, and finally call the synchronous request execute() method or asynchronously request the enqueue() method to get Response.

private final OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder()
      .url("https://github.com/")
      .build();
    //Synchronization request
    Response response = client.newCall(request).execute();
    //todo handle response

    //Asynchronous request
    client.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(@NotNull Call call, @NotNull IOException e) {
      //todo handle request failed
      }

      @Override
      public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
          //todo handle Response
      }
    });

Introduction to basic objects

As described in the usage method, we have built OkHttpClient object, Request object and Call object successively. What are the meanings and functions of these objects? This requires us to further study and understand.

OkHttpClient

A request configuration class adopts the builder mode to facilitate users to configure some request parameters, such as callTimeout, cookie, interceptor, etc.

open class OkHttpClient internal constructor(
  builder: Builder
) : Cloneable, Call.Factory, WebSocket.Factory {

  constructor() : this(Builder())

  class Builder constructor() {
    //Scheduler
    internal var dispatcher: Dispatcher = Dispatcher()
    //Connection pool
    internal var connectionPool: ConnectionPool = ConnectionPool()
    //Overall process interceptor
    internal val interceptors: MutableList<Interceptor> = mutableListOf()
    //Network flow interceptor
    internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()
    //Process listener
    internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory()
    //Do you want to reconnect when the connection fails
    internal var retryOnConnectionFailure = true
    //Server authentication settings
    internal var authenticator: Authenticator = Authenticator.NONE
    //Redirect
    internal var followRedirects = true
    //Redirect from HTTP to HTTPS
    internal var followSslRedirects = true
    //cookie settings
    internal var cookieJar: CookieJar = CookieJar.NO_COOKIES
    //Cache settings
    internal var cache: Cache? = null
    //DNS settings
    internal var dns: Dns = Dns.SYSTEM
    //Proxy settings
    internal var proxy: Proxy? = null
    //Proxy selector settings
    internal var proxySelector: ProxySelector? = null
    //Proxy authentication settings
    internal var proxyAuthenticator: Authenticator = Authenticator.NONE
    //socket configuration
    internal var socketFactory: SocketFactory = SocketFactory.getDefault()
    //https socket configuration
    internal var sslSocketFactoryOrNull: SSLSocketFactory? = null
    internal var x509TrustManagerOrNull: X509TrustManager? = null
    internal var connectionSpecs: List<ConnectionSpec> = DEFAULT_CONNECTION_SPECS
    //agreement
    internal var protocols: List<Protocol> = DEFAULT_PROTOCOLS
    //Domain name verification
    internal var hostnameVerifier: HostnameVerifier = OkHostnameVerifier
    internal var certificatePinner: CertificatePinner = CertificatePinner.DEFAULT
    internal var certificateChainCleaner: CertificateChainCleaner? = null
    //request timeout
    internal var callTimeout = 0
    //connection timed out
    internal var connectTimeout = 10_000
    //Read timeout
    internal var readTimeout = 10_000
    //Write timeout
    internal var writeTimeout = 10_000
    internal var pingInterval = 0
    internal var minWebSocketMessageToCompress = RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE
    internal var routeDatabase: RouteDatabase? = null
    
···Omit code···

Request

The same configuration class for Request parameters also adopts the builder mode. However, compared with OkHttpClient, Request is very simple. There are only four parameters: Request URL, Request method, Request header and Request body.

class Request internal constructor(
  @get:JvmName("url") val url: HttpUrl,
  @get:JvmName("method") val method: String,
  @get:JvmName("headers") val headers: Headers,
  @get:JvmName("body") val body: RequestBody?,
  internal val tags: Map<Class<*>, Any>
) {

  open class Builder {
    //Requested URL
    internal var url: HttpUrl? = null
    //Request methods, such as GET, POST
    internal var method: String
    //Request header
    internal var headers: Headers.Builder
    //Request body
    internal var body: RequestBody? = null
  ···Omit code···

Call

The request calls the interface, indicating that the request is ready to be executed or cancelled. It can only be executed once.

interface Call : Cloneable {
  /** Returns the original request that initiated this call */
  fun request(): Request

  /**
   * Synchronization request, execute immediately.
   * 
   * Two exceptions are thrown:
   * 1. The request fails and throws IOException;
   * 2. Throw IllegalStateException if you execute again on the premise of one execution;*/
  @Throws(IOException::class)
  fun execute(): Response

  /**
   * Asynchronous request, which is scheduled to be executed at a certain point in the future.
   * If you execute again after one execution, throw the IllegalStateException */
  fun enqueue(responseCallback: Callback)

  /** Cancel the request. Completed requests cannot be cancelled */
  fun cancel()

  /** Has it been executed  */
  fun isExecuted(): Boolean

  /** Cancelled   */
  fun isCanceled(): Boolean

  /** The timeout configuration of a complete Call request process is selected from [OkHttpClient.Builder.callTimeout] by default */
  fun timeout(): Timeout

  /** Clone the call and create a new and identical call */
  public override fun clone(): Call

  /** Use factory mode to let OkHttpClient create Call objects */
  fun interface Factory {
    fun newCall(request: Request): Call
  }
}

RealCall

In OkHttpClient, we use the newCall method to create a Call object, but we can see from the source code that the newCall method returns a RealCall object.

OkHttpClient.kt

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

RealCall is the specific implementation class of the Call interface. It is the connection bridge between the application end and the network layer. It shows the original request and connection data of the application end, as well as the response and other data flows returned by the network layer. It can also be seen by using methods that after creating a RealCall object, synchronous or asynchronous request methods will be called, so it also includes synchronous request execute() and asynchronous request enqueue() methods. (detailed analysis later)

AsyncCall

Asynchronous request call is an internal class of RealCall, that is, a Runnable, which is executed by the thread pool in the scheduler.

inner class AsyncCall(
    //Response callback method passed in by user
    private val responseCallback: Callback
  ) : Runnable {
    //The number of requests for the same domain name. volatile + AtomicInteger ensures timely visibility and atomicity under multithreading
    @Volatile var callsPerHost = AtomicInteger(0)
      private set

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

···Omit code···

    fun executeOn(executorService: ExecutorService) {
      client.dispatcher.assertThreadDoesntHoldLock()

      var success = false
      try {
        //Call thread pool execution
        executorService.execute(this)
        success = true
      } catch (e: RejectedExecutionException) {
        val ioException = InterruptedIOException("executor rejected")
        ioException.initCause(e)
        noMoreExchanges(ioException)
        //The request failed, call callback Onfailure() method
        responseCallback.onFailure(this@RealCall, ioException)
      } finally {
        if (!success) {
          //The request failed. Call the scheduler finish method
          client.dispatcher.finished(this) // This call is no longer running!
        }
      }
    }

    override fun run() {
      threadName("OkHttp ${redactedUrl()}") {
        var signalledCallback = false
        timeout.enter()
        try {
          //The request is successful. Get the response returned by the server
          val response = getResponseWithInterceptorChain()
          signalledCallback = true
          //Call callback Onresponse () method to pass out the response
          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 {
            //The request failed, call callback Onfailure() method
            responseCallback.onFailure(this@RealCall, e)
          }
        } catch (t: Throwable) {
          //If an exception occurs in the request, call the cancel method to cancel the request
          cancel()
          if (!signalledCallback) {
            val canceledException = IOException("canceled due to $t")
            canceledException.addSuppressed(t)
            //The request failed, call callback Onfailure() method
            responseCallback.onFailure(this@RealCall, canceledException)
          }
          throw t
        } finally {
          //When the request ends, call the scheduler finish method
          client.dispatcher.finished(this)
        }
      }
    }
  }

Dispatcher

The scheduler is used to schedule Call objects, including thread pool and asynchronous request queue, and is used to store and execute AsyncCall objects.

class Dispatcher constructor() {
  @get:Synchronized
  @get:JvmName("executorService") val executorService: ExecutorService
    get() {
      if (executorServiceOrNull == null) {
        //Create a cache thread pool to handle request calls
        executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
            SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
      }
      return executorServiceOrNull!!
    }

  /** Prepared asynchronous request queue */
  @get:Synchronized
  private val readyAsyncCalls = ArrayDeque<AsyncCall>()

  /** A running asynchronous request queue that contains asynccalls that have been cancelled but have not been completed */
  private val runningAsyncCalls = ArrayDeque<AsyncCall>()

  /** The running synchronization request queue contains realcalls that have been cancelled but have not been completed */
  private val runningSyncCalls = ArrayDeque<RealCall>()

···Omit code···
}

To sum up

objecteffect
CallThe request calls the interface, indicating that the request is ready for execution or can be cancelled. It can only be executed once.
RealCallThe specific implementation class of Call interface is the connection bridge between application and network layer, including OkHttpClient and Request information.
AsyncCallAn asynchronous request call is actually a Runnable and will be placed in the thread pool for processing.
DispatcherThe scheduler is used to schedule Call objects, including thread pool and asynchronous request queue, and is used to store and execute AsyncCall objects.
RequestRequest class, including url, method, headers and body.
ResponseResponse data returned by the network layer.
CallbackResponse callback function interface, including onFailure and onResponse methods.

Process analysis

After introducing the object, let's take a look at the source code according to the use method.

Synchronization request

How to use synchronization requests.

client.newCall(request).execute();

The newCall method creates a RealCall object and executes its execute() method.

  RealCall.kt
  
  override fun execute(): Response {
    //CAS judges whether it has been executed and ensures that it can only be executed once. If it has been executed, an exception will be thrown
    check(executed.compareAndSet(false, true)) { "Already Executed" }

    //Request timeout start timing
    timeout.enter()
    //Enable request listening
    callStart()
    try {
      //The executed() method in the scheduler is called, and the scheduler just adds the call to the runningSyncCalls queue
      client.dispatcher.executed(this)
      //Call the getResponseWithInterceptorChain method to get the response
      return getResponseWithInterceptorChain()
    } finally {
      //After execution, the scheduler removes the call from the runningSyncCalls queue
      client.dispatcher.finished(this)
    }
  }

Calling the scheduler executed method is to add the current RealCall object to the runningSyncCalls queue, and then call the getResponseWithInterceptorChain method to get response.

Asynchronous request

Now let's look at asynchronous requests.

  RealCall.kt

  override fun enqueue(responseCallback: Callback) {
    //CAS judges whether it has been executed and ensures that it can only be executed once. If it has been executed, an exception is thrown
    check(executed.compareAndSet(false, true)) { "Already Executed" }
    //Enable request listening
    callStart()
    //Create a new AsyncCall object and add it to the readyAsyncCalls queue through the scheduler enqueue method
    client.dispatcher.enqueue(AsyncCall(responseCallback))
  }

Then call the scheduler's enqueue method.

  Dispatcher.kt
  
  internal fun enqueue(call: AsyncCall) {
    //Lock to ensure thread safety
    synchronized(this) {
      //Add the request call to the readyAsyncCalls queue
      readyAsyncCalls.add(call)

      // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
      // the same host.
      if (!call.call.forWebSocket) {
        //Use the domain name to find whether there are requests for the same domain name, and reuse if there are.
        val existingCall = findExistingCallWithHost(call.host)
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
      }
    }
    //Execute request
    promoteAndExecute()
  }


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

    val executableCalls = mutableListOf<AsyncCall>()
    //Determine whether a request is being executed
    val isRunning: Boolean
    //Lock to ensure thread safety
    synchronized(this) {
      //Traverse readyAsyncCalls queue
      val i = readyAsyncCalls.iterator()
      while (i.hasNext()) {
        val asyncCall = i.next()
        //The number of runningAsyncCalls cannot be greater than the maximum number of concurrent requests 64
        if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
        //The maximum number of requests in the same domain name is 5. A maximum of 5 threads are allowed to execute requests at the same time in the same domain name
        if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

        //Remove from the readyAsyncCalls queue and add to the executableCalls and runningAsyncCalls queues
        i.remove()
        asyncCall.callsPerHost.incrementAndGet()
        executableCalls.add(asyncCall)
        runningAsyncCalls.add(asyncCall)
      }
      //Whether a request is executing is determined by the number of requests in the run queue
      isRunning = runningCallsCount() > 0
    }

    //Traverse the executable queue and call the thread pool to execute AsyncCall
    for (i in 0 until executableCalls.size) {
      val asyncCall = executableCalls[i]
      asyncCall.executeOn(executorService)
    }

    return isRunning
  }

The enqueue method of the scheduler is to add AsyncCall to the readyAsyncCalls queue and then invoke the promoteAndExecute method to execute the request. The promoteAndExecute method is actually traversing the readyAsyncCalls queue, then executing the qualified request with the thread pool, that is, AsyncCall. will be executed. Run() method.

See the specific code of AsyncCall method Basic object introduction AsyncCall , not shown here. To put it simply, call the getResponseWithInterceptorChain method to get the response, and then call back The onresponse method is passed out. On the contrary, if the request fails and an exception is caught, it will pass the callback Onfailure passes exception information. Finally, the request ends and the scheduler finish method is called.

  Dispatcher.kt

  /** Asynchronous request call end method */
  internal fun finished(call: AsyncCall) {
    call.callsPerHost.decrementAndGet()
    finished(runningAsyncCalls, call)
  }

  /** Synchronous request call end method */
  internal fun finished(call: RealCall) {
    finished(runningSyncCalls, call)
  }

  private fun <T> finished(calls: Deque<T>, call: T) {
    val idleCallback: Runnable?
    synchronized(this) {
      //Removes the current request call from the running queue
      if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
      idleCallback = this.idleCallback
    }

    //Continue to execute the remaining requests, take the call from readyAsyncCalls, add it to runningAsyncCalls, and then execute
    val isRunning = promoteAndExecute()

    if (!isRunning && idleCallback != null) {
      //If all requests are executed and are idle, the idle callback method is called
      idleCallback.run()
    }
  }

Get Response

The next step is to see how the getResponseWithInterceptorChain method gets the response.

  internal fun getResponseWithInterceptorChain(): Response {
    //Interceptor list
    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)

    //Build interceptor responsibility chain
    val chain = RealInterceptorChain(
        call = this,
        interceptors = interceptors,
        index = 0,
        exchange = null,
        request = originalRequest,
        connectTimeoutMillis = client.connectTimeoutMillis,
        readTimeoutMillis = client.readTimeoutMillis,
        writeTimeoutMillis = client.writeTimeoutMillis
    )
    //If the call request is completed, it means that the interaction is completed and there is no more to exchange
    var calledNoMoreExchanges = false
    try {
      //Execute the interceptor responsibility chain to obtain the response
      val response = chain.proceed(originalRequest)
      //If cancelled, close the response and throw an exception
      if (isCanceled()) {
        response.closeQuietly()
        throw IOException("Canceled")
      }
      return response
    } catch (e: IOException) {
      calledNoMoreExchanges = true
      throw noMoreExchanges(e) as Throwable
    } finally {
      if (!calledNoMoreExchanges) {
        noMoreExchanges(null)
      }
    }
  }

A brief summary: the responsibility chain design pattern is adopted here. The real interceptorchain responsibility chain is constructed through the interceptor, and then the processed method is executed to obtain the response.

So, what is the interceptor? What is the interceptor responsibility chain?

Interceptor

Only one interceptor method is declared and implemented in the subclass. It also contains a Chain interface. The core method is to process the request to obtain the response.

fun interface Interceptor {
  /** interceptor method  */
  @Throws(IOException::class)
  fun intercept(chain: Chain): Response

  interface Chain {
    /** Original request data */
    fun request(): Request

    /** The core method is to process the request and obtain the response */
    @Throws(IOException::class)
    fun proceed(request: Request): Response
    
    fun connection(): Connection?

    fun call(): Call

    fun connectTimeoutMillis(): Int

    fun withConnectTimeout(timeout: Int, unit: TimeUnit): Chain

    fun readTimeoutMillis(): Int

    fun withReadTimeout(timeout: Int, unit: TimeUnit): Chain

    fun writeTimeoutMillis(): Int

    fun withWriteTimeout(timeout: Int, unit: TimeUnit): Chain
  }
}

RealInterceptorChain

Interceptor chain is to implement interceptor The chain interface focuses on the processed method of replication.

class RealInterceptorChain(
  internal val call: RealCall,
  private val interceptors: List<Interceptor>,
  private val index: Int,
  internal val exchange: Exchange?,
  internal val request: Request,
  internal val connectTimeoutMillis: Int,
  internal val readTimeoutMillis: Int,
  internal val writeTimeoutMillis: Int
) : Interceptor.Chain {

···Omit code···
  private var calls: Int = 0
  override fun call(): Call = call
  override fun request(): Request = request

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

    calls++

    if (exchange != null) {
      check(exchange.finder.sameHostAndPort(request.url)) {
        "network interceptor ${interceptors[index - 1]} must retain the same host and port"
      }
      check(calls == 1) {
        "network interceptor ${interceptors[index - 1]} must call proceed() exactly once"
      }
    }

    //index+1, copy and create a new responsibility chain, which means calling the next handler in the responsibility chain, that is, the next interceptor
    val next = copy(index = index + 1, request = request)
    //Remove the current interceptor
    val interceptor = interceptors[index]

    //Execute the interception method of the current interceptor
    @Suppress("USELESS_ELVIS")
    val response = interceptor.intercept(next) ?: throw NullPointerException(
        "interceptor $interceptor returned null")

    if (exchange != null) {
      check(index + 1 >= interceptors.size || next.calls == 1) {
        "network interceptor $interceptor must call proceed() exactly once"
      }
    }

    check(response.body != null) { "interceptor $interceptor returned a response with no body" }

    return response
  }
}

The chain call will eventually execute each interceptor in the interceptor list and return the Response.

Interceptor

OK, it's time to look at the specific interceptors in the interceptor list.

First, a summary of various interceptors is given, in order:

  1. client.interceptors: This is set by developers. The earliest interceptors will be processed before all interceptors are processed. It can be used to add some public parameters, such as custom header, custom log, etc.
  2. RetryAndFollowUpInterceptor: some initialization work will be done for the connection, as well as the retry work for the failed request and the subsequent request work for redirection. Like his name, it does retry work and some connection tracking work.
  3. BridgeInterceptor: it is the communication bridge between the client and the server. It is responsible for converting the user built request into the request required by the server, and converting the response returned from the network request into the response available to the user.
  4. CacheInterceptor: This is mainly the cache related processing. According to the cache configuration defined by the user in OkHttpClient, a new cache policy will be created in combination with the request to determine whether to use the network or cache to build the response.
  5. ConnectInterceptor: it is mainly responsible for establishing connections. TCP connections or TLS connections will be established.
  6. client. Network interceptors: This is also set up by the developers themselves, so it is essentially similar to the first interceptor, but its use is different due to different locations.
  7. CallServerInterceptor: here is the request and response of network data, that is, the actual network I/O operation, sending the request header and request body to the server, and parsing the response returned by the server.

Next, we will interpret these interceptors one by one from top to bottom.

client.interceptors

This is a user-defined interceptor, called an application interceptor, and will be saved in the interceptors: List < interceptor > list of OkHttpClient. It is the first interceptor in the interceptor responsibility chain, that is, it will be the first to execute the interception method. We can add custom Header information through it, such as:

class HeaderInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request().newBuilder()
                .addHeader("device-android", "xxxxxxxxxxx")
                .addHeader("country-code", "ZH")
                .build();
        return chain.proceed(request);
    }
}

//Then add OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(60, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    .writeTimeout(15, TimeUnit.SECONDS)
    .cookieJar(new MyCookieJar())
    .addInterceptor(new HeaderInterceptor())//Add custom Header interceptor
    .build();

RetryAndFollowUpInterceptor

The second interceptor, known from its name, is responsible for the retry of failed requests and subsequent requests for redirection. At the same time, it will initialize the connection.

class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {

  @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) {
      //An exchange Finder will be created here, and ConnectInterceptor will use it
      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) {
          //The attempt to connect via routing failed. The request will not be sent.
          if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
            throw e.firstConnectException.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e.firstConnectException
          }
          newExchangeFinder = false
          continue
        } catch (e: IOException) {
          //The attempt to communicate with the 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
        }

        // Attach the prior response if it exists. Such responses never have a body.
        //Try to associate the previous response. Note: body is null
        if (priorResponse != null) {
          response = response.newBuilder()
              .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
              .build()
        }

        val exchange = call.interceptorScopedExchange
        //According to the responseCode, a new request will be constructed and returned for retry or redirection
        val followUp = followUpRequest(response, exchange)

        if (followUp == null) {
          if (exchange != null && exchange.isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }
        //If the request body is one-time, you do not need to retry again
        val followUpBody = followUp.body
        if (followUpBody != null && followUpBody.isOneShot()) {
          closeActiveExchange = false
          return response
        }

        response.body?.closeQuietly()

        //The maximum number of retries is different for different browsers. For example, Chrome is 21 and Safari is 16
        if (++followUpCount > MAX_FOLLOW_UPS) {
          throw ProtocolException("Too many follow-up requests: $followUpCount")
        }

        request = followUp
        priorResponse = response
      } finally {
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
  }

  /** Judge whether to reconnect. False - > do not attempt to reconnect; True - > try reconnection.*/
  private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    //The client cannot retry
    if (!client.retryOnConnectionFailure) return false

    //The request body cannot be sent again
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

    //The exception occurred is fatal and cannot be recovered, such as ProtocolException
    if (!isRecoverable(e, requestSendStarted)) return false

    //There is no more way to try reconnection
    if (!call.retryAfterFailure()) return false

    // For failed recovery, use the same route selector with the new connection
    return true
  }
···Omit code··· 

BridgeInterceptor

As can be seen from its name, it is positioned as a communication bridge between the client and the server. It is responsible for converting the requests built by the user into the requests required by the server, such as adding content type, adding cookies, adding user agent, etc. Then do some processing to convert the response returned by the server into the response required by the client. For example, remove content encoding, content length, and so on from the response header.

class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    //Get original request data
    val userRequest = chain.request()
    val requestBuilder = userRequest.newBuilder()
    //Rebuild the request header and request body information
    val body = userRequest.body

    val contentType = body.contentType()
    requestBuilder.header("Content-Type", contentType.toString())
    requestBuilder.header("Content-Length", contentLength.toString())
    requestBuilder.header("Transfer-Encoding", "chunked")
    requestBuilder.header("Host", userRequest.url.toHostHeader())
    requestBuilder.header("Connection", "Keep-Alive")

   ···Omit code···
   
    //Add cookie
    val cookies = cookieJar.loadForRequest(userRequest.url)
    if (cookies.isNotEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies))
    }
    //Add user agent
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", userAgent)
    }
    //Rebuild a Request, and then execute the next interceptor to process the Request
    val networkResponse = chain.proceed(requestBuilder.build())

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

    //Create a new responseBuilder to build the original request data into the response
    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()
        //Modify the response header information and remove the content encoding and content length information
        responseBuilder.headers(strippedHeaders)
        val contentType = networkResponse.header("Content-Type")
        //Modify the response body information
        responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
      }
    }
    
    return responseBuilder.build()
   
···Omit code···

CacheInterceptor

Users can use okhttpclient Cache is used to configure the cache. The cache interceptor determines whether to use the network or cache to build the response through CacheStrategy.

class CacheInterceptor(internal val cache: Cache?) : Interceptor {

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    //From okhttpclient.com via request Get cache from cache
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()
    //Create a cache policy to determine how to use the cache
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    //If it is blank, it means that the network is not used; otherwise, it means that the network is used
    val networkRequest = strategy.networkRequest
    //If it is empty, the cache will not be used; otherwise, the cache will be used
    val cacheResponse = strategy.cacheResponse
    //Track network and cache usage
    cache?.trackResponse(strategy)
    val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE
    //Cache available but not applicable, close it
    if (cacheCandidate != null && cacheResponse == null) {
      cacheCandidate.body?.closeQuietly()
    }

    //If the network is disabled but the cache is empty, build a response with code 504 and return
    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 disable the network, do not use the network, and have a cache, we can directly build and return the response according to the cache content
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build().also {
            listener.cacheHit(call, it)
          }
    }
    //Add listener for cache
    if (cacheResponse != null) {
      listener.cacheConditionalHit(call, cacheResponse)
    } else if (cache != null) {
      listener.cacheMiss(call)
    }

    var networkResponse: Response? = null
    try {
      //The responsibility chain is processed downward, and the response returned from the server is assigned to the networkResponse
      networkResponse = chain.proceed(networkRequest)
    } finally {
      //When I/O or other exceptions are caught, the request fails, the networkResponse is empty, and there is a cache, the cache content is not exposed.
      if (networkResponse == null && cacheCandidate != null) {
        cacheCandidate.body?.closeQuietly()
      }
    }

    //If there is a cache
    if (cacheResponse != null) {
      //When the response code returned by the network is 304, a new Response return is constructed using the cached content.
      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 {
        //Otherwise, the cached response body is turned off
        cacheResponse.body?.closeQuietly()
      }
    }

    //Build response of network request
    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()

    //If the cache is not null, that is, the user has configured the cache in OkHttpClient, save the network request response newly built in the previous step into the cache
    if (cache != null) {
      //According to the response code,header and CacheControl Nostore to determine whether it can be cached
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // Store the response in the cache
        val cacheRequest = cache.put(response)
        return cacheWritingResponse(cacheRequest, response).also {
          if (cacheResponse != null) {
            listener.cacheMiss(call)
          }
        }
      }
      //Judge whether the cache is valid according to the request method. Only Get requests are cached, and requests from other methods are removed
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          //The cache is invalid. Remove the request cache from the client cache configuration
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }

    return response
  }
  
···Omit code···  

ConnectInterceptor

Responsible for establishing a real connection with the server,

object ConnectInterceptor : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    //Initialize an exchange object
    val exchange = realChain.call.initExchange(chain)
    //Create a new connection responsibility chain based on the exchange object
    val connectedChain = realChain.copy(exchange = exchange)
    //Execute this link chain of responsibility
    return connectedChain.proceed(realChain.request)
  }
}

Once swept down, the code is very simple, and there are only three steps in the interception method.

  1. Initialize an exchange object.
  2. Then create a new connection responsibility chain based on the exchange object.
  3. Execute the link responsibility chain.

What is the exchange object?

RealCall.kt

internal fun initExchange(chain: RealInterceptorChain): Exchange {
    ...Omit code...
    //The exchangeFinder here is created in the RetryAndFollowUpInterceptor
    val exchangeFinder = this.exchangeFinder!!
    //Returns an exchange codec (an encoder that encodes a request and decodes a response)
    val codec = exchangeFinder.find(client, chain)
    //Build a new Exchange object according to exchangeFinder and codec, and return
    val result = Exchange(this, eventListener, exchangeFinder, codec)
  ...Omit code...
    return result
  }

See exchange finder Find(),

ExchangeFinder.kt

fun find(
    client: OkHttpClient,
    chain: RealInterceptorChain
  ): ExchangeCodec {
    try {
      //Find qualified and available connections and return a RealConnection object
      val resultConnection = findHealthyConnection(
          connectTimeout = chain.connectTimeoutMillis,
          readTimeout = chain.readTimeoutMillis,
          writeTimeout = chain.writeTimeoutMillis,
          pingIntervalMillis = client.pingIntervalMillis,
          connectionRetryEnabled = client.retryOnConnectionFailure,
          doExtensiveHealthChecks = chain.request.method != "GET"
      )
      //According to the connection, create and return a request response encoder: Http1ExchangeCodec or Http2ExchangeCodec, corresponding to Http1 protocol and Http2 protocol respectively
      return resultConnection.newCodec(client, chain)
    } catch (e: RouteException) {
      trackFailure(e.lastConnectException)
      throw e
    } catch (e: IOException) {
      trackFailure(e)
      throw RouteException(e)
    }
  }

Continue to look at the findHealthyConnection method

ExchangeFinder.kt

  private fun findHealthyConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    doExtensiveHealthChecks: Boolean
  ): RealConnection {
    while (true) {
      //Important: find connections
      val candidate = findConnection(
          connectTimeout = connectTimeout,
          readTimeout = readTimeout,
          writeTimeout = writeTimeout,
          pingIntervalMillis = pingIntervalMillis,
          connectionRetryEnabled = connectionRetryEnabled
      )
      //Check whether the connection is qualified and available. If it is qualified, return to the connection directly
      if (candidate.isHealthy(doExtensiveHealthChecks)) {
        return candidate
      }
      //If the connection is unqualified, it is marked as unavailable and removed from the connection pool
      candidate.noNewExchanges()
    ...Omit code...
    }
  }


To sum up, use the findConnection method to find the connection. After finding the connection, judge whether it is qualified and available. If it is qualified, it will directly return to the connection.

So the core method is findConnection. Let's continue to take a deep look at this method:

private fun findConnection(
    connectTimeout: Int, 
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    if (call.isCanceled()) throw IOException("Canceled")

    //For the first time, try to reconnect the connection in the call. You don't need to get the connection again
    val callConnection = call.connection // This may be mutated by releaseConnectionNoEvents()!
    if (callConnection != null) {
      var toClose: Socket? = null
      synchronized(callConnection) {
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          toClose = call.releaseConnectionNoEvents()
        }
      }

      //If the connection in call has not been released, reuse it.
      if (call.connection != null) {
        check(toClose == null)
        return callConnection
      }

      //If the connection in the call has been released, close the Socket
      toClose?.closeQuietly()
      eventListener.connectionReleased(call, callConnection)
    }

    //A new connection is needed, so reset some states
    refusedStreamCount = 0
    connectionShutdownCount = 0
    otherFailureCount = 0

    //The second time, try to get a connection from the connection pool without routing and multiplexing
    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      val result = call.connection!!
      eventListener.connectionAcquired(call, result)
      return result
    }

    //The connection pool is empty. Prepare the route for the next connection attempt
    val routes: List<Route>?
    val route: Route
    
    ...Omit code...

      //The third time, try to get a connection from the connection pool again, with routing and without multiplexing
      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        eventListener.connectionAcquired(call, result)
        return result
      }

      route = localRouteSelection.next()
    }

    //The fourth time, manually create a new connection
    val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    call.client.routeDatabase.connected(newConnection.route())

    //The fifth time, try to get a connection from the connection pool again, with routing and multiplexing.
    //This step is mainly for verification. For example, if there is already a connection, it can be reused directly without using the new connection created manually.
    if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
      val result = call.connection!!
      nextRouteToTry = route
      newConnection.socket().closeQuietly()
      eventListener.connectionAcquired(call, result)
      return result
    }

    synchronized(newConnection) {
      //Put the manually created new connection into the connection pool
      connectionPool.put(newConnection)
      call.acquireConnectionNoEvents(newConnection)
    }

    eventListener.connectionAcquired(call, newConnection)
    return newConnection
  }

It can be seen from the code that a total of 5 attempts have been made to get the connection:

  1. For the first time, try to reconnect the connection in the call without having to re acquire the connection.
  2. The second time, try to get a connection from the connection pool without routing and multiplexing.
  3. The third time, try to get a connection from the connection pool again, with routing and without multiplexing.
  4. The fourth time, manually create a new connection.
  5. The fifth time, try to get a connection from the connection pool again, with routing and multiplexing.

OK, at this point, even if the connection is established.

client.networkInterceptors

This interceptor is called a network interceptor, which is similar to the client Like interceptors, they are also defined by the user and also exist in OkHttpClient in the form of a list.

What's the difference between the two interceptors?

In fact, the difference between the two is due to their different positions. The application interceptor is in the first position, so it will be executed anyway and only once. The network interceptor is in the penultimate position. It may not be executed and may be executed many times. For example, when RetryAndFollowUpInterceptor fails or CacheInterceptor directly returns to the cache, our network interceptor will not be executed.

CallServerInterceptor

Here, the client and the server have established a connection, and then send the request header and request body to the server and parse the response returned by the server.

class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val exchange = realChain.exchange!!
    val request = realChain.request
    val requestBody = request.body
    var invokeStartEvent = true
    var responseBuilder: Response.Builder? = null
    try {
      //Write request header
      exchange.writeRequestHeaders(request)
      //If it is not a GET request and the request body is not empty
      if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
        //When the request header is "expect: 100 continue", you need to wait for the server to return the response of "HTTP/1.1 100 Continue" before sending the request body. If you do not wait for the response, the request body will not be sent.
        //For POST request, first send the request header, and then continue to send the request body after obtaining the 100 continue status
        if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
          //Refresh request, that is, send the request header
          exchange.flushRequest()
          //Parse response header
          responseBuilder = exchange.readResponseHeaders(expectContinue = true)
          exchange.responseHeadersStart()
          invokeStartEvent = false
        }
        //Write request body
        if (responseBuilder == null) {
          if (requestBody.isDuplex()) {
            //If the request body is a dual public body, send the request header first and then send the request body later
            exchange.flushRequest()
            val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
            //Write request body
            requestBody.writeTo(bufferedRequestBody)
          } else {
            //If the "expect: 100 continue" response is obtained, write the request body
            val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
            requestBody.writeTo(bufferedRequestBody)
            bufferedRequestBody.close()
          }
       ···Omit code···
        //End of request, send request body
        exchange.finishRequest()
    ···Omit code···

    try {
      if (responseBuilder == null) {
        //Read response header
        responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
        ···Omit code···
      //Build a response
      var response = responseBuilder
          .request(request)
          .handshake(exchange.connection.handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build()
      var code = response.code
      ···Omit code···
      return response

···Omit code···

Briefly summarize: write the send request header, and then write the send request body according to the conditions, and the request ends. Parse the request header returned by the server, then build a new response and return. Here, CallServerInterceptor is the last interceptor in the interceptor responsibility chain, so it will not call chain Instead of going down, the processed () method passes the constructed response up to each interceptor in the responsibility chain.

summary

We analyzed the request process, including synchronous request and asynchronous request, and carefully analyzed each interceptor in the interceptor responsibility chain. Now draw a flow chart and briefly summarize it. You can go through the process by referring to the flow chart.

reflect

Design pattern

  1. Builder mode: Builder mode is used in OkHttpClient, Request or Response. Because there are many parameters in these classes, users need to select the required parameters to Build the desired instance. Therefore, Build mode is very common in open source libraries.
  2. Factory method pattern: helps to generate complex objects, such as okhttpclient Newcall (request request) to create a Call object.
  3. Responsibility chain mode: This is wonderful. Seven interceptors form the interceptor responsibility chain, and then execute it from top to bottom in order. After obtaining the Response, upload it back from bottom to top.

Thread safety

The callsPerHost variable in the AsyncCall class is decorated with Volatile + AtomicInteger to ensure thread safety under multithreading.

inner class AsyncCall(
    private val responseCallback: Callback
  ) : Runnable {
    //The number of requests for the same domain name. volatile + AtomicInteger ensures timely visibility and atomicity under multithreading
    @Volatile var callsPerHost = AtomicInteger(0)
      private set
    ...Omit code...

data structure

Why does readyAsyncCalls runningAsyncCalls runningSyncCalls use ArrayDeque?

Two answers: first, they are used to store network requests. These requests need to be first come, first served, so queues are used. 2, According to the code, when enqueue is executed, we need to traverse readyAsyncCalls and add the calls that meet the execution conditions to runningAsyncCalls. Compared with the linked list, the search efficiency of the array is higher, so ArrayDeque is adopted.

ending

So far, the source code analysis of OkHttp is introduced.

In fact, the best way to learn the source code is to clone the code, and then face the use method, follow the process step by step.

In fact, the biggest purpose of sharing articles is to wait for someone to point out my mistakes. If you find any mistakes, please point them out without reservation and ask for advice with an open mind. In addition, if you think the article is good and helpful to you, please give me a praise and be encouraged. Thank you ~ Peace ~!

Video:
Senior architects explain the OkHttp of selected high-frequency interview questions of Android manufacturers one by one
Zero foundation for Android development OkHttp from entry to mastery
Original text: https://juejin.cn/post/7033307467199021086

Topics: Android OkHttp