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
object | effect |
---|---|
Call | The request calls the interface, indicating that the request is ready for execution or can be cancelled. It can only be executed once. |
RealCall | The specific implementation class of Call interface is the connection bridge between application and network layer, including OkHttpClient and Request information. |
AsyncCall | An asynchronous request call is actually a Runnable and will be placed in the thread pool for processing. |
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. |
Request | Request class, including url, method, headers and body. |
Response | Response data returned by the network layer. |
Callback | Response 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:
- 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.
- 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.
- 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.
- 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.
- ConnectInterceptor: it is mainly responsible for establishing connections. TCP connections or TLS connections will be established.
- 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.
- 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.
- Initialize an exchange object.
- Then create a new connection responsibility chain based on the exchange object.
- 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:
- For the first time, try to reconnect the connection in the call without having to re acquire the connection.
- The second time, try to get a connection from the connection pool without routing and multiplexing.
- The third time, try to get a connection from the connection pool again, with routing and without multiplexing.
- The fourth time, manually create a new connection.
- 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
- 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.
- Factory method pattern: helps to generate complex objects, such as okhttpclient Newcall (request request) to create a Call object.
- 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