OkHttp request process

Article said OkHttp request process in getResponseWithInterceptorChain (), the following analysis of the request and response process, first look at this way:

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

This core method is divided into three parts, which make up the core code of OkHttp.

getResponseWithInterceptorChainAnalysis of the

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

The above code is simple, just assemble all interceptors. The first interceptor, Client. Interceptors, is the developer’s custom interceptor.

The assembly

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

In this step, the configuration information and interceptors are assembled into a RealInterceptorChain.

To deal with

Here’s where the real processing happens:

      val response = chain.proceed(originalRequest)

Copy the code

Click on it to see how it works:

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

The “proceed” method on the RealInterceptorChain does two things. The “index+1” method is another RealInterceptorChain. Pass the newly formed RealInterceptorChain to the interceptor to execute the intercepting code.

Val interceptor = interceptors[index] the first interceptor in the “proceed” method corresponding to the RealInterceptorChain, Val next = copy(index = index + 1, request = request) is a new RealInterceptorChain with the same configuration but index is 1. Note that this is not an interceptor, this is a chain.

Val Response = interceptor. Intercept (next) Here is the current interceptor. This is an interface whose implementation contains a line of code as seen above. Point here is that the RetryAndFollowUpInterceptor:

          response = realChain.proceed(request)

Copy the code

The realChain is the next interceptor. Intercept (Next), and the index is the second interceptor. Is it suddenly dawning that it is Russian nesting dolls, one after another, and not a method to the end. In the first interceptor implementation, the code in the middle will execute the second interceptor, and then the third and fourth interceptors. And then return one by one.

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 } // Attach the prior response if it exists. Such responses never have a body. 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) } } }Copy the code

There are also three parts:

  1. Configure information to perform its own interception.
  2. Invoke the next interceptor execution
  3. Action after the next interceptor returns

The whole process is like an assembly line, running from left to right and then back to execution.

Interceptor analysis

RetryAndFollowUpInterceptor

Request failed retry and follow redirection interceptor.

 while (true) {}
Copy the code

The first line of code in the loop:

      call.enterNetworkInterceptorExchange(request, newExchangeFinder)
Copy the code

This prepares you for the next connection by creating an ExchangeFinder object for the current call that can find available connections

  this.exchangeFinder = ExchangeFinder(
          connectionPool,
          createAddress(request.url),
          this,
          eventListener
      )
Copy the code

If RouteException or IOException occurs in this loop, it will continue to retry if it can be retried. To determine whether to retry, in RECOVER:

if (! recover(e.lastConnectException, call, request, requestSendStarted = false)) { throw e.firstConnectException.withSuppressed(recoveredFailures) } else { recoveredFailures += e.firstConnectException }Copy the code

Here’s how to determine if you want to redirect:

 val exchange = call.interceptorScopedExchange
 val followUp = followUpRequest(response, exchange)
Copy the code

The returned status code determines if it is a redirect and redirects:

   HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
        return buildRedirectRequest(userResponse, method)
      }
Copy the code

Return response exits the loop until there is no retry and redirection.

After analyzing the interceptor, summarize what it does:

  1. Ready to connect
  2. Invoke the next interceptor
  3. Retry and redirect

BridgeInterceptor

Bridge interceptor, whose main function is to add headers to the current request. RequestBuilder. Header (” accept-encoding “, “gzip”) compression is added by default, and the response data is automatically decompressed.

After analyzing the interceptor, summarize what it does:

  1. addheader
  2. Invoke the next interceptor
  3. Unpack the

CacheInterceptor

Cache interceptor. If there is a cache, 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)
          }
    }
Copy the code

Here we use caching to build a Response.

If the next interceptor is not executed to make a network request, this line will appear many times:

      networkResponse = chain.proceed(networkRequest)

Copy the code

Then cache the results:

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

If you want to see the cache policy, you can click on this class to view it:

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

Copy the code

ConnectInterceptor

The core interceptor, this is where the request is set up.

object ConnectInterceptor : Interceptor {
  @Throws(IOException::class)
  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

This line of code is different from the above:

