This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

preface

OkHttp can be said to be the most common network request framework in Android development,OkHttp is easy to use, strong scalability, powerful,OkHttp source code and principle is also a frequent interview, but OkHttp source content is more, want to learn its source code is often a thousand threads, a moment to grasp the focus. This article is intended to help you quickly build your knowledge of OKHttp

This article mainly includes the following contents

  1. OKHttpWhat is the overall flow of the request?
  2. OKHttpHow does the dispenser work?
  3. OKHttpHow does the interceptor work?
  4. What is the difference between an application interceptor and a network interceptor?
  5. OKHttpHow to reuseTCPThe connection?
  6. OKHttpHow do I clear idle connections?
  7. OKHttpWhat are the advantages?
  8. OKHttpWhat design patterns are used in the framework?

1. OKHttpThis section describes the overall request process

Let’s start by looking at how one of the simplest Http requests is sent.

   val okHttpClient = OkHttpClient()
   val request: Request = Request.Builder()
       .url("https://www.google.com/")
       .build()

   okHttpClient.newCall(request).enqueue(object :Callback{
       override fun onFailure(call: Call, e: IOException){}override fun onResponse(call: Call, response: Response){}})Copy the code

This code looks relatively simple, the OkHttp Request process only needs to contact OkHttpClient, Request, Call, Response, but the framework does a lot of logic processing. Most of the logic for all network requests is concentrated in the interceptor, but it depends on the dispatcher to dispatch request tasks before entering the interceptor. Dispensers and interceptors are briefly described here and will be explained in more detail later

  • Distributor: internal maintenance queue and thread pool, complete request allocation;
  • Interceptors: Five default interceptors complete the request process.



The entire network request process is roughly as shown above

  1. Build through the builder patternOKHttpClientwithRequest
  2. OKHttpClientthroughnewCallInitiate a new request
  3. Requests are deployed by maintaining request queues and thread pools through the dispatcher
  4. Five default interceptors are used to retry requests, cache processing, and establish connections
  5. Get network request results

2. OKHttpHow does the dispenser work?

Dispenser maintenance is the main purpose of the request queue and thread pool, such as asynchronous request, we have 100 certainly cannot put them at the same time request, but they should be lined up to class, is divided into in the request list and waiting lists, such as after completion of the request, can be removed from the waiting list for the request, to complete all the requests

Here the synchronous requests are slightly different from the asynchronous requests

A synchronous request

synchronized void executed(RealCall call) {
	runningSyncCalls.add(call);
}
Copy the code

Because synchronous requests do not require a thread pool, there are no restrictions. So the dispenser just keeps track. The subsequent requests can be synchronized according to the sequence of joining the queue

An asynchronous request

synchronized void enqueue(AsyncCall call) {
	// The number of requests to a Host cannot exceed 64. The number of requests to a Host cannot exceed 5
	if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) 	  {
		runningAsyncCalls.add(call);
		executorService().execute(call);
	} else{ readyAsyncCalls.add(call); }}Copy the code

If the number of tasks being executed does not exceed the maximum limit of 64 and the number of requests to the same Host does not exceed 5, the system adds the tasks to the executing queue and submits them to the thread pool. Otherwise, join the waiting queue first. After each task is complete, the dispatcher’s FINISHED method is called, which fetches the finished task from the wait queue to continue execution

3. OKHttpHow does the interceptor work?

After the task distribution from the distributor above, it’s time to start a series of configurations using interceptors

# RealCall
  override fun execute(a): Response {
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)}}Copy the code

We’ll look at RealCall the execute method, it can be seen that finally returned to the getResponseWithInterceptorChain, the construction of a chain of responsibility and processing is actually in this method

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
    )
    val response = chain.proceed(originalRequest)
  }
Copy the code

As shown above, the responsibility chain of the OkHttp interceptor is built, which is, as the name implies, an execution chain used to handle the responsibility of the relevant transaction. There are multiple nodes on the execution chain, and each node has the opportunity (conditional matching) to process the requested transaction. If a node has finished processing, it can be passed to the next node to continue processing or return to the end of processing according to actual service requirements. The order and functions of adding responsibility chain as shown above are shown in the following table:

