Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

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

There are many common design patterns in OkHttp’s source code, Factory (call. Factory, websocket. Factory, cacheStrategy. Factory in CacheInterceptor, etc.), externalizer (OkHttpClient), etc. But the most obvious and important thing about OkHttp is that there are three modes: builder mode, responsibility chain mode, and share mode.

Builder model

Definition: To separate the construction and presentation of complex objects. The Builder pattern encapsulates the creation of a complex object, allowing the object to be created in multiple steps. The Builder pattern can be used to its advantage when the object is particularly complex and has many internal parameters.

If the source code has the word Builder, the Builder mode is most likely used.

OkHttpClient

OkHttpClient is the default mode, but it contains a lot of complex objects: Timeout, proxy, cache, dispatcher, interceptors and so on.

public class OkHttpClient implements Cloneable.Call.Factory.WebSocketCall.Factory {
  
  // constructor 1
  public OkHttpClient(a) {
    this(new Builder()); 
  }  
  
  // constructor 2
  private OkHttpClient(Builder builder) {... }public Builder newBuilder(a) {
    return new Builder(this);
  }
  
  / / Builder class
  public static final class Builder {  
    
    public Builder(a) { }   
    
    Builder(OkHttpClient okHttpClient) { }  
    
    / / Builder class OkHttpClient
    public OkHttpClient build(a) {
      return new OkHttpClient(this); }}}Copy the code

The newBuilder function does:

Instantiate the OkHttpClient object with the Builder pattern:

// Instantiate a default HTTP client
OkHttpClient client = new OkHttpClient();
// Create an HTTP client instance with custom Settings
OkHttpClient client = new OkHttpClient.Builder()
                                .addInterceptor(new HttpLoggingInterceptor())  // Add interceptors
                                .cache(new Cache(cacheDir, cacheSize))  // Sets the response cache for reading and writing cached responses.
                                .build();
// Instantiate an HTTP client instance that timed out at 500 milliseconds
OkHttpClient eagerClient = client.newBuilder()
                                .readTimeout(500, TimeUnit.MILLISECONDS)
                                .build();   
Copy the code

Request

The Request class generates instances through Request.Builder through Builder mode, where URL represents the URL of the Http Request, method refers to GET/POST and other requests, header and body are naturally the head and body of the Http Request. CacheControl is cacheControl.

Kotlin code:

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") valbody: RequestBody? .internal val tags: Map<Class<*>, Any>
) {

  private var lazyCacheControl: CacheControl? = null
  val isHttps: Boolean
  fun header(name: String): String? = headers[name]
  fun headers(name: String): List<String> = headers.values(name)
  fun newBuilder(a): Builder = Builder(this)
  @get:JvmName("cacheControl") val cacheControl: CacheControl
   
  open class Builder {...open fun url(url: String): Builder {}
    open fun url(url: URL) = url(url.toString().toHttpUrl())
    open fun header(name: String, value: String) 
    open fun addHeader(name: String, value: String)
    open fun removeHeader(name: String)
    open fun headers(headers: Headers)
    open fun cacheControl(cacheControl: CacheControl): Builder {}

    open fun get(a) = method("GET".null)
    open fun head(a) = method("HEAD".null)
    open fun post(body: RequestBody) = method("POST", body)

    @JvmOverloads
    open fun delete(body: RequestBody? = EMPTY_REQUEST) = method("DELETE", body)
    open fun put(body: RequestBody) = method("PUT", body)
    open fun patch(body: RequestBody) = method("PATCH", body)
    open fun method(method: String, body: RequestBody?).: Builder = apply {}
		...
    open fun build(a): Request {}
  }
}

Copy the code

Java code:

public final class Request {

  private Request(Builder builder) {... }public Builder newBuilder(a) {
    return new Builder(this);
  }

  public static class Builder {   
    private HttpUrl url;
    public Builder(a) {... }private Builder(Request request) {
      this.url = request.url; . }public Builder url(HttpUrl url) {}
    public Builder header(String name, String value) {}...public Request build(a) {
      if (url == null) throw new IllegalStateException("url == null");
      return new Request(this); }}}Copy the code

This class

. A similar Request

Chain of Responsibility model

define

If the word Chain is present in the source code, the Chain of responsibility pattern is most likely used.

Using an example

A simple interceptor that logs outgoing requests and incoming responses:

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();

    // Work ahead
    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    // Work in the middle
    Response response = chain.proceed(request);

    
    // do the post-work
    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));

    returnresponse; }}Copy the code

