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.
getResponseWithInterceptorChain
Analysis 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:
- Configure information to perform its own interception.
- Invoke the next interceptor execution
- 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:
- Ready to connect
- Invoke the next interceptor
- 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:
- add
header
- Invoke the next interceptor
- 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
-
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.
-
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.
-
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.
- 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.
- 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.