    return connectedChain.proceed(realChain.request)

Copy the code

The other interceptors do some post-work, such as the cache interceptor decompresses. This is because this is where the connection is actually established, and at this point the request is established and no further action is required.

Here’s how the interceptor works:

/** Finds a new or pooled connection to carry a forthcoming request and response. */ internal fun initExchange(chain: RealInterceptorChain): Exchange { synchronized(this) { check(expectMoreExchanges) { "released" } check(! responseBodyOpen) check(! requestBodyOpen) } val exchangeFinder = this.exchangeFinder!! val codec = exchangeFinder.find(client, chain) val result = Exchange(this, eventListener, exchangeFinder, codec) this.interceptorScopedExchange = result this.exchange = result synchronized(this) { this.requestBodyOpen = true this.responseBodyOpen = true } if (canceled) throw IOException("Canceled") return result }Copy the code

Val codec = exchangeFinder.find(client, chain)

private fun findHealthyConnection( connectTimeout: Int, readTimeout: Int, writeTimeout: Int, pingIntervalMillis: Int, connectionRetryEnabled: Boolean, doExtensiveHealthChecks: Boolean ): RealConnection { while (true) { val candidate = findConnection( connectTimeout = connectTimeout, readTimeout = readTimeout, writeTimeout = writeTimeout, pingIntervalMillis = pingIntervalMillis, connectionRetryEnabled = connectionRetryEnabled ) // Confirm that the connection is good. if (candidate.isHealthy(doExtensiveHealthChecks)) { return candidate } // If it isn't, take it out of the pool. candidate.noNewExchanges() // Make sure we have some routes left to try. One example where we may exhaust all the routes // would happen if we made a new connection and it immediately is detected as unhealthy. if (nextRouteToTry ! = null) continue val routesLeft = routeSelection? .hasNext() ? : true if (routesLeft) continue val routesSelectionLeft = routeSelector? .hasNext() ? : true if (routesSelectionLeft) continue throw IOException("exhausted all routes") } }Copy the code

In the loop, findConnection is used to retrieve the available connection, verify whether the connection is healthy (normal connection, heartbeat, etc.), and return to the healthy state. There are five methods, including initial state retrieval and re-entry retrieval.

Initial Status Indicates the connection mode for three times

  1. Initial state go to the connection pool to find connections:

      if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
          val result = call.connection!!
          eventListener.connectionAcquired(call, result)
          return result
        }
    Copy the code

    This determines whether the number of connections exceeds the limit.

  2. The route is passed in with different parameters:

      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
            val result = call.connection!!
            eventListener.connectionAcquired(call, result)
            return result
          }
    Copy the code

    Obtain the route configuration, the so-called route is actually a proxy, IP address and other parameters of a combination. After retrieving the route, try retrieving the connection from the connection pool, which is mainly for http2 multiplexing.

  3. No connection available, new connection:

newConnection.connect( connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, EventListener)... connectionPool.put(newConnection)Copy the code

If two requests are set up at the same time, one of them will be discarded, saving resources. If two requests are set up at the same time, one of them will be discarded.

    if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
      val result = call.connection!!
      nextRouteToTry = route
      newConnection.socket().closeQuietly()
      eventListener.connectionAcquired(call, result)
      return result
    }
Copy the code

Then add the connection to the connection pool:

      connectionPool.put(newConnection)

Copy the code

Conclusion:

  • Gets a non-multiplexing connection
  • Get all connections
  • Create connections and only get multiplexed connections

Enter again to get the connection

So this is the first time you get a connection, but if you redirect or something like that when you get a connection a second time, there are two ways to do it.

  1. Connection unavailable, close the connection and enter the initial state, but reuse the routing information, do not need to find the route again:
if (callConnection ! = null) { var toClose: Socket? = null synchronized(callConnection) { if (callConnection.noNewExchanges || ! sameHostAndPort(callConnection.route().address.url)) { toClose = call.releaseConnectionNoEvents() } }Copy the code

If the connection does not accept new connections, cannot be reused, or Http is redirected to Https, the connection is released.

  1. If the connection is available, the connection is reused directly:
if (call.connection ! = null) { check(toClose == null) return callConnection }Copy the code

Establish a connection

Click newConnection.connect to see how to establish a connection.

while (true) { try { if (route.requiresTunnel()) { connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener) if (rawSocket == null) { // We were unable to connect the tunnel but properly closed down our resources. break } } else { connectSocket(connectTimeout, readTimeout, call, eventListener) } establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener) eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol) break } catch (e: IOException) { socket? .closeQuietly() rawSocket? .closeQuietly() socket = null rawSocket = null source = null sink = null handshake = null protocol = null http2Connection = null allocationLimit = 1 eventListener.connectFailed(call, route.socketAddress, route.proxy, null, e) if (routeException == null) { routeException = RouteException(e) } else { routeException.addConnectException(e) } if (! connectionRetryEnabled || ! connectionSpecSelector.connectionFailed(e)) { throw routeException } } }Copy the code

Route.requirestunnel (), if the proxy is Http and the target is Https, createTunnelRequest will be created after the socket is created:

connectSocket(connectTimeout, readTimeout, call, eventListener) tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url) ? : break // TCopy the code

Create a socket for a normal Http request:

connectSocket(connectTimeout, readTimeout, call, eventListener)
Copy the code

Its implementation is as follows:

val rawSocket = when (proxy.type()) {
  Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
  else -> Socket(proxy)
}
Copy the code

After the socket is established, create an Http connection:

establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
Copy the code

Different versions of Http connections and encrypted connections are created, depending on the support.

CallServerInterceptor

This interceptor is an interceptor that sends and receives data to the server, and the code is relatively simple.

Conclusion: