Abstract

This article is mainly to analyze the Android mainstream network request framework OkHttp source code, see every day in the deal with the guy in the end do what. It deals with the most fundamental HTTP problems in the network for us, such as the processing of HTTP request packets and response packets. Allows us to customize the interceptor to implement our own functionality, such as log printing, adding requests, etc. The default six interceptors handle network requests, cache reuse, connection reuse, data conversion, and retry recovery mechanisms.

request

OkHttp’s asynchronous request mainly calls the Enqueue function of the Call object, while the Call interface’s only implementation class is RealCall. So asynchronous requests call RealCall enQueue, while synchronous requests are RealCall object execute.

Enqueue and Execute functions of RealCall

Let’s start with asynchronous requests:

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

    callStart()
    client.dispatcher.enqueue(AsyncCall(responseCallback))
  }
Copy the code

RealCall calls the Enqueue function of the Dispatcher

//Dispatcher
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()
}
Copy the code

ReadyAsyncCalls is a queue that holds incoming AsyncCall.

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

    val executableCalls = mutableListOf<AsyncCall>()
    val isRunning: Boolean
    synchronized(this) {
      val i = readyAsyncCalls.iterator()
      while (i.hasNext()) {
        val asyncCall = i.next()
		//maxRequests The default maximum number of network requests is 64
        if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
        //maxRequestsPerHost The default maximum number of destination host requests is 5
        if (asyncCall.callsPerHost.get() > =this.maxRequestsPerHost) continue // Host max capacity.

        i.remove()
        asyncCall.callsPerHost.incrementAndGet()
        executableCalls.add(asyncCall)
        runningAsyncCalls.add(asyncCall)
      }
      isRunning = runningCallsCount() > 0// To run + Number of running hosts
    }

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

    return isRunning
  }
Copy the code

RunningAsyncCalls is a queue that holds ongoing AsyncCall. The promoteAndExecute() function basically adds the readyAsyncCalls queue to the runningAsyncCalls queue with the request to run and calls its executeOn function.

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

      var success = false
      try {
          // Add AsyncCall to the thread pool
        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!}}}Copy the code

The executeOn function is all about adding AsyncCall to a thread pool. So let’s take a look at AsyncCall’s run function and see how to implement it.

override fun run(a) {
  threadName("OkHttp ${redactedUrl()}") {
    var signalledCallback = false
    timeout.enter()
    try {
      // The response data is returned here
      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)}}}}Copy the code

In the run function, through getResponseWithInterceptorChain () function returns the response data, visible in the function of the network request.

Back to the synchronization request, which calls the Execute function of realCall.

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

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

Visible, a synchronous request directly call the getResponseWithInterceptorChain () function, rather than to the thread pool to execute. So remember to switch to child threads when synchronizing requests.

getResponseWithInterceptorChain

  @Throws(IOException::class)
  internal fun getResponseWithInterceptorChain(a): 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)}}}Copy the code

GetResponseWithInterceptorChain first () function will be our custom interceptors and default interceptor is added to the collection, and create RealInterceptorChain object chain, known as the interceptor chain. Proceed (originalRequest) is then called to return the response data. Each interceptor implements its own specific functionality and calls chain.proceed(request) in its overloaded intercept function to call the intercept function of the next interceptor. Here the request is no longer the originalRequest, and has been modified by the current interceptor, if necessary. Regardless of what each interceptor does, you have a rough idea of the call diagram for the interceptor chain.

Interception chain

In OkHttp, what matters most is the work of each interceptor in the interceptor chain. Examples are the CallServerInterceptor for network requests, the ConnectInterceptor for network connections, the CacheInterceptor for cache policies, the BridgeInterceptor for bridge mode, and the RetryAndFollowU for retry recovery PInterceptor. And our own Interceptor, which can be used to print logs and so on. Depending on where they are in the Interceptor chain, and what they do, we can further customize the Interceptor.

Network request:CallServerInterceptor

