preface

  • Previous article: OkHttp3 source code analysis request flow

This article continues to explore another important aspect of OKHTTP through source code: Interceptors, we know that in the previous article, before the request is sent to the server has a series of interceptor do to request processing before sending out, after the server returns a response, also have a series of interceptor returned to after processing has been done for the responses of the initiating caller, visible, the interceptor is an important part of the core function of okhttp, The analysis of each interceptor’s function will also involve okHTTP’s caching mechanism and connection mechanism.

The source of this article is based on okhttp3.14.x

Okhttp project address: okhttp

Simple use of interceptors

An Interceptor interface is defined as follows:

public interface Interceptor {
   
  // We need to implement the Intercept (chain) method to define our intercept logic
  Response intercept(Chain chain) throws IOException;

  interface Chain {
      
     // Return the Request object
    Request request(a);

    // Call the Chain's proceed(Request) method to process the Request and return a Response
    Response proceed(Request request) throws IOException;


    // If you are currently a network interceptor, this method returns the connection established after the Request execution
    This method returns null if the interceptor is currently being applied
    @Nullable Connection connection(a);

    // Return the corresponding Call object
    Call call(a);

     // The following method, as the name implies, returns or writes timeout
    int connectTimeoutMillis(a);
    Chain withConnectTimeout(int timeout, TimeUnit unit);
    int readTimeoutMillis(a);
    Chain withReadTimeout(int timeout, TimeUnit unit);
    int writeTimeoutMillis(a);
    Chain withWriteTimeout(int timeout, TimeUnit unit)}}Copy the code

The Interceptor is made up of two parts: the Intercept (Chain) method and the Chain internal interface. The following is the common logic for customizing an Interceptor:

public class MyInterceptor implements Interceptor {   
    @Override
    public Response intercept(Chain chain) throws IOException {
        
        //1. Obtain the Request through the Chain
        Request request = chain.request();
        
      	//2
        / /...
        
        Proceed (Request); proceed(Request)
        Response response = chain.proceed(request);
     
        //4
        / /...
        
        //5
        returnresponse; }}Copy the code

This is the general logic of an Interceptor. First of all, we inherit the Interceptor method to implement the Intercept (Chain) method, and complete our own interception logic, that is, 1, 2, 3, 4, 5 steps according to need. This is probably the template implementation for both custom interceptors and okHTTP’s default interceptors. After defining interceptors, When constructing OkhttpChient, we can add a custom Interceptor by addInterceptor(Interceptor) or addNetworkInterceptor(Interceptor) :

OkHttpClient client = new OkHttpClient.Builder()
     .addInterceptor(newMyInterceptor()) .build(); Or OkHttpClient client =new OkHttpClient.Builder()
     .addNetworkInterceptor(new MyInterceptor())
     .build();
Copy the code

So okHTTP will call our custom Interceptor when it chip-calls the Interceptor to handle the request. What’s the difference between addNetworkInterceptor and addNetworkInterceptor? The okHTTP-wiki Interceptors are Interceptors for applications and Interceptors for networks. The okHTTP-wiki Interceptors are Interceptors for applications and Interceptors for networks. When we usually do application development addInterceptor(Interceptor) on the line.

Now that we have our own custom interceptor, let’s take a look at what okHTTP’s default interceptor does.

RealCall :: getResponseWithInterceptorChain()

In the previous article know RealCall getResponseWithInterceptorChain () is to process and send the request and returns a response, we again getResponseWithInterceptorChain () method of source code, as follows:

//RealCall.java
Response getResponseWithInterceptorChain(a) throws IOException {
    // Create a new List to hold interceptors
    List<Interceptor> interceptors = new ArrayList<>();
    // Add our custom application interceptor
    interceptors.addAll(client.interceptors());
    // Add the interceptor responsible for retry redirection
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    // Add interceptors that transform request responses
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // Add interceptors for caching
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // Add interceptors that manage connections
    interceptors.add(new ConnectInterceptor(client));
    if(! forWebSocket) {// No special requirements, no WebSocket protocol, what is WebSocket? To baidu
      // Add our custom web interceptor
      interceptors.addAll(client.networkInterceptors());
    }
    // Add the interceptor responsible for making the request and getting the response
    interceptors.add(new CallServerInterceptor(forWebSocket));

    // Create the first Chain
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null.0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
       // Call the Chain's proceed(Request) method to process the Request
      Response response = chain.proceed(originalRequest);
      / /...
      // Return the response
      return response;
    }
    / /... Omit exception handling
  }
Copy the code

GetResponseWithInterceptorChain () to do three things: 1, to add to the list of interceptors interceptor; 2. Construct the first Chain; 3. Call the Chain’s proceed(Request) method to process the Request. The following are introduced respectively:

Add interceptors to the list of interceptors

In addition to adding our own custom interceptors, we also added the default interceptors as follows:

  • 1, RetryAndFollowUpInterceptor: responsible for the failure retry and redirection.
  • BridgeInterceptor: Is responsible for converting user-constructed requests into requests sent to the server and converting Response returned from the server into a user-friendly Response.
  • CacheInterceptor: Reads the cache and updates the cache.
  • ConnectInterceptor: Is responsible for establishing and managing the connection with the server.
  • CallServerInterceptor: Is responsible for sending requests to and reading responses from the server.

These default interceptors are the focus of this article and will be described separately later.

2. Construct the first Chain

Chain is an internal interface of the Interceptor, and its implementation class is RealInterceptorChain. The first six construction parameters passed in are as follows:

public final class RealInterceptorChain implements Interceptor.Chain {
    
    / /...
    
    public RealInterceptorChain(List<Interceptor> interceptors, Transmitter transmitter, @Nullable Exchange exchange, int index, Request request, Call call, int connectTimeout, int readTimeout, int writeTimeout) {
        this.interceptors = interceptors;/ / list of interceptors
        this.transmitter = transmitter;//Transmitter object, will be introduced later
        this.exchange = exchange;//Exchange object, described later
        this.index = index;//interceptor is simply used to get the interceptor from the interceptors list
        this.request = request;/ / request the request
        this.call = call;/ / Call object
        / /...
    }
    
    / /...
}
Copy the code

These parameters can be passed through the Chain in any subsequent interceptor. As we know, to give each interceptor a chance to process a request, OKHTTP uses the Chain of responsibility pattern to Chain interceptors together. Interceptors are nodes in the Chain of responsibility, and Chain is the connection point between nodes in the Chain of responsibility that connects interceptors together. So how is it connected? Look at the proceed method of the Chain below.

3. Call the Chain’s proceed(Request) method to process the Request

The proceed(Request) method of the RealInterceptorChain is as follows:

public final class RealInterceptorChain implements Interceptor.Chain {
    
    / /...
    
    @Override 
    public Response proceed(Request request) throws IOException {
        return proceed(request, transmitter, exchange);
    }

    public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
        throws IOException {

        //index cannot cross the boundary
        if (index >= interceptors.size()) throw new AssertionError();

        / /...

        // Create a Chain with index + 1
        RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
                                                             index + 1, request, call, connectTimeout, readTimeout, writeTimeout);

        // Gets the next interceptor in the list of interceptors
        Interceptor interceptor = interceptors.get(index);

        // Call the intercept(Chain) method of the next interceptor, pass in the newly created RealInterceptorChain, and return Response
        Response response = interceptor.intercept(next);

        / /...

        // Return the response
        returnresponse; }}Copy the code

The proceed method creates a new Chain and passes an index + 1 constructor argument to it. The proceed method obtains an interceptor from the index interceptors list and calls the Intercept method. Proceed (Request); / / Chain proceed(Request); / / Chain proceed(Request) This repeats the Chain’s proceed logic. Since the index has already been incremented, the Chain fetches the next interceptor via index, calls the intercept(Chain) method of the next interceptor, and so on. This links each interceptor through a Chain, forming a Chain that passes the Request down the Chain until the Request is processed, and then returns a Response, which is passed down the Chain as follows:

From the above knowable, when there is no custom interceptors, chain of responsibility is the first node RetryAndFollowUpInterceptor, end node is CallServerInterceptor, Request in accordance with the order of the interceptor is processing, the Response is reverse processing, Each interceptor has a chance to handle Request and Response, a perfect implementation of the chain of responsibility pattern.

Know the getResponseWithInterceptorChain () the whole process, the following respectively introduce the function of each default interceptor.

RetryAndFollowUpInterceptor

Said when I was in a custom interceptors, the Interceptor intercept (Chain) method is interceptors to intercept, the intercept RetryAndFollowUpInterceptor (Chain) method is as follows:

//RetryAndFollowUpInterceptor.java
@Override 
public Response intercept(Chain chain) throws IOException {

    / / get Request
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    / / get the Transmitter
    Transmitter transmitter = realChain.transmitter();

    // Redirection times
    int followUpCount = 0;
    Response priorResponse = null;
    
    // An infinite loop
    while (true) {
        
        // Call the prepareToConnect method as the Transmitter to prepare the connection
        transmitter.prepareToConnect(request);

        if (transmitter.isCanceled()) {
            throw new IOException("Canceled");
        }
        
        Response response;
        boolean success = false;
        try {
            // Call the proceed method, which calls the Intercept method of the next interceptor, BridgeInterceptor
            response = realChain.proceed(request, transmitter, null);
            success = true;
        }catch (RouteException e) {// RouteException occurs
            // Call the recover method to check whether the connection can continue to be used
            if(! recover(e.getLastConnectException(), transmitter,false, request)) {
                throw e.getFirstConnectException();
            }
            continue;
        } catch (IOException e) {// IOException occurs, and the connection to the server fails to be established
            booleanrequestSendStarted = ! (einstanceof ConnectionShutdownException);
            // Call the recover method to check whether the connection can continue to be used
            if(! recover(e, transmitter, requestSendStarted, request))throw e;
            continue;
        } finally {// Other unknown exceptions occur
            if(! success) {/ / call the Transmitter exchangeDoneDueToException release connection () methodtransmitter.exchangeDoneDueToException(); }}// The connection is successful and the response is returned

        / /...

        // Request headers are processed according to the response code
        Request followUp = followUpRequest(response, route);

        // If followUp is empty, no redirection is required
        if (followUp == null) {
            / /...
            return response;
        }

        //followUp is not empty and needs to be redirected

        / /...

        // The MAX_FOLLOW_UPS value is 20 and the redirection times cannot be greater than 20
        if (++followUpCount > MAX_FOLLOW_UPS) {
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
        }

        // Try again with the redirected Requestrequest = followUp; priorResponse = response; }}Copy the code

RetryAndFollowUpInterceptor intercept (Chain) method mainly failure retry and redirect logic, the method process is as follows:

1, first get Transmitter class;

2, then enter a dead loop, first call prepareToConnect method, ready to establish a connection; (The connection is actually set up in the ConnectInterceptor)

Proceed (); proceed (BridgeInterceptor);

3.1 if RouteException or IOException is thrown during the request process, the RECOVER method will be called to detect whether the connection can continue to use, if not, the exception will be thrown, the whole process ends, otherwise, it will retry again, which is failure retry;

3.2, if in the process of request to throw in addition to 3.1 abnormalities, it invokes the Transmitter exchangeDoneDueToException () method to release the connection, the whole process is over.

The followUpRequest method will be invoked after the Response is returned. The followUpRequest method will be used to determine whether the followUp request needs to be redirected according to the Response code returned. If the followUp request does not need to be redirected, the followUp request will be returned directly. If redirection is required, use the redirected Request and try again. The redirection times cannot exceed 20.

1, the Transmitter

There is a Transmitter in the process of the whole method, which is the bridge between the application layer and the network layer in OKHTTP. It manages the relationship between all the connections, requests, responses and IO streams of the same Cal. It is created after the creation of RealCall, as follows:

//RealCall.java
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    / / create RealCall
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    // Create Transmitter and assign the Transmitter field to call
    call.transmitter = new Transmitter(client, call);
    return call;
}
Copy the code

After creation, when the Chain structure node as a parameter to pass to go in, have talked about in the getResponseWithInterceptorChain method, so it can be through the Chain in the intercept method. The transmitter (), Its life cycle runs through all interceptors, and you’ll find it in ConnectInterceptor and CallServerInterceptor. Let’s look at its main members as follows:

public final class Transmitter {
    
    private final OkHttpClient client;/ / OkHttpClient big housekeeper
    private final RealConnectionPool connectionPool;// Connection pool, which manages connections
    public RealConnection connection;// This time the connection object
    private ExchangeFinder exchangeFinder;// Create a connection
    private @Nullable Exchange exchange;// Is responsible for connecting IO stream reads and writes
    private final Call call;/ / Call object
    
    / /...
    
    public Transmitter(OkHttpClient client, Call call) {
        this.client = client;
        this.connectionPool = Internal.instance.realConnectionPool(client.connectionPool());
        this.call = call;
        this.eventListener = client.eventListenerFactory().create(call);
        this.timeout.timeout(client.callTimeoutMillis(), MILLISECONDS);
    }

    
    public void prepareToConnect(Request request) {
        if (this.request ! =null) {
            if (sameConnection(this.request.url(), request.url()) && exchangeFinder.hasRouteToTry()) {
                return; // Already ready.
            }
           / /...
        }
        this.request = request;
        / / create ExchangeFinder
        this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()), call, eventListener); }}Copy the code

In Transmitter, we know both client and call, and the remaining RealConnectionPool, RealConnection, ExchangeFinder and Exchange are all related to the connection mechanism of OKHTTP. Both are introduced in the ConnectInterceptor, which is responsible for managing the relationship between them. Just keep in mind that the prepareToConnect method basically creates an ExchangeFinder, which prepares the connection to be established in the ConnectInterceptor.

BridgeInterceptor

The BridgeInterceptor intercept(Chain) method is as follows:

//BridgeInterceptor.java
@Override 
public Response intercept(Chain chain) throws IOException {
    
    / / get Request
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
  
    // Add or remove information from the Request header as needed
    
    RequestBody body = userRequest.body();
    if(body ! =null) {
      MediaType contentType = body.contentType();
      if(contentType ! =null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if(contentLength ! = -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding"."chunked");
        requestBuilder.removeHeader("Content-Length"); }}if (userRequest.header("Host") = =null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") = =null) {
      requestBuilder.header("Connection"."Keep-Alive");
    }

    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") = =null && userRequest.header("Range") = =null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding"."gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if(! cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") = =null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    // Call the proceed method, which calls the Intercept method of the next CacheInterceptor
    Response networkResponse = chain.proceed(requestBuilder.build());

    // Response is returned
    // Add or remove information for the Response header as needed
    
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }
Copy the code

The logic in the BridgeInterceptor is the simplest of all the default interceptors. It basically does some processing on the Request or Response header and converts the user-constructed Request into a Request sent to the server. And convert the Response returned by the server into a user-friendly Response. For example, for Request, when the developer does not add accept-encoding, it automatically adds accept-encoding: gzip, indicating that the client supports gzip; For Response, when content-Encoding is gZIP and the client automatically adds gZIP support, it removes content-Encoding, content-Length, and then decompresses the Content of the Response.

CacheInterceptor

The CacheInterceptor intercept(Chain) method is as follows:

//CacheInterceptor.java
@Override
public Response intercept(Chain chain) throws IOException {
    
    // Response is cached in the Cache according to RequestResponse cacheCandidate = cache ! =null ? cache.get(chain.request()) : null;

    long now = System.currentTimeMillis();

    // create a CacheStrategy: network, cache, or both. What is CacheStrategy
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    / / get networkRequest
    Request networkRequest = strategy.networkRequest;
    // Get cacheResponse, which equals the cacheCandidate above
    Response cacheResponse = strategy.cacheResponse;

    / /...

    // This Response cache is invalid, close it
    if(cacheCandidate ! =null && cacheResponse == null) {
        closeQuietly(cacheCandidate.body()); 
    }

    If networkRequest is null and cacheResponse is null, the cache is mandatory but no cache is available. Therefore, Response with status code 504 and body empty is constructed
    if (networkRequest == null && cacheResponse == null) {
        return new Response.Builder()
            .request(chain.request())
            .protocol(Protocol.HTTP_1_1)
            .code(504)// Status code 504
            .message("Unsatisfiable Request (only-if-cached)")
            .body(Util.EMPTY_RESPONSE)
            .sentRequestAtMillis(-1L)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    }

   //2. NetworkRequest is null but cacheResponse is not null: Indicates that the cache is forced and exists. Therefore, the cached Response is returned directly
    if (networkRequest == null) {
        return cacheResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse))
            .build();
    }

    Response networkResponse = null;
    try {
        //networkRequest is not null, so a networkRequest can be made by calling chain.proceed(Request), which calls the next BridgeInterceptor intercept method, The networkResponse from the network request is returned
        networkResponse = chain.proceed(networkRequest);
       
    } finally {
        // Handle I/O exceptions or other exceptions when initiating network requests
        / /...
    }

    NetworkRequest is not null and cacheResponse is not null. Because cacheResponse is not null, you can determine whether to use cacheResponse based on the networkResponse obtained from the network request and the cached cacheResponse
    if(cacheResponse ! =null) {
        if (networkResponse.code() == HTTP_NOT_MODIFIED) {//HTTP_NOT_MODIFIED equals 304,304 indicates that the server resource has not been updated, so clients can directly use the local cache cacheResponse
            The cacheResponse header is updated with a Response constructed by cacheResponse
            Response 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 cacheResponse cache locally
            cache.trackConditionalCacheHit();
            cache.update(cacheResponse, response);
            // Return the Response of the construct
            return response;
        } else {// The server returns 200, the server resource is updated, so the cacheResponse client is invalid, close itcloseQuietly(cacheResponse.body()); }}// networkRequest is not null but cacheResponse is null or the server returns 200: CacheResponse is null, no cache is used, 200 is returned from the server, and the local cache is invalid. In both cases, the networkResponse is read from networkResponse and the Response is constructed for return
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if(cache ! =null) {// If Cache is not null, Cache is used
        if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
            // Cache Response in Cache
            CacheRequest cacheRequest = cache.put(response);
            return cacheWritingResponse(cacheRequest, response);
        }

        if (HttpMethod.invalidatesCache(networkRequest.method())) {
            try {
                cache.remove(networkRequest);
            } catch (IOException ignored) {
                // The cache cannot be written.}}}/ / returns the Response
    return response;
}
Copy the code

Okhttp’s Cache mechanism is defined in the Intercept (Chain) of the CacheInterceptor. We’ll look at two classes: Cache and CacheStrategy to understand the logic behind the Intercept (Chain).

1. Cache-cache implementation

Cache is an implementation of the Cache in okHTTP, internally using DiskLruCache, as follows:

public final class Cache implements Closeable.Flushable {
    / /...
    // All internal are implemented by DiskLruCache
    final DiskLruCache cache;
    
    // There is an InternalCache implementation that calls methods in the Cache
    final InternalCache internalCache = new InternalCache() {

        @Override public @Nullable Response get(Request request) throws IOException {
            return Cache.this.get(request);
        }

        @Override public @Nullable CacheRequest put(Response response) throws IOException {
            return Cache.this.put(response);
        }

        @Override public void remove(Request request) throws IOException {
            Cache.this.remove(request);
        }

        @Override public void update(Response cached, Response network) {
            Cache.this.update(cached, network);
        }

        @Override public void trackConditionalCacheHit(a) {
            Cache.this.trackConditionalCacheHit();
        }

        @Override public void trackResponse(CacheStrategy cacheStrategy) {
            Cache.this.trackResponse(cacheStrategy); }}// A Cache can be constructed using the following two constructors
    
    public Cache(File directory, long maxSize) {
        this(directory, maxSize, FileSystem.SYSTEM);
    }

    Cache(File directory, long maxSize, FileSystem fileSystem) {
        this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
    }

    // Here are the main methods
    
    @Nullable Response get(Request request) {
      	/ /...
    }

    @Nullable CacheRequest put(Response response) {
       / /...
    }

    void remove(Request request) throws IOException {
        / /...
    }

    void update(Response cached, Response network) {
      / /...
    }
    
    synchronized void trackConditionalCacheHit(a) {
       / /...
    }

    synchronized void trackResponse(CacheStrategy cacheStrategy) {
       / /...
    }

    @Override public void flush(a) throws IOException {
       / /...
    }

    @Override public void close(a) throws IOException {
        / /...}}Copy the code

It implements the InternalCache interface. The methods in this interface have the same name as the methods in the Cache. All of the methods in this implementation class call the corresponding methods in the Cache. InternalCache contains the same methods as the InternalCache. However, unlike InternalCache, the InternalCache contains more methods for external calls to flush(), close(), etc. InternalCache provides more control over the Cache, while InternalCache methods are only the basic operations of the Cache, such as get, put, remove, update methods, these methods are based on the logic of the Cache DiskLruCache implementation. For details, see DiskLruCache’s principle.

Okhttp does not use caches by default, which means that the Cache is null. If you want to use caches, you need to configure the Cache mechanism by using the following method:

// Cache path
File cacheDir = new File(Constant.PATH_NET_CACHE);
// A Cache is constructed with a two-parameter constructor
Cache cache = new Cache(cacheDir, 1024 * 1024 * 10);// The maximum size of the cache is 10M

// Then set it to OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .build();

Copy the code

After the above global setting, neither Cache nor InternalCache is null, because InternalCache is also created when the Cache is created, and okHTTP’s Cache mechanism takes effect.

Returning to the CacheInterceptor intercept method, which checks whether the cache is null in the first place, where does the CacheInterceptor cache come from? Is in the constructor, as follows:

public final class CacheInterceptor implements Interceptor {
    final @Nullable InternalCache cache;

    public CacheInterceptor(@Nullable InternalCache cache) {
        this.cache = cache;
    }
    / /...
}
Copy the code

See InternalCache examples, it is available in getResponseWithInterceptorChain when interceptor is added () by the client as the InternalCache assignment, as follows:

//RealCall.java
Response getResponseWithInterceptorChain(a) throws IOException {
    / /...
    // Add interceptors for caching
    interceptors.add(new CacheInterceptor(client.internalCache()));
    / /...
}
Copy the code

New CacheInterceptor(client.internalCache())), so let’s look at the client internalCache method:

//OkHttpClient.java
@Nullable InternalCache internalCache(a) {
    returncache ! =null ? cache.internalCache : internalCache;
  }
Copy the code

Cache is the global cache instance specified above. Therefore, it is not null. InternalCache is returned in the cache.

2. CacheStrategy – Caching policy

CacheStrategy is an implementation of the OKHTTP cache policy, which follows the HTTP cache policy. Therefore, to understand the OKHTTP cache policy, you need to understand HTTP caching: With the HTTP caching strategy in mind, let’s look at the CacheStrategy as follows:

public final class CacheStrategy {

    //CacheStrategy has two main member variables: networkRequest and cacheResponse
    public final @Nullable Request networkRequest;
    public final @Nullable Response cacheResponse;

    CacheStrategy(Request networkRequest, Response cacheResponse) {
        this.networkRequest = networkRequest;
        this.cacheResponse = cacheResponse;
    }
    
    / /...
    
    // Create CacheStrategy with factory mode
    public static class Factory {
        final long nowMillis;
        final Request request;
        final Response cacheResponse;

        public Factory(long nowMillis, Request request, Response cacheResponse) {
            this.nowMillis = nowMillis;
            this.request = request;
            this.cacheResponse = cacheResponse;
            / /...
        }

        public CacheStrategy get(a) {
            CacheStrategy candidate = getCandidate();
            / /...
            return candidate;
        }
        
         / /...}}Copy the code

CacheStrategy is created through the factory pattern and has two main member variables: NetworkRequest, cacheResponse, and CacheInterceptor’s Intercept method use a combination of networkRequest and cacheResponse in CacheStrategy to determine which policy to implement. Whether networkRequest is empty determines whether to request a network, and whether cacheResponse is empty determines whether to use cache. The four combinations of networkRequest and cacheResponse and their corresponding cache policies are as follows:

  • 1. If networkRequest is null and cacheResponse is null, Response with status code 504 is constructed because no cache is used and no networkRequest is made.
  • 2. NetworkRequest is null but cacheResponse is not null: The cache is in use and the cache exists within the validity period. Therefore, the cached Response is returned directly.
  • NetworkRequest is not null and cacheResponse is not null: The cache is used, but the cache exists in the client’s judgment, indicating that the cache has expired. Therefore, the server is requested to make a decision on whether to use the cached Response.
  • NetworkRequest is not null but cacheResponse is null: No cache is used, so the Response returned by the server is directly used

NetworkRequest and cacheResponse assign values by constructing parameters when creating a CacheStrategy. Where is the CacheStrategy created? Cachestrategy.factory (long, Request, Response).get() returns an instance of CacheStrategy, so CacheStrategy is created in the Factory’s get method, Let’s look at the Factory get method as follows:

//CacheStrategy.Factory
public CacheStrategy get(a) {
    CacheStrategy candidate = getCandidate();
    / /...
    return candidate;
}
Copy the code

As you can see, CacheStrategy is created using the Factory’s getCandidate method, which looks like this:

//CacheStrategy.Factory
private CacheStrategy getCandidate(a) {
    //1. No Response cache
    if (cacheResponse == null) {
        return new CacheStrategy(request, null);
    }

    //2. If the TLS handshake information is lost, make a network request directly
    if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
    }

    //3. Check whether the Response Cache is available according to the status code of Expired and the no-store of cache-control
    if(! isCacheable(cacheResponse, request)) {//Response The cache is not available
        return new CacheStrategy(request, null);
    }

    
    // get the Request CacheControl field CacheControl
    CacheControl requestCaching = request.cacheControl();
    If-modified-since or if-none-match is set for noCache and header of cache-control in Request
    if (requestCaching.noCache() || hasConditions(request)) {
        // Do not use the Response cache to make network requests directly
        return new CacheStrategy(request, null);
    }
    
 	// Go here to indicate that the Response cache is available

    // get the CacheControl field CacheControl of Response
    CacheControl responseCaching = cacheResponse.cacheControl();

    // Get how long the Response has been cached
    long ageMillis = cacheResponseAge();
    // Get how long the Response can be cached
    long freshMillis = computeFreshnessLifetime();

    if(requestCaching.maxAgeSeconds() ! = -1) 
        // Max-age is usually used
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
    }

    long minFreshMillis = 0;
    if(requestCaching.minFreshSeconds() ! = -1) {
        // The value is set to 0
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
    }

    long maxStaleMillis = 0;
    if(! responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() ! = -1) {
        / / take Max - stale,
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
    }

	5. Check whether the cache is expired and decide whether to use the Response cache: The Response cache duration is < max-stale + max-age
    if(! responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { Response.Builder builder = cacheResponse.newBuilder();if (ageMillis + minFreshMillis >= freshMillis) {
            builder.addHeader("Warning"."110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
            builder.addHeader("Warning"."113 HttpURLConnection \"Heuristic expiration\"");
        }
        //5.1. The cache does not expire. Use the Response cache directly
        return new CacheStrategy(null, builder.build());
    }

   The cache has expired. Check whether the Etag or last-Modified flag is set
    String conditionName;
    String conditionValue;
    if(etag ! =null) {
        conditionName = "If-None-Match";
        conditionValue = etag;
    } else if(lastModified ! =null) {
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
    } else if(servedDate ! =null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
    } else {
        // The cache does not set Etag or last-Modified, so the network request is made directly
        return new CacheStrategy(request, null);
    }

	// The cache is set to Etag or last-Modified, so add if-none-match or if-modified-since headers to construct the request and give it to the server to determine whether the cache is available
    Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
    Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

    Request conditionalRequest = request.newBuilder()
        .headers(conditionalRequestHeaders.build())
        .build();
    // Neither networkRequest nor cacheResponse is null
    return new CacheStrategy(conditionalRequest, cacheResponse);
}
Copy the code

The getCandidate() method determines the combination of networkRequest and cacheResponse based on the HTTP cache policy. From the getCandidate() method, we can see that there are two HTTP cache policies:

  • 1. Mandatory cache: The client participates in the decision whether to continue using the cache. The first time the client requests data, the server returns the expiration time of the cache: Expires or cache-control: When a client requests Expires again, it determines the expiration date of the Cache. If the client does not, it can continue to use the Cache. Otherwise, it does not use the Cache and requests the server again.
  • 2. Compare cache: The server participates in the decision whether to continue using cache. When the client requests data for the first time, the server identifies the cache: Last-modified/if-modified-since, Etag/ if-none-match and the data are returned to the client. When the client requests it again, the client sends the cache identifier to the server. The server determines the cache identifier based on the cache identifier. If 304 is returned, the client can continue to use the cache. Otherwise, the client cannot continue to use the cache and can only use the new response returned by the server.

In addition, forced caching takes precedence over comparison caching, and we post a diagram from the HTTP protocol caching mechanism, which explains the steps 1 to 5 of the getCandidate() method as follows:

Caching mechanism

Going back to the CacheInterceptor Intercept method, steps 1 through 4 are four well-commented combinations of networkRequest and cacheResponse from CacheStrategy, each of which corresponds to a caching policy. The Cache strategy is based on the dead HTTP Cache strategy written in the getCandidate() method. Combined with the implementation Cache of okHTTP local Cache, we draw the conclusion that okHTTP Cache mechanism = Cache Cache implementation + HTTP-based Cache strategy. The whole flowchart is as follows:

Now that we know about okHTTP’s caching mechanism, let’s move on to the next interceptor, ConnectInterceptor.

ConnectInterceptor

The ConnectInterceptor intercept(Chain) method is as follows:

//ConnectInterceptor.java
@Override
public Response intercept(Chain chain) throws IOException {
   
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    / / get the Transmitter
    Transmitter transmitter = realChain.transmitter();
	
    booleandoExtensiveHealthChecks = ! request.method().equals("GET");
    // create a new Exchange
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    // Call the proceed method, which calls the intercept method of the next interceptor, CallServerInterceptor
    // The proceed method is called with three parameters, which pass in Request, Transmitter, and the newly created Exchange
    return realChain.proceed(request, transmitter, exchange);
}
Copy the code

The intercept(Chain) method of the ConnectInterceptor is concise. It defines the connection mechanism of okHTTP. It first obtains a Transmitter, and then creates an Exchange through the Transmitter newExchange method. Pass it to the next interceptor, CallServerInterceptor. What is Exchange? Exchange is responsible for writing requests and reading responses from the IO stream created for the connection, completing the request/response process once. You’ll see what it really does in the CallServerInterceptor, but ignore it for now. Therefore, note 1’s newExchange method is the main logical implementation of the link mechanism, and we continue to look at the newExchange method as follows:

//Transmitter.java
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {

    / /... Omit exception handling

    //1. Find an exchange dec using ExchangeFinder's find method
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);

    // Create an Exchange and pass in the Exchange Ecodec instance codec, so the Exchange ecodec instance is held inside the Exchange
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    / /...
    
    return result;
}
Copy the code

Focus on note 1, ExchangeFinder object via the Transmitter in RetryAndFollowUpInterceptor early prepareToConnect method to create, it find methods is where the connection to create real, What is ExchangeFinder? ExchangeFinder is responsible for the creation of a connection, putting it into a pool, and taking it out of the pool if it already exists, so ExchangeFinder manages two important roles: RealConnection, RealConnectionPool, RealConnectionPool and RealConnection.

1. RealConnection – Realization of connection

The real realization of the Connection, the realization of the Connection interface, internal use of Socket to establish a Connection, as follows:

public interface Connection {
    // Return the Route used for this connection
    Route route(a);

    // Return the Socket used for this connection
    Socket socket(a);

    // If HTTPS is used, TLS handshake information is returned for establishing the connection, otherwise null is returned
    @Nullable Handshake handshake(a);

    Protocol is an enumeration, such as HTTP1.1 and HTTP2
    Protocol protocol(a);
}

public final class RealConnection extends Http2Connection.Listener implements Connection {

    public final RealConnectionPool connectionPool;
    / / routing
    private final Route route;
    // Use this rawSocket internally to establish connections at the TCP layer
    private Socket rawSocket;
    // If HTTPS is not used, then socket == rawSocket, otherwise the socket == SSLSocket
    private Socket socket;
    / / TLS handshake
    private Handshake handshake;
    // Application layer protocol
    private Protocol protocol;
    / / HTTP2 connections
    private Http2Connection http2Connection;
    // The okio library's BufferedSource and BufferedSink are javaIO's input and output streams
    private BufferedSource source;
    private BufferedSink sink;


    public RealConnection(RealConnectionPool connectionPool, Route route) {
        this.connectionPool = connectionPool;
        this.route = route;
    }


    public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
        / /...
    }

    / /...
}
Copy the code

RealConnection has a connect method that can be called externally to establish a connection. The connect method looks like this:

//RealConnection.java
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
    if(protocol ! =null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    // Route selection
    if (route.address().sslSocketFactory() == null) {
      if(! connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if(! Platform.get().isCleartextTrafficPermitted(host)) {throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy")); }}else {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS")); }}// Start the connection
    while (true) {
      try {
        if (route.requiresTunnel()) {// If it is channel mode, establish channel connection
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break; }}else {//1. Otherwise, the Socket connection is performed
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        // Establish an HTTPS connection
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        break;
      }
      / /... Omit exception handling

    if(http2Connection ! =null) {
      synchronized(connectionPool) { allocationLimit = http2Connection.maxConcurrentStreams(); }}}Copy the code

The connectSocket method is called to establish a Socket connection:

//RealConnection.java
private void connectSocket(int connectTimeout, int readTimeout, Call call,
                           EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    // Create sockets based on the proxy type
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    eventListener.connectStart(call, route.socketAddress(), proxy);
    rawSocket.setSoTimeout(readTimeout);
    try {
        //1. Establish a Socket connection
        Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    }
    / /... Omit exception handling

    try {
        // Get Socket input/output streams
        source = Okio.buffer(Okio.source(rawSocket));
        sink = Okio.buffer(Okio.sink(rawSocket));
    } 
     / /... Omit exception handling
}
Copy the code

Platform’s connectSocket method eventually calls rawSocket’s Connect () method to establish its Socket connection. After the Socket connection is established, the input and output streams source and sink can be obtained through the Socket connection. Okhttp can read data from source or write data to sink. Source and sink are types of BufferedSource and BufferedSink. They come from okio, a library that encapsulates java.io and java.nio. Okhttp relies on this library to read and write data. What’s good about Okio? Read more about Okio in this article.

RealConnectionPool – Connection pool

Connection pool, used to manage connection objects RealConnection, as follows:

public final class RealConnectionPool {

    / / thread pool
    private static final Executor executor = new ThreadPoolExecutor(
        0 /* corePoolSize */,
        Integer.MAX_VALUE /* maximumPoolSize */.60L /* keepAliveTime */, 
        TimeUnit.SECONDS,
        new SynchronousQueue<>(), 
        Util.threadFactory("OkHttp ConnectionPool".true));
 
    boolean cleanupRunning;
    // Clean up connection tasks, executed in executor
    private final Runnable cleanupRunnable = () -> {
        while (true) {
            // Call the cleanup method to perform the cleanup logic
            long waitNanos = cleanup(System.nanoTime());
            if (waitNanos == -1) return;
            if (waitNanos > 0) {
                long waitMillis = waitNanos / 1000000L;
                waitNanos -= (waitMillis * 1000000L);
                synchronized (RealConnectionPool.this) {
                    try {
                        // Call the wait method to wait
                        RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
                    } catch (InterruptedException ignored) {
                    }
                }
            }
        }
    };

    // Double end queue, save connection
    private final Deque<RealConnection> connections = new ArrayDeque<>();

    void put(RealConnection connection) {
        if(! cleanupRunning) { cleanupRunning =true;
            // Use thread pools to perform cleanup tasks
            executor.execute(cleanupRunnable);
        }
        // Insert the new connection into the queue
        connections.add(connection);
    }

    long cleanup(long now) {
        / /...
    }

    / /...
}
Copy the code

RealConnectionPool internally maintains a thread pool to perform the cleanupRunnable connection task, as well as a double-ended queue connections to cache connections that have been created. To create a connection, you need to go through a TCP handshake, or a TLS handshake if you’re using HTTPS. Both handshake processes are time-consuming, so connections is needed to cache the connection for reuse. Okhttp supports 5 concurrent connections. By default, each connection is keepAlive for 5 minutes. KeepAlive is the length of time a connection is kept alive after it has been idle.

When we first call RealConnectionPool’s PUT method to cache a new connection, if cleanupRunnable hasn’t been executed yet, it will first execute cleanupRunnable using the thread pool and then put the new connection into a two-ended queue. The cleanup method is called in cleanupRunnable, which returns the interval between now and the next cleanup, then calls the WIAT method to wait, then calls the cleanup method again, and so on. Let’s look at the cleanup logic for the cleanup method:

//RealConnectionPool.java
long cleanup(long now) {
    
    int inUseConnectionCount = 0;// The number of connections being used
    int idleConnectionCount = 0;// Number of idle connections
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    synchronized (this) {
        // Iterate over all connections, recording the number of idle connections and the number of connections in use
        for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
            RealConnection connection = i.next();

            / / if the connection is still in use, pruneAndGetAllocationCount through reference counting ways to judge whether a connection idle
            if (pruneAndGetAllocationCount(connection, now) > 0) {
                // Add 1 to the number of connections
                inUseConnectionCount++;
                continue;
            }
            
            // The connection is not in use

            // The number of free connections is increased by 1
            idleConnectionCount++;

            // Record the connection with the longest keepalive duration
            long idleDurationNs = now - connection.idleAtNanos;
            if (idleDurationNs > longestIdleDurationNs) {
                longestIdleDurationNs = idleDurationNs;
                // This connection will probably be removed because the idle time is too longlongestIdleConnection = connection; }}// Out of the loop

        // The default keepalive duration keepAliveDurationNs is 5 minutes. The maximum number of idle connections idleConnectionCount is 5
        if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) {// If the keepalive time of longestIdleConnection is greater than 5 minutes or the number of idle connections is greater than 5
            // Clear the longestIdleConnection connection from the queue
            connections.remove(longestIdleConnection);
        } else if (idleConnectionCount > 0) {// If the number of idle connections is less than 5 and the longestIdleConnection connection has not expired, clean up
            // Return the expiration time of the connection and clean it up next time
            return keepAliveDurationNs - longestIdleDurationNs;
        } else if (inUseConnectionCount > 0) {// If there are no free connections and all connections are still in use
            // Go back to keepAliveDurationNs and clean up after 5 minutes
            return keepAliveDurationNs;
        } else {
            // Reset cleanupRunning without any connection
            cleanupRunning = false;
            return -1; }}// After clearing the longestIdleConnection connection from the queue, close the socket for the connection, return 0, immediately clean up again
    closeQuietly(longestIdleConnection.socket());

    return 0;
}
Copy the code

From the cleanup method, okHTTP cleans up connections using the following logic:

1. Firstly, all connections are traversed, and idleConnectionCount and inUseConnectionCount are recorded. When recording the number of idle connections, longestIdleConnection with the longest idle time is also found. The connection is likely to be cleared;

2. After traversal, decide whether to clean longestIdleConnection according to the maximum idle time and the maximum number of idle connections.

2.1. If the idle time of longestIdleConnection is greater than the maximum idle time or the number of idle connections is greater than the maximum number of idle connections, the connection will be removed from the queue, and then close the socket of the connection, return 0, and immediately clean up again.

2.2. If the number of idle connections is less than 5 and the idle time of longestIdleConnection is less than the maximum idle time, that is, it has not expired for cleaning, then return the expiration time of the connection and clean it next time;

2.3. If there are no idle connections and all connections are still in use, the default keepAlive time is returned and cleaned up after 5 minutes.

2.4, There is no connection, idleConnectionCount and inUseConnectionCount are both 0, reset cleanupRunning and wait for the next PUT connection to execute cleanupRunnable again using the thread pool.

With RealConnectionPool and RealConnection in mind, let’s go back to the find method of ExchangeFinder, where the connection is created.

3. Connection creation (Connection mechanism)

The fing method for ExchangeFinder is as follows:

//ExchangeFinder.java
public ExchangeCodec find( OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
   / /...
    try {
        
      // Call the findHealthyConnection method to return RealConnection
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,  writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
        
      return resultConnection.newCodec(client, chain);  
    }
    / /... Omit exception handling
  }

 
  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
     // An infinite loop
    while (true) {
        
       // Call the findConnection method to return RealConnection
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);
        
	  / /...

        // Determine whether the connection is available
        if(! candidate.isHealthy(doExtensiveHealthChecks)) { candidate.noNewExchanges();continue;
        }

      return candidate;
    }
  
Copy the code

ExchangeFinder’s find method calls the findHealthyConnection method, which keeps calling until an available connection returns. ExchangeFinder’s findConnection method is as follows:

//ExchangeFinder.java
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;// Return the result of the available connection
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
       if (transmitter.isCanceled()) throw new IOException("Canceled");
      hasStreamFailure = false; .

	 //1. Try to use a connection that has already been created. The connection that has already been created may be restricted to create new streams
      releasedConnection = transmitter.connection;
      / / 1.1, if the connection has been created has restricted to create a new flow, the release of the connection (releaseConnectionNoEvents will connect the empty), and returns the connection Socket to shut downtoClose = transmitter.connection ! =null && transmitter.connection.noNewExchanges
          ? transmitter.releaseConnectionNoEvents()
          : null;

        // if the connection is still available, use it as a result.
        if(transmitter.connection ! =null) {
            result = transmitter.connection;
            releasedConnection = null;
        }

        // The connection that has been created cannot be used
        if (result == null) {
            2.1. Try to find the connection available in the connection pool. If the connection is found, the value will be stored in the Transmitter
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null.false)) {
                2.2. Find an available connection from the connection pool
                foundPooledConnection = true;
                result = transmitter.connection;
            } else if(nextRouteToTry ! =null) {
                selectedRoute = nextRouteToTry;
                nextRouteToTry = null;
            } else if (retryCurrentRoute()) {
                selectedRoute = transmitter.connection.route();
            }
        }
    }
	closeQuietly(toClose);
    
	/ /...
    
    if(result ! =null) {
        // if a connection is already available, return the result
        return result;
    }
    
    // No connection available

    // See if routing is required, multi-IP operation
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null| |! routeSelection.hasNext())) { newRouteSelection =true;
        routeSelection = routeSelector.next();
    }
    List<Route> routes = null;
    synchronized (connectionPool) {
        if (transmitter.isCanceled()) throw new IOException("Canceled");

        // If there is a next route
        if (newRouteSelection) {
            routes = routeSelection.getAll();
            // This is the second attempt to find available connections from the connection pool
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
                4.1. Find an available connection from the connection pool
                foundPooledConnection = true; result = transmitter.connection; }}// No available connection was found in the connection pool
        if(! foundPooledConnection) {if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }

           // create a Socket connection
            result = newRealConnection(connectionPool, selectedRoute); connectingConnection = result; }}If a connection is available in the connection pool, return the connection directly
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
        return result;
    }

    Call RealConnection's connect method to connect the Socket. This is described in RealConnection
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener);
    
    connectionPool.routeDatabase.connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
        connectingConnection = null;
        // If we just created a multiplexed connection with the same address, release this connection and get that one
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
            result.noNewExchanges = true;
            socket = result.socket();
            result = transmitter.connection;
        } else {
            5.2. Add the newly created connection to the connection pool
            connectionPool.put(result);
            Save the newly created connection to the Transmitter connection field
            transmitter.acquireConnectionNoEvents(result);
        }
    }
    
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    
    //5.4
    return result;
}
Copy the code

This findConnection method is the core of the ConnectInterceptor. We ignore multiple IP operations and multiplexing (HTTP2). Assume that this connection is not in the connection pool or Transmitter for the first time, so skip 1, 2, 3 and go directly to 5. Create a new connection and put it in the connection pool and Transmitter; Then we use the same Call to make the second request. At this time, there is the connection in the connection pool and the Transmitter, so we will go 1, 2 and 3. If the connection in the Transmitter is still available, we will return, otherwise we will get an available connection from the connection pool, so the general process of the whole connection mechanism is as follows:

What is the difference between a connection in Transmitter and a connection in a connection pool? As we know, every time a Call is created, a corresponding Transmitter will be created. A Call can send multiple calls (synchronous and asynchronous), and different calls have different transmitters. The connection pool is created when OkhttpClient is created, so the connection pool is shared by all calls. That is, all calls in the connection pool can be reused, while the connection in the Transmitter only corresponds to its corresponding Call, which can only be reused by all requests of this Call.

Now that you know the connection mechanism for OKHTTP, let’s move on to the CallServerInterceptor interceptor.

CallServerInterceptor

The CallServerInterceptor intercept(Chain) method is as follows:

//CallServerInterceptor.java
@Override
public Response intercept(Chain chain) throws IOException {
    
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    / / for Exchange
    Exchange exchange = realChain.exchange();
    / / get Request
    Request request = realChain.request();

	// Write the request header using Exchange's writeRequestHeaders(request) method
    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    
    if(HttpMethod.permitsRequestBody(request.method()) && request.body() ! =null) {
        / /...
        if (responseBuilder == null) {
            // Write the body of the request through okio
            if (request.body().isDuplex()) {
                exchange.flushRequest();
                BufferedSink bufferedRequestBody = Okio.buffer(
                    exchange.createRequestBody(request, true));
                request.body().writeTo(bufferedRequestBody);
            } else {
                BufferedSink bufferedRequestBody = Okio.buffer(
                    exchange.createRequestBody(request, false)); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); }}else {
           / /...}}else {
      exchange.noRequestBody();
    }

    / /...
    
    // Start getting the response from the network request
    
    Read the header of the response using the Exchange readResponseHeaders(Boolean) method
    if (responseBuilder == null) {
        responseBuilder = exchange.readResponseHeaders(false);
    }
    
    // After getting the Response, construct the Response with Builder mode
    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    / /...
    
    // Construct the body of Response
    if (forWebSocket && code == 101) {
        // Construct an empty body Response
        response = response.newBuilder()
            .body(Util.EMPTY_RESPONSE)
            .build();
    } else {
        // The body of the Response is read through the Exchange openResponseBody(Response) method, and the Response continues with the body of the Response
        response = response.newBuilder()
            .body(exchange.openResponseBody(response))
            .build();
    }
    
    / /...

    // Return Response Response
    return response;
  }
Copy the code

The Chain of the CallServerInterceptor method sends a request to the server and receives a response from the server. The Chain of the CallServerInterceptor method sends a request to the server and receives a response from the server.

1, send request:

1.1. Write the request header through Exchange’s writeRequestHeaders(Request) method;

If the body of the request is not empty, write the body of the request through okio.

2. Get the response:

2.1. Read the header of the response using the Exchange readResponseHeaders(Boolean) method.

2.2. Read the body of the Response through the Exchange openResponseBody(Response) method.

It’s created in the ConnectInterceptor and passed in the Process method, so you can get Exchange through the Chain. Exchange is responsible for writing requests and reading responses from the IO stream. To complete a request/response process, its internal read and write are carried out through an ExchangeCodec type COdec, while ExchangeCodec’s internal READ and write are carried out through Okio BufferedSource and BufferedSink. This process was analyzed in the last article and is not covered here.

conclusion

In conjunction with the previous article, we already have a good understanding of okHTTP. First, we initialize a Call instance at request time and then execute its execute() or enqueue() methods. Within the last execution to getResponseWithInterceptorChain () method, this method through the interceptor chain of responsibility, A response is obtained and handed to the user through the following interception processes: user-defined normal interceptor, retry interceptor, bridge interceptor, cache interceptor, connection interceptor, user-defined network interceptor, and access server interceptor. Okhttp’s request flow, caching, and connection mechanisms are the focus, and I learned a lot while reading the source code. Next time, I’ll look at its partner Retrofit.