The source code parsing

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(
        ...
    )

    var calledNoMoreExchanges = false
    try {
      val response = chain.proceed(originalRequest)
      ...
      return response
    } catch(e: IOException) { ... }}Copy the code

The layers of Interceptor

From the sequence diagram of responsibility chain above, it can be seen that the most important operation of all interceptors is intercceptor() directly, which makes the responsibility chain continuously transmitted and executed. We call the operation before intercceptor() method of each interceptor as pre-operation. The operations that follow the intercceptor() method are called pre and post operations for detailed analysis.

Developer custom Interceptor

The addInterceptor Interceptor set by the developer will run before all other Interceptor processes. It will also receive a Response and do the final cleanup. If you have a uniform header to add, you can set it here;

RetryAndFollowUpInterceptor

Responsible for error retry and redirection. This layer of interceptors makes error retries and redirects insensitive to the developer.

while (true) {
      call.enterNetworkInterceptorExchange(request, newExchangeFinder)
      ...
        try {
          response = realChain.proceed(request)
          newExchangeFinder = true
        } catch(e: RouteException) { ... }... }Copy the code

Pre-work:

Through the call. EnterNetworkInterceptorExchange () method, create ExchangeFinder, which is available for use in the subsequent interceptors.

Central work:

response = realChain.proceed()

Post work:

Error retry, redirection -> Return response

BridgeInterceptor

Bridge (bridge from application code to network code)

Pre-work:

Fill the HEAD header in the Http request protocol

Central work:

response = realChain.proceed()

Post work:

Unlock the body with GzipSource

if (transparentGzip &&
        "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
        networkResponse.promisesBody()) {
      val responseBody = networkResponse.body
      if(responseBody ! =null) {
        val gzipSource = GzipSource(responseBody.source())
        val strippedHeaders = networkResponse.headers.newBuilder()
            .removeAll("Content-Encoding")
            .removeAll("Content-Length")
            .build()
        responseBuilder.headers(strippedHeaders)
        val contentType = networkResponse.header("Content-Type")
        responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
      }
    }
    return responseBuilder.build()
  }
Copy the code

CacheInterceptor

Pre-work:

Check if there is a cache, use it and return it ahead of time; In addition, if the network is prohibited or the cache is insufficient, the system will return earlier.

// If we're forbidden from using the network and the cache is insufficient, fail.
    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 don't need the network, we're done.
    if (networkRequest == null) {
      returncacheResponse!! .newBuilder() .cacheResponse(stripBody(cacheResponse)) .build().also { listener.cacheHit(call, it) } }Copy the code

Central work:

response = realChain.proceed()

Post work:

Determine if the response results can be cached, and cache/update them if possible

 if(cache ! =null) {
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        val cacheRequest = cache.put(response)
        return cacheWritingResponse(cacheRequest, response).also {
          if(cacheResponse ! =null) {
            // This will log a conditional cache miss only.
            listener.cacheMiss(call)
          }
        }
      }

      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.}}}Copy the code

The Interceptor also provides an IF-Modified-since (304) HTTP request. This Interceptor provides an IF-Modified-since (304) HTTP request.

Determines whether the data cached by the client has expired and retrieves new data if it has

It is typically used when the server needs to return large files to speed up HTTP transfers.

    // If we have a cache response too, then we're doing a conditional get.
    if(cacheResponse ! =null) {
      if(networkResponse? .code == HTTP_NOT_MODIFIED) {valresponse = cacheResponse.newBuilder() .headers(combine(cacheResponse.headers, networkResponse.headers)) .sentRequestAtMillis(networkResponse.sentRequestAtMillis) .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build() networkResponse.body!! .close()// Update the 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

ConnectInterceptor

This interceptor is the heart of okHTTP’s core.

Because the interceptor only takes care of the connection, the pre-action is to find a connection, the mid-action is connectedChain.proceed(), and the post-action returns a response directly, with no additional post-action.

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

The key is the initExchange() line, which takes a connection and makes it healthy, creates a Codec object based on the connection, and then pushes the codec object into the Exchange.

 internal fun initExchange(chain: RealInterceptorChain): Exchange {
    ...
    val exchangeFinder = this.exchangeFinder!!
    val codec = exchangeFinder.find(client, chain)
    val result = Exchange(this, eventListener, exchangeFinder, codec)
    this.interceptorScopedExchange = result
    this.exchange = result
    ...
    return result
  }
Copy the code

ExchangeFinder find methods in class as you can see, the get a when a connection is available and health through resultConnection. NewCodec (client, chain) method, from an available and healthy connection object created the Codec.

class ExchangeFinder(...). {...fun find(
    client: OkHttpClient,
    chain: RealInterceptorChain
  ): ExchangeCodec {
    try {
      val resultConnection = findHealthyConnection(
          ...
      )
      return resultConnection.newCodec(client, chain)
    } catch(e: RouteException) { ... }}}Copy the code

In findHealthyConnection () method will be called ExchangeFinder. FindConnection () method, this method in the flyweight pattern under this chapter to explore.

NetworkInterceptor

A network interceptor is not usually used unless you want to get the raw network request and return, because the next layer of the interceptor is the CallServerInterceptor. The next layer is the CallServerInterceptor, which directly receives and receives the network without any pre – and post-operations.

FaceBook’s Stetho.

CallServerInterceptor

The last interceptor in the chain, since it is only responsible for sending and receiving with the network, it has no pre – and post-operations and only executes the intercept intermediate method.

It operates exchage, sends requests, reads responses (and IO operations), makes network calls to the server to send content, and outputs fields to the IO stream. Specific embodiment:

Http1.1: Input to string stream

Http2: Input to binary frames, HEADER frames and DATA frames

 exchange.writeRequestHeaders(request)

    var invokeStartEvent = true
    var responseBuilder: Response.Builder? = null
    if(HttpMethod.permitsRequestBody(request.method) && requestBody ! =null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
        exchange.flushRequest()
        responseBuilder = exchange.readResponseHeaders(expectContinue = true)
        exchange.responseHeadersStart()
        invokeStartEvent = false
      }
      if (responseBuilder == null) {... }else {
      exchange.noRequestBody()
    }
Copy the code

summary

When using the framework, only application interceptors and network interceptors can be customized.

Each Interceptor must follow the Interceptor interface, and the implementation class portion of the Interceptor responsibility chain will return the next Interceptor in the chain as index increments.

RealInterceptorChain:

@Throws(IOException::class)
  override fun proceed(request: Request): Response {
    ...
    // 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")...return response
  }
}
Copy the code

Other chain of responsibility patterns common in Android: ViewGroups distribute events to subviews.

The flyweight pattern

Core of the Share design pattern: reuse in the pool.

Existing objects are stored in the pool and reused or created.

The general concept of pooling is most likely to use the metadata pattern: thread pool, connection pool, data pool, cache pool, message pool, etc.

ExchangeFinder.findConnection()

OkHttp3 defines the Connection between a client and a server as an interface Connection, implemented through a RealConnection, stored in a two-ended queue in a ConnectionPool.

  private val connections = ConcurrentLinkedQueue<RealConnection>()
Copy the code

You need to call in ConnectInterceptor realChain. Call. InitExchange find exchange (chain), and in initExchange, The findConnection() method in ExchangeFinder is called to find an available connection connection. Connections are stored in connections queues in a RealConnectionPool, and OkHttp embodies the core idea of the share pattern in its search for available connections.

Route

Route represents a specific route, which contains the connection IP address, port, proxy mode, and some direct HttpClient connection information.

class Route(
  @get:JvmName("address") val address: Address,
  @get:JvmName("proxy") val proxy: Proxy,
  @get:JvmName("socketAddress") val socketAddress: InetSocketAddress
) {
  ...
}

Copy the code

In OkHtttp, routes form the RouteSelection, which is actually a Selection class that is a collection of routes from different IP addresses of the same port and proxy type. A RouteSelection constitutes a RouteSelector. [Http2] [Route] [IP] [proxy] [Route] [IP] [proxy] [proxy] [Route] [IP] [proxy] [proxy] [Http2] [proxy] [Route] [IP] [proxy] [proxy] [proxy] [Route] [IP] [proxy] [proxy] [Http2] [proxy]

Connection finding procedure

ExchangeFinder. FindConnection () returns a connection to host a new stream, it will give preference to an existing stream, if not, will eventually create a new connection pool. This method checks for cancellation before each blocking operation.

ExchangeFinder. FindConnection () a total of five times for getting connection:

  • First: Connected and available:

    Call. connection is not empty and eligible for reuse. It is used directly and returned

  • The second time: no connection, go to the pool, non-multiplexing connection:

    Call callAcquirePooledConnection find the multiplexing link

  • Third time: No connection, go to the pool, multiplexed/non-multiplexed connection:

    Call callAcquirePooledConnection non/road to get more

  • Fourth time: none, I will create the connection myself

  • Fifth time: Based on the fourth time, try to get only the multiplexed link, use this one if you can get it

    Called again callAcquirePooledConnection take multiplexing link, if get will create the discarded for the fourth time, will return the result

@Throws(IOException::class)
  private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    // Check whether the call is cancelled. If the call is cancelled, no further operations are required
    if (call.isCanceled()) throw IOException("Canceled")

    // Try to reuse an existing connection in call
    val callConnection = call.connection 
    if(callConnection ! =null) {
      var toClose: Socket? = null
      synchronized(callConnection) {
        if(callConnection.noNewExchanges || ! sameHostAndPort(callConnection.route().address.url)) {// This step first checks whether the existing connection in the call meets the reuse criteria. If it does not, the connection will be released: releaseConnection
          toClose = call.releaseConnectionNoEvents()
        }
      }

      // The call connection is not null, indicating that the above reusability condition has been passed
      // If the connection exists, it is still a good link
      if(call.connection ! =null) {
        check(toClose == null)
        return callConnection
      }

      // The call connection is released, and the connection is closedtoClose? .closeQuietly() eventListener.connectionReleased(call, callConnection) }// We need a new connection
    refusedStreamCount = 0
    connectionShutdownCount = 0
    otherFailureCount = 0

    // The first attempt to get a connection from the pool,
    // The first fetch does not look for a multiplexable connection
    // The connection is successful
    RequireMultiplexed: false
    // select only non-multiplexed connections (Http1.1 connections, Http2 connections)
    if (connectionPool.callAcquirePooledConnection(address, call, null.false)) {
      val result = call.connection!!
      eventListener.connectionAcquired(call, result)
      return result
    }

    // There is nothing in the pool. Calculate the next route we need
    val routes: List<Route>?
    val route: Route
    if(nextRouteToTry ! =null) {
      // Use a route from a preceding coalesced connection.
      routes = null
      route = nextRouteToTry!!
      nextRouteToTry = null
    } else if(routeSelection ! =null&& routeSelection!! .hasNext()) {// Use a route from an existing route selection.
      routes = nullroute = routeSelection!! .next() }else {
      // Compute a new route selection. This is a blocking operation!
      var localRouteSelector = routeSelector
      if (localRouteSelector == null) {
        localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
        this.routeSelector = localRouteSelector
      }
      val localRouteSelection = localRouteSelector.next()
      routeSelection = localRouteSelection
      routes = localRouteSelection.routes

      if (call.isCanceled()) throw IOException("Canceled")

      // With a route, you can find connections that can merge connections (Http2)
      / / second try call connectionPool. CallAcquirePooledConnection
      RequireMultiplexed false
      // I take either multiplexing or not
      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        eventListener.connectionAcquired(call, result)
        return result
      }

      route = localRouteSelection.next()
    }

    // I need to create my own connection
    val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      // After the connection is created, the network connection is blocked
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    call.client.routeDatabase.connected(newConnection.route())

    // To prevent the creation of two connections that can be merged, try to take the first one created first and recycle the later one.
    Multiplex: // Route is passed and requireMultiplexed is true
    // indicates that only Http2 connections can be multiplexed
    if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
      val result = call.connection!!
      nextRouteToTry = route
      newConnection.socket().closeQuietly()
      eventListener.connectionAcquired(call, result)
      return result
    }

    synchronized(newConnection) {
      // The connection created by the user is pooled for sharingconnectionPool.put(newConnection) call.acquireConnectionNoEvents(newConnection) } eventListener.connectionAcquired(call,  newConnection)return newConnection
  }
Copy the code

Pay attention to

1. In Java, static is used to modify member variables or functions. However, there is a special use of the term static for inner classes. Ordinary classes are not allowed to be declared static. Only inner classes can be declared static. A static inner class can be used as a normal class without having to instance an external class.

2.Kotlin defaults to static inner classes, as opposed to Java.

reference

square.github.io/okhttp/

zhuanlan.zhihu.com/p/58093669

www.jianshu.com/p/8d69fd920…

Juejin. Cn/post / 684490…