So, we pretend that all interceptors will call the next interceptor, up to the last CallServerInterceptor interceptor. As the last interceptor in the chain, only the Intercept function is overridden and the actual network request is made.

  @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
    val sentRequestMillis = System.currentTimeMillis()

    exchange.writeRequestHeaders(request)

    var invokeStartEvent = true
    var responseBuilder: Response.Builder? = null
      // Return false for all but head+get, since others require the request body
    if(HttpMethod.permitsRequestBody(request.method) && requestBody ! =null) {
      // The client request header carries "Expect: 100-continue", which tells the server that the client has an expectation (such as a large request body) that the server can properly handle the request. The response packet status code is 100 yes and 417 no
      if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
        exchange.flushRequest()
        //responseBuilder=null indicates that the server can handle it
        responseBuilder = exchange.readResponseHeaders(expectContinue = true)
        exchange.responseHeadersStart()
        invokeStartEvent = false
      }
      if (responseBuilder == null) {
        if (requestBody.isDuplex()) {
          Http 2.0 full-duplex sends the request and writes the request body to the underlying TCP send cache
          exchange.flushRequest()
          val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
          requestBody.writeTo(bufferedRequestBody)
        } else {
          // The server can handle 'Expect: 100-continue' expectations, writing the request body to the underlying TCP send cache
          val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
          requestBody.writeTo(bufferedRequestBody)
          bufferedRequestBody.close()
        }
      } else {
        exchange.noRequestBody()
        if(! exchange.connection.isMultiplexed) {// Prevent the use of HTTP/1.1 when not 100-continue
          exchange.noNewExchangesOnConnection()
        }
      }
    } else {
      exchange.noRequestBody()
    }

    if (requestBody == null| |! requestBody.isDuplex()) { exchange.finishRequest() }// Start fetching the response header
    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!!!!if (invokeStartEvent) {
        exchange.responseHeadersStart()
        invokeStartEvent = false}}var response = responseBuilder
        .request(request)
        .handshake(exchange.connection.handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build()
    var code = response.code
    // Server response: Expect: 100-continue
    if (code == 100) {
	  // Re-read the response header
      responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!!!!if (invokeStartEvent) {
        exchange.responseHeadersStart()
      }
      response = responseBuilder
          .request(request)
          .handshake(exchange.connection.handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build()
      code = response.code
    }
	// The response header is finished reading
    exchange.responseHeadersEnd(response)

    response = if (forWebSocket && code == 101) {
      // Indicates that the server supports an upgrade to websocket or HTTP 2.
      response.newBuilder()
          .body(EMPTY_RESPONSE)
          .build()
    } else {
      response.newBuilder()
          .body(exchange.openResponseBody(response)) // Get the response data where it really is
          .build()
    }
    if ("close".equals(response.request.header("Connection"), ignoreCase = true) | |"close".equals(response.header("Connection"), ignoreCase = true)) {
      exchange.noNewExchangesOnConnection()// Close the connection
    }
    if ((code == 204 || code == 205) && response.body? .contentLength() ? : -1L > 0L) {
      throw ProtocolException(
          "HTTP $code had non-zero Content-Length: ${response.body? .contentLength()}")}return response
  }
Copy the code

The whole block of code, mainly to distinguish the following cases:

  • Expect: 100-continue: The client request packet carries the request header field, indicating that the server is expected to process a certain expectation of the client, such as a large packet. If the condition is met, the server returns a response packet with the status code 100.

    OKHttp sends the request directly if the condition is met and waits for the server to respond. If the server supports the request, the data is written directly to the sending cache.

  • Full-duplex HTTP2.0: Only VERSION 2.0 of HTTP supports full-duplex. During full-duplex, the transmission of request packets and response packets is inconsistent. HTTP/1.1 half-duplex does not support full-duplex.

    OKHttp sends the request directly if the condition is met and waits for the server to respond. If the server supports the request, the data is written directly to the sending cache.

  • General: Call exchange.norequestBody () directly for the request.

Finally, according to the response headers to judge whether need agreement to upgrade, or through exchange. OpenResponseBody (response) to obtain the response data.

So here’s the process:

// Send the request
exchange.flushRequest()
// Write the request data section
val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
requestBody.writeTo(bufferedRequestBody)
// End the request
exchange.finishRequest()
// Start reading the response header
exchange.responseHeadersStart()
// Read the response header
exchange.readResponseHeaders(expectContinue = false)!!!!!// Stop reading the response header
exchange.responseHeadersEnd(response)
// Read the response body
exchange.openResponseBody(response)
Copy the code

After the normal response data is retrieved, the onResponse function of the Callback object is called. At this point, the whole process is over.

Connection multiplexing:ConnectInterceptor

The Intercept function of the ConnectInterceptor connection interceptor is very simple, but refers to an important concept of OkHttp, connection reuse. As we all know, HTTP is TCP protocol based on transport layer (HTTP/3 will be based on UDP protocol), which means TCP three-way handshake to establish the connection and four-way wave to disconnect the connection, as well as the slow start phase of congestion mechanism, which affects the speed of APP to obtain data from the server. TCP connection multiplexing reduces latency and improves resource utilization.

  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val exchange = realChain.call.initExchange(chain)
    val connectedChain = realChain.copy(exchange = exchange)
    return connectedChain.proceed(realChain.request)
  }