The interceptor role
Apply interceptor You get the original request, and you can add custom headers, generic parameters, parameter encryption, gateway access, and so on.
RetryAndFollowUpInterceptor Handles error retries and redirects
BridgeInterceptor The main job of the application layer and network layer bridge interceptor is to add cookies to the request, add fixed headers, such as Host, Content-Length, content-Type, user-agent, etc., and then save the cookie of the response result. If the response has been compressed using gzip, you also need to decompress it.
CacheInterceptor A cache interceptor that does not initiate a network request if it hits the cache.
ConnectInterceptor Connection interceptors, which internally maintain a connection pool, are responsible for connection reuse, connection creation (three-way handshake, etc.), connection release, and socket flow creation on connections.
networkInterceptors User-defined interceptors are typically used to monitor data transfers at the network layer.
CallServerInterceptor The request interceptor, after the preparatory work is complete, actually initiates the network request.

That’s how our web requests go down the chain of responsibility until they’re executedCallServerInterceptortheinterceptMethod that encapsulates the result of the network response into oneResponseObject andreturn. And then you go back up the chain of responsibility, and you end up backgetResponseWithInterceptorChainMethod, as shown below:

4. What is the difference between application interceptor and network interceptor?

From the perspective of the whole responsible link, the application interceptor is the interceptor executed first, that is, the original request after the user sets the Request attribute. The network interceptor is located between the ConnectInterceptor and CallServerInterceptor. At this time, the network link is ready. Just wait for the request data to be sent. The main differences are as follows

  1. First, apply the interceptor inRetryAndFollowUpInterceptorandCacheInterceptorSo once error retries or network redirects occur, the network interceptor may execute multiple times because it is equivalent to making a second request, but applying the interceptor will only fire once. In addition, if theCacheInterceptorIf the cache is hit, there is no need to go network request, so there will be short circuit network interceptor situation.
  2. Secondly, in addition toCallServerInterceptorIn addition, each interceptor should be called at least oncerealChain.proceedMethods. It can actually be called multiple times in the application interceptor layerproceedMethod (local exception retry) or notproceedMethod (interrupt), but the network interceptor layer of the connection is ready and can be called only onceproceedMethods.
  3. Finally, from the use scenario, the application interceptor will be called only once, usually used to count the network request initiated by the client; A call of a network interceptor means that a network communication must be initiated, so it can usually be used to count the data transmitted over a network link.

5. OKHttpHow to reuseTCPThe connection?

The main work of the ConnectInterceptor is to establish a TCP connection. To establish a TCP connection, you need to shake hands three times and wave hands four times. If you need to create a TCP connection for each HTTP request, it will consume a lot of resources. Http1.1 already supports keep-alive, i.e. multiple HTTP requests reuse a TCP connection

ConnectInterceptor will eventually find the connection code calls to ExchangeFinder. FindConnection method, specific as follows:

# ExchangeFinder
// Find connections to host new data flows. The search sequence is allocated connections, connection pools, and new connections
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  synchronized (connectionPool) {
    // 1. Try to use the connection that has been allocated to the data stream.
    releasedConnection = transmitter.connection;
    result = transmitter.connection;

    if (result == null) {
      // 2. If there are no available connections allocated, try to obtain them from the connection pool. (More on connection pooling later)
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null.false)) { result = transmitter.connection; }}}synchronized (connectionPool) {
    if (newRouteSelection) {
      //3. Now that you have the IP address, try again to get it from the connection pool. Possible matches due to join merge. (routes passed, null passed above)
      routes = routeSelection.getAll();
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
        foundPooledConnection = true; result = transmitter.connection; }}// 4. If the connection fails for the second time, set up the connection with the server through TCP + TLS handshake. Yes blocking operation
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);

  synchronized (connectionPool) {
    // 5. The last attempt is to retrieve the connection from the pool. Note that the last parameter is true, which requires multiplexing (http2.0).
    If this is HTTP2.0, then in order to ensure multiplexing (since the handshake above is not thread-safe) the pool will be rechecked to see if the same connection exists at this time
    if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
      // Close the connection we created and return the obtained connection
      result = transmitter.connection;
    } else {
      // If the last attempt fails, store the newly created connection to the connection poolconnectionPool.put(result); }}return result;
}
Copy the code

As you can see from the simplified code above, the connection interceptor uses five methods to find connections

  1. The first attempt is to use the connection already assigned to the request. (A connection has been allocated, such as a redirection request, indicating that a connection was made last time.)
  2. If there are no available connections allocated, try to match them from the connection pool. Because there is no routing information at this time, the matching conditions are as follows:addressConsistent –host,port, proxy, etc., and the matching connection can accept new requests.
  3. If it is not obtained from the connection pool, it is passedroutesTry to get again, this is mainly forHttp2.0An operation of,Http2.0You can reusesquare.comwithsquare.caThe connection of the
  4. If it does not get it again, create itRealConnectionInstance, proceedTCP + TLSShake hands to establish a connection with the server.
  5. In order to make sureHttp2.0The multiplexing of the connection is matched from the connection pool for the third time. Because the handshake process for newly established connections is non-thread-safe, it is possible that the same connection is newly stored in the connection pool.
  6. If a match is found for the third time, the existing connection is used to release the newly created connection. If no match is found, the new connection is pooled and returned.

This is the connection interceptor’s attempt to reuse the connection. The flow chart is as follows:

6. OKHttpHow do I clear idle connections?

It says that we will set up a TCP connection pool, but if there are no more tasks, idle connections should be cleared. How does OKHttp do this?

  # RealConnectionPool
  private val cleanupQueue: TaskQueue = taskRunner.newQueue()
  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
    override fun runOnce(a): Long = cleanup(System.nanoTime())
  }

  long cleanup(long now) {
    int inUseConnectionCount = 0;// The number of connections in use
    int idleConnectionCount = 0;// Number of idle connections
    RealConnection longestIdleConnection = null;// The connection with the longest idle time
    long longestIdleDurationNs = Long.MIN_VALUE;// The longest idle time

    // Walk through the connection: find the connection to clean up, find the next time to clean up (has not reached the maximum idle time)
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is in use, continue, the number of connections in use +1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }
		// Number of idle connections +1
        idleConnectionCount++;

        // Assign the maximum idle time and the corresponding connection
        long idleDurationNs = now - connection.idleAtNanos;
        if(idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; }}If the maximum idle time is greater than 5 minutes or the number of idle times is greater than 5, remove and close the connection
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // else, return how much time is left to arrive 5 minutes, then wait this time to clean up again
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // If there is no free connection, try to clean up after 5 minutes.
        return keepAliveDurationNs;
      } else {
        // No connection, no cleanup
        cleanupRunning = false;
        return -1; }}// Close the removed connection
    closeQuietly(longestIdleConnection.socket());

    // Close remove immediately after the next attempt to clean up
    return 0;
  }
Copy the code

The idea is clear:

  1. The scheduled task is started when the connection is added to the connection pool
  2. If there are idle connections, if the longest idle time is greater than 5 minutes or the number of idle connections is greater than 5, remove and close the longest idle connections. If the idle number is less than 5 and the maximum idle time is less than 5 minutes, go back to the remaining 5 minutes and wait for that time to clean up.
  3. If there are no free connections, wait 5 minutes and then try to clean up.
  4. No connection does not clean up.

The process is as follows:

7. OKHttpWhat are the advantages?

  1. Simple to use, in the design of the use of appearance mode, the complexity of the whole system to hide, subsystem interface through a clientOkHttpClientUnity is exposed.
  2. With strong expansibility, it can fulfill various customized requirements of users through customized application interceptors and network interceptors
  3. Powerful and supportedSpdy,Http1.X,Http2, as well asWebSocketAnd so on.
  4. Reuse the underlying layer through connection poolingTCP(Socket) to reduce request latency
  5. Seamless supportGZIPReduce data traffic
  6. Support data caching to reduce repeated network requests
  7. Supports automatic retry of other hosts when a request failsip, automatic redirection

8. OKHttpWhat design patterns are used in the framework?

  1. Builder pattern:OkHttpClientwithRequestAll builds use the Builder pattern
  2. Appearance mode:OkHttpThe facade pattern is used to hide the complexity of the entire system, and the subsystem interface is passed through a clientOkHttpClientUnity is exposed.
  3. Chain of Responsibility mode:OKHttpAt its heart is the chain of responsibility pattern, which configures requests through a chain of five default interceptors
  4. Share mode: The core of share mode is reuse in pool,OKHttpreuseTCPConnection pools are used for connections, and thread pools are used for asynchronous requests

conclusion

This article mainly summarizes the knowledge points related to OKHttp principle and answers the following questions:

  1. OKHttpWhat is the overall flow of the request?
  2. OKHttpHow does the dispenser work?
  3. OKHttpHow does the interceptor work?
  4. What is the difference between an application interceptor and a network interceptor?
  5. OKHttpHow to reuseTCPThe connection?
  6. OKHttpHow do I clear idle connections?
  7. OKHttpWhat are the advantages?
  8. OKHttpWhat design patterns are used in the framework?

If you help, welcome to like, thank you ~

The resources

Interviewer: I hear you’re familiar with OkHttp? OkHttp: OkHttp: OkHttp: OkHttp: OkHttp: OkHttp: OkHttp: OkHttp: OkHttp: OkHttp: OkHttp