Copy the code

In the most important code in the intercept function realChain. Call. InitExchange (chain), create or reuse a TCP connection to the HTTP request or response. Here’s a chain of calls:

RealCall.initExchange->ExchangeFinder.find->ExchangeFinder.findHealthyConnection
->ExchangeFinder.findConnection
Copy the code

In the findConnection function, the steps for finding available connections are:

  1. Reuse current requestRealCallThe connection. If the currentRealCallCarrying connectionconnectionDon’t fornullAnd the port number of the host is the same as that requested by the current host. Determine if there are other requestscallIn theconnectionInteracts with the server, notifies the connection pool to release the connection, returns the Socket, and reuses the connectionconnection.
  2. Look in the connection pool. Traverses all of the connections in the poolconnectionThrough theconnection.isEligibleTo determine whether it can be reusedconnection.isEligibleThe function checks for consistency of domain names, domain names, proxies, DNS, and so on. Even HTTP / 2connectFor example, certificates.
  3. Create a new connection. If you don’t find the right one in the first two stepsconnectionCan only create new onesconnection. Before creating a new connection, it also tries to find a suitable one in the connection pool after creating a routeconnection. Otherwise the request will be currentcallCreate a newconnectionAnd add to the connection pool.

The connection pool:RealConnectionPool

RealConnectionPool has an internal ConcurrentLinkedQueue

Cache policy:CacheInterceptor

The cache interceptor normally only supports caching of GET requests. Each request calculates the key based on the URL and then takes a snapshot of the cache (the data cached from the last request) in DiskLruCache, if any. Then, a Cache policy is generated based on the Cache snapshot and the request packet. That is, a Cache policy is generated based on the related fields (cache-Control, Etag, etc.) in the header line of the request packet and response packet. Different operations are performed according to different cache policies.

  1. Obtain the cache snapshot by using MD5 + HEX of the URL

    The CACHE SnapShot of DiskLruCache is obtained by using the MD5 hex value calculated from the URL of the Request as the key. The Snapshot caches data related to request and response messages, such as the request line and status line of the request message. The SnapShot is used to construct an Entry object, which stores the header information of request packets and response packets. Finally, the cache Response is created through Entry and SnapShot.

    GetSource (snapshot.getSource) obtains the first metadata information, and snapshot.getSource(ENTRY_BODY) obtains the cached response data.

  2. Cachestrategy. Factory Computes a cache policy

    Through CacheStrategy. Factory (now, chain. The request (), The CacheStartegy object generated by cacheCandidate).compute() contains networkRequest and cacheResponse. If cacheResponse== NULL, data is pulled from the server without caching. If networkRequest== NULL, caching is forced. If both values are null, only-if-cached is not supported. The compute function generates a cache policy based on the request headers of the request and response messages.

    CacheResponse = = null * * * * :

    • There is no local cache, which is the first stepcacheCandidate==nll.
    • The headers of HTTP packets or request packets and response packets that do not support caching are setCache-Control:no-store
    • The cached response header is not setETag.Last-Modified.DateField.

    NetworkRequest = = null * * * * :

    • The request set upCache-Control:only-if-cached.
    • A negotiated cache is used and the cache is not expired.
  3. If networkRequest== NULL, only cache is used.

  4. Sends a network request. If the server responds to 304 and the local cache is not NULL, the local cache is used and updated.

  5. Finally, the request and response packets are stored in the cache. Only GET cache is supported.

  override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    // Step 1: get the cache snapshot using MD5 + HEX of the URL
    valcacheCandidate = cache? .get(chain.request())

    val now = System.currentTimeMillis()
	// Calculate the cache policy
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    val networkRequest = strategy.networkRequest
    valcacheResponse = strategy.cacheResponse cache? .trackResponse(strategy)val listener = (call as? RealCall)? .eventListener ? : EventListener.NONEif(cacheCandidate ! =null && cacheResponse == null) {
      // The cache candidate wasn't applicable. Close it.cacheCandidate.body? .closeQuietly() }// Request packets are cached. That is, the request header carries' cache-control: if-only-cached '
    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)
          }
    }

    // networkRequest==null indicates that no networkRequest is used
    if (networkRequest == null) {
      returncacheResponse!! .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 {
      	// Initiates a network request, which calls the CallServerInterceptor intercept function
        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() } }// If there is a local cache and the server returns 304 packets, the response packet is used
    if(cacheResponse ! =null) {
      if(networkResponse? .code == HTTP_NOT_MODIFIED) {valresponse = 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 cachecache!! .trackConditionalCacheHit() cache.update(cacheResponse, response)return response.also {
          listener.cacheHit(call, it)
        }
      } else{ cacheResponse.body? .closeQuietly() } }valresponse = networkResponse!! .newBuilder() .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build()if(cache ! =null) {
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // Store 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)
          }
        }
      }
	  // Only GET cache is supported
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.}}}return response
  }
Copy the code

Bridge mode: BridgeInterceptor

It is called a bridging mode interceptor because it converts application-initiated requests into HTTP requests and network response data into application data. If there is no header information added to the Request message, add it to the Request message, such as content-type and content-Length.

Header line of request message:

  • Content-Type: if theReqeuestBodyaddedContentType, help us add toheader.
  • Content-LengthIf:ReqeuestBodythecontentLengthIf no, addTransfer-Encoding:chunkedOtherwise, add toheader.
  • Transfer-Encoding:chunked, in the absence ofReqeuestBodyThe request header is set only when the request header is set.
  • Host: domain name
  • Connection:Keep-Alive
  • Accept-Encoding: gzipIf you help us add the request header, you are also responsible for helping us uncompress it.
  • Cookie:XXXIf there is one
  • User-Agent:okhttp/${OkHttp.VERSION}
 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")}}//Host: domain name request header
    if (userRequest.header("Host") = =null) {
      requestBuilder.header("Host", userRequest.url.toHostHeader())
    }
	//Connection: keep-alive HTTP1.1 Short Connection request header
    if (userRequest.header("Connection") = =null) {
      requestBuilder.header("Connection"."Keep-Alive")}// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    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())

    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()
  }
Copy the code

Retry recovery: RetryAndFollowUpInterceptor

Under the client retry recovery mechanism, the interceptor retries the recovery request mainly according to some exception types thrown by the network request or the status code of the response message. If the exception or status code does not support recovery, or the number of retries reaches the specified number, an exception is thrown and the request initiated by the client this time.

  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) {
          // The attempt to connect via a route failed. The request will not have been sent.
          if(! recover(e.lastConnectException, call, request, requestSendStarted =false)) {
            throw e.firstConnectException.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e.firstConnectException
          }
          newExchangeFinder = false
          continue
        } catch (e: IOException) {
          // An attempt to communicate with a server failed. The request may have been sent.
          if(! recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newExchangeFinder = false
          continue
        }

        // While is an infinite loop
        if(priorResponse ! =null) {
          response = response.newBuilder()
              .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
              .build()
        }

        val exchange = call.interceptorScopedExchange
        // Read some common network errors or redirects, such as 400, and try to create a recovery connection for request
        val followUp = followUpRequest(response, exchange)

        if (followUp == null) {
          if(exchange ! =null && exchange.isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }

        val followUpBody = followUp.body
        if(followUpBody ! =null && followUpBody.isOneShot()) {
          closeActiveExchange = false
          returnresponse } response.body? .closeQuietly()if (++followUpCount > MAX_FOLLOW_UPS) {
          throw ProtocolException("Too many follow-up requests: $followUpCount")
        }

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

The recover function checks whether an exception can be recovered. If set up client retryOnConnectionFailure = false, an exception is not directly to the request. It mainly determines whether the error is avoided according to the type of the error. If it cannot be avoided, it directly throws an exception and re-enters the next while loop if possible.

Custom interceptors

We have two kinds of custom interceptors, one is the common application interceptor, which is the beginning of the interceptor chain. The second type is the network interceptor, which is the second from the bottom of the chain, before the network is initiated. The former is at the front of the interceptor chain and can call subsequent interceptors of the interceptor chain many times, but cannot intercept the content they process. The latter is intercepted only by the originating network and is not called if a cache hit, for example. Therefore, when you customize interceptors, you need to implement different interceptors according to specific requirements.

Chain of Responsibility model

OkHttp’s interceptor chain uses the chain of responsibility design pattern to delegate a request to multiple objects, decoupling the sender and receiver of the request. The object is formed into a chain and the request is passed along the chain until a processing object terminates the request. For example, a cache interceptor hits the cache and terminates the request to the next processing object.

Each processing object, according to its own characteristics, that is, processing power, when the request meets its conditions, it will process without further delivery.

conclusion

Through the analysis of OkHttp source code, it is found that most HTTP knowledge points, namely OkHttp itself implements a set of network request framework from the bottom to the top. I used to think OkHttp would be complicated, given its popularity. But did not expect such a foundation and stability, learning, do not be afraid of the source code, brave past learning.


【 Related links 】

HTTP and HTTPS

The TCP transport layer

OkHttp use

Welcome to like + follow + comment triple play

【Github】【 Nuggets 】【 blog 】