Article links:

Android OkHttp: Android OkHttp: Android OkHttp

Android OkHttp interceptors and Chains of Responsibility

preface

The previous article we mainly explain the basic use of OkHttp and synchronous request, asynchronous request source code analysis, I believe you are the basic process and effect of its internal have roughly understanding, remember we mentioned in the previous getResponseWithInterceptorChain chain of responsibility, This article will give you an in-depth source analysis of OkHttp’s internal five Interceptors and responsibility chain mode.

The body of the

What does the OkHttp Interceptors do

Interceptors are a powerful mechanism that can monitor, rewrite, and retry calls

How to understand this sentence? In simple terms, it is good, for example, you take a dish to Beijing to drive an examination, encountered five batches of bandits, but its strange is that their goals are money, some are color, each different; In the example you are to perform a task is equivalent to a network request, is always accompanied interceptors, their role is to take the request carries parameters with changes in the past, judgment, check and release, the actual is to implement the AOP (aspect oriented programming), about the understanding of AOP, believe that come in contact with the Spring framework is the most familiar with.

Let’s take a look at the official interceptor diagram

There are two types of interceptors: Application Interception and Network Interception. For this article, we will focus on OkHttp core.

So what are the interceptors inside the OkHttp system? What are the functions?

The Response getResponseWithInterceptorChain () throws IOException {/ / Build a full stack of interceptors. / / chain of responsibility List<Interceptor> interceptors = new ArrayList<>(); // Add Application Interception interceptors.addall (client. Interceptors ()); / / add responsible for handling errors, failure retry, redirect the interceptor RetryAndFollowUpInterceptor interceptors. Add (RetryAndFollowUpInterceptor); BridgeInterceptor Interceptors. add(new BridgeInterceptor(client.cookiejar ()))); Add (new CacheInterceptor(client.internalCache())); // Add (new CacheInterceptor(client.internalCache())); ConnectInterceptor Interceptors. add(new ConnectInterceptor(client));if (!forWebSocket) {/ / add Network interceptor (Network Interception) interceptors. AddAll (client.net workInterceptors ()); } // Add CallServerInterceptor interceptors.add(new CallServerInterceptor();forWebSocket)); // Pass the interceptors collection and parameters to the RealInterceptorChain constructor to create the responsibility Chain RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); // Invoke the execution of the chain of responsibilityreturn chain.proceed(originalRequest);
  }
Copy the code

From here you can see, getResponseWithInterceptorChain method first created a series of interceptors, and integrated into a collection of Interceptor, each Interceptor is responsible for different parts at the same time, to deal with different functions, The collection is then added to the RealInterceptorChain constructor to create the chain of interceptors. The chain of responsibility pattern is to manage multiple chains of interceptors. The understanding of the chain of responsibility pattern is, in short, ubiquitous in everyday development code

 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case 1:
                      System.out.println("I am the first interceptor:" + requestCode);
                    break;
                case 2:
                      System.out.println("I am the second interceptor:" + requestCode);
                    break;
                case 3:
                      System.out.println("I am the third interceptor:" + requestCode);
                    break;
                default:
                    break; }}}Copy the code

If you don’t understand this, you must be a fake Android programmer. Of course, this is a very simplified responsibility chain mode, many definitions have errors. This is just to give you a brief introduction of the chain of responsibility mode process, if you want to go deeper and apply it to the actual development, you need to read the relevant documentation, if you know what I mean.

Public Interface Interceptor {// Each Interceptor triggers a call to the next Interceptor according to the Chain of interceptors, until the last Interceptor does not trigger // When all interceptors in the Chain have been executed in sequence, Response Intercept (Chain Chain) throws IOException; Interface Chain {// Return Request Request (); Response proceed(Request Request) throws IOException; } } public final class RealInterceptorChain implements Interceptor.Chain { private final List<Interceptor> interceptors;  private final StreamAllocation streamAllocation; private final HttpCodec httpCodec; private final RealConnection connection; private final int index; private final Request request; private final Call call; private final EventListener eventListener; private final int connectTimeout; private final intreadTimeout;
  private final int writeTimeout;
  private int calls;

  public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
      HttpCodec httpCodec, RealConnection connection, int index, Request request, Call call,
      EventListener eventListener, int connectTimeout, int readTimeout, int writeTimeout) {
    this.interceptors = interceptors;
    this.connection = connection;
    this.streamAllocation = streamAllocation;
    this.httpCodec = httpCodec;
    this.index = index;
    this.request = request;
    this.call = call;
    this.eventListener = eventListener;
    this.connectTimeout = connectTimeout;
    this.readTimeout = readTimeout;
    this.writeTimeout = writeTimeout;
  }
   @Override public Response proceed(Request request) throws IOException {
    returnproceed(request, streamAllocation, httpCodec, connection); }}Copy the code

RealInterceptorChain implements the Interceptor. Chain method. In this code, we can see that the Interceptor is an interface class, and the RealInterceptorChain implements the Interceptor. You can see that we are actually calling the Method proceed from the RealInterceptorChain

  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if(index >= interceptors.size()) throw new AssertionError(); calls++; // Call the next interceptorin// Create the next chain of interceptors. Index +1 means that if you want to continue to access interceptors in the chain, you can only access them from the next interceptor, not from the current interceptor. The new interceptor chain will have one less interceptor than the previous one, RealInterceptorChain next = new RealInterceptorChain(Interceptors, streamAllocation, httpCodec, Connection, index + 1, request, call, eventListener, connectTimeout,readTimeout, writeTimeout); // Interceptor = interceptors.get(index); Response Response = interceptor.intercept(next); Response = interceptor.intercept(next);return response;
  }
Copy the code

You may be confused by this, but after a brief understanding, your question may be as follows

1. What does index+1 do?

2. How is the interceptor currently executed?

3. How is the chain of interceptors invoked and executed sequentially?

4. Understand the above 3 points, but each interceptor returns different results, how does it return a final result to me?

First of all, we should be clear about the process of responsibility chain mode. As mentioned above, scholars carry books, SoftBank and their families go to Beijing to take the exam. When they meet robbers on the way, the first group of robbers snatch books and release them, and the second group of robbers snatch SoftBank and release them. The RealInterceptorChain is the original Interceptor, the robber is the Interceptor, and the index+1 function is to create a new Interceptor chain, in simple terms, is the scholar (book, SoftBank, family) → book robber → scholar (SoftBank, Family member (loot robber (scholar (family member) By creating new RealInterceptorChain sprockets, the interceptors in the interceptors are executed in a chain of responsibility mode until the interceptors in the chain are processed and the final result is returned

// Interceptor = interceptors.get(index); List. Get (0),list. Get (1), list. This will take all the interceptors out of the interceptors. As mentioned earlier, Interceptor is an interface that okHTTP interceptors implement to handle their own business

RetryAndFollowUpInterceptor(Redirect interceptor)

Responsible for handling errors, retry failures, redirects

/**
 * This interceptor recovers from failures and follows redirects as necessary. It may throw an
 * {@link IOException} ifthe call was canceled. */ public final class RetryAndFollowUpInterceptor implements Interceptor { /** * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox, * curl, and wget follow 20; Safari follows 16; */ / Recommends 5. */ / Private static final int MAX_FOLLOW_UPS = 20; public RetryAndFollowUpInterceptor(OkHttpClient client, booleanforWebSocket) {
    this.client = client;
    this.forWebSocket = forWebSocket; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); ConnectInterceptor (); // This is the first Http interceptor that provides a Connection to the server. // This is the first Http interceptor that provides a Connection to the server. Global connection pool, 2. StreamAllocation = new streamAllocation (Client.connectionPool (), createAddress(Request.url ()), callStackTrace); int followUpCount = 0; Response priorResponse = null; / / the response in the endwhile (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response = null;
      boolean releaseConnection = true; Try {// Executes the next interceptor, i.e. BridgeInterceptor // passes the initialized connection object to the next interceptor, executes the next interceptor chain via the proceed method // The response returned here is the response returned by the next interceptor processing, With priorResponse, Response = ((RealInterceptorChain) chain). Proceed (request, streamAllocation, null, null); releaseConnection =false; } catch (RouteException e) { // The attempt to connect via a route failed. The request will not have been sent. // If there is an exception, determine whether to restore itif(! recover(e.getLastConnectException(),false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue; } catch (IOException e) { // An attempt to communicate with a server failed. The request may have been sent. boolean requestSendStarted = ! (e instanceof ConnectionShutdownException);if(! recover(e, requestSendStarted, request)) throw e; releaseConnection =false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources. if (releaseConnection) { streamAllocation.streamFailed(null); streamAllocation.release(); } } // Attach the prior response if it exists. Such responses never have a body. if (priorResponse ! = null) { response = response.newBuilder() .priorResponse(priorResponse.newBuilder() .body(null) .build()) .build(); Request followUp = followUpRequest(response); if (followUp == null) { if (! forWebSocket) { streamAllocation.release(); } // return response; } // closeQuietly(response.body()); If (++followUpCount > MAX_FOLLOW_UPS) {streamAllocation. Release (); throw new ProtocolException("Too many follow-up requests: " + followUpCount); } if (followUp.body() instanceof UnrepeatableRequestBody) { streamAllocation.release(); throw new HttpRetryException("Cannot retry streamed HTTP body", response.code()); } // Whether there is the same connection if (! sameConnection(response, followUp.url())) { streamAllocation.release(); streamAllocation = new StreamAllocation( client.connectionPool(), createAddress(followUp.url()), callStackTrace); } else if (streamAllocation.codec() ! = null) { throw new IllegalStateException("Closing the body of " + response + " didn't close its backing stream. Bad interceptor?"); } request = followUp; priorResponse = response; }}Copy the code

Response = ((RealInterceptorChain) chain).proceed(Request, streamAllocation, null, null), It is clear that proceed executes the new interceptor chain we passed in, thus forming the chain of responsibility, so that you can see how the chain of interceptors is executed in sequence. Actually RetryAndFollowUpInterceptor is mainly responsible for reconnection is failure, but note, not all of the network can even again request failed, so RetryAndFollowUpInterceptor internal will help us to detect abnormal network request and the response code judging, Failure reconnection can be performed if the conditions are met.

StreamAllocation: Establishes the objects needed to perform Http requests

Obtain the Connection to the server. 2. Connect the input and output streams used for data transmission on the server

Passed through the chain of responsibility mode until it is specifically used in the ConnectInterceptor,

(1) global connection pool, (2) connection line Address, (3) stack object

streamAllocation = new StreamAllocation( client.connectionPool(), createAddress(request.url()), callStackTrace); CreateAddress (request.url()) creates an OKIO-based Socket connection to the Address object based on the URL. The status processing flow is as follows

First, the WhIE (True) loop is executed. If the network request canceled, the streamAllocation resource is freed and an exception is thrown

2. Execute the next interceptor chain. If an exception occurs, go to the catch and determine whether to resume the request or not

3. If the priorResponse is not empty, combine the current Response returned with the Response returned after the previous Response (that is why a complete Response was returned).

4. Call followUpRequest to see if the response needs redirection or return to the current request if it does not

5. FollowUpCount redirection count +1 and check whether the maximum redirection count is reached. StreamAllocation is released and an exception is thrown

6. SameConnection checks if there are the same links. If so, StreamAllocation will release and rebuild

7. Reset request and save the current Response to priorResponse, continuing the while loop

To see RetryAndFollowUpInterceptor main implementation process:

1) Create the StreamAllocation object

2) call RealInterceptorChain. Proceed (…). Making network requests

3) Determine whether to request again according to abnormal results or response results

4) Invoke the next interceptor, process the response and return it to the previous interceptor

BridgeInterceptor(Bridge interceptor)

Responsible for setting up encoding, adding headers, keep-alive connections, and conversions between application and network layer request and response types

/** * Bridges from application code to network code. First it builds a network request from a user * request. Then it proceeds to call the network. Finally it builds a user response from the network * response. */ public final class BridgeInterceptor implements Interceptor { private final CookieJar cookieJar; public BridgeInterceptor(CookieJar cookieJar) { this.cookieJar = cookieJar; } @override // Public Response Intercept (interceptor.chain Chain) throws IOException {Request userRequest = chain.request(); Request.Builder requestBuilder = userRequest.newBuilder(); 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");
        }
        // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing // the transfer stream. boolean transparentGzip = false; if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { transparentGzip = true; requestBuilder.header("Accept-Encoding", "gzip"); } List
      
        cookies = cookiejar.loadForRequest (userRequest.url()); if (! Cookie.isempty ()) {// cookieJar, requestBuilder.header("Cookie", cookieHeader(cookies)); } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", Version.userAgent()); Responsenetworkresponse = chain.proceed(requestBuilder.build()); // The request is completed, and the return header is processed. If there is no custom configuration will not be parsed by a cookie HttpHeaders. ReceiveHeaders (cookieJar userRequest. Url (), networkResponse. Headers ()); Response.Builder responseBuilder = networkResponse.newBuilder() .request(userRequest); }
      Copy the code

And as you can see here, The BridgeInterceptor does all of its work before sending a network Request by adding content-Type, Content-Length, Transfer-Encoding, Host, Connection ( Default keep-alive), accept-encoding, user-agent, make it a Request that can send network requests. We specific to see HttpHeaders. ReceiveHeaders (cookieJar userRequest. Url (), networkResponse headers ()), Call the receiveHeaders static method in the Http header to convert the Response returned by the server into a Response available for the user Response

Public static void receiveHeaders(CookieJar CookieJar, HttpUrl URL, Headers Headers) {// No configuration is requiredif (cookieJar == CookieJar.NO_COOKIES) return; List<Cookie> cookies = cookie. parseAll(url, headers);if (cookies.isEmpty()) return; / / and then save, namely custom cookieJar saveFromResponse (url, cookies); }Copy the code

After we customize the Cookie configuration, the receiveHeaders method will parse the Cookie for us and add it to the header for saving

// After parsing the header, determine whether the server supports Gzip compression format, if yes, will be sent to Okio processingif (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); Responsebuilder.body (new RealResponseBody(strippedHeaders, okio.buffer (responseBody))); }return responseBuilder.build();
}
Copy the code
  1. TransparentGzip Determines whether the server supports Gzip compression

  2. Meets the requirement to determine whether the current header content-Encoding supports GZIP

  3. Check whether the Http header has a body

If the above conditions are met, change the response. body input stream to GzipSource type, obtain the decompressed data stream, remove header Content-Encoding and Content-Length in the Response, and construct a new Response to return.

BridgeInterceptor performs the following steps:

1) Responsible for converting user-constructed Request requests into network access requests

2) Execute the network Request for the qualified Request

3) Convert the Response after the network request Response (Gzip compression, Gzip decompression) into the Response available to users

CacheInterceptor(Cache interceptor)

Responsible for cache processing

/** Serves requests from the cache and writes responses to the cache. */ public final class CacheInterceptor implements Interceptor { final InternalCache cache; public CacheInterceptor(InternalCache cache) { this.cache = cache; } @override public Response Intercept (Chain Chain) throws IOException {// Obtains Response Response from the cache through Request cacheCandidate = cache ! = null ? cache.get(chain.request()) : null; long now = System.currentTimeMillis(); // get the system time // cache policy class, which determines whether to use caching or network requests // get the user-specified cache policy according to the request header, and get the networkRequest and cacheResponse according to the cache policy. CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); // networkRequest. If the value is null, no networkRequest is required. // Retrieves Response from the CacheStrategy cache. If this parameter is null, the cache is not used.if(cache ! = null) {// According to the cache policy, update statistics: number of requests, number of network requests, number of cache requests cache. TrackResponse (strategy); }if(cacheCandidate ! Closeecandidate (cachecandiDate.body ()); // The cache candidate wasn't applicable. Close it. } // If we'Re forbidden from using the network and the cache is insufficient, fail. // If we forbid using the network and the cache is insufficient, return 504.if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // If we don't need the network, we'Re done. // Returns the response without using the network request and with a cacheif (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

Copy the code

If you’re not familiar with the Http cache protocol before you get started, it’s a good idea to look into it when you come back. As you can see, the main purpose of the CacheInterceptor is to manage the cache. It also involves checking network status and updating the cache

1. Obtain the Response from the Request cache (Chain is an interface method in the Interceptor interface class, which processes and returns requests).

2. Get the current timestamp and the cache policy using the cacheStrategy. Factory class

public final class CacheStrategy {
  /** The request to send on the network, or null if this call doesn't use the network. */ public final @Nullable Request networkRequest; /** The cached response to return or validate; or null if this call doesn't use a cache. */
  public final @Nullable Response cacheResponse;

  CacheStrategy(Request networkRequest, Response cacheResponse) {
    this.networkRequest = networkRequest;
    this.cacheResponse = cacheResponse;
  }

  public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      if(candidate.networkRequest ! = null && request.cacheControl().onlyIfCached()) { // We're forbidden from using the network and the cache is insufficient. return new CacheStrategy(null, null); } return candidate; } /** Returns a strategy to use assuming the request can use the network. */ private CacheStrategy getCandidate() { If (cacheResponse == null) {if (cacheResponse == null) {if (cacheResponse == null) {if (cacheResponse == null) {if (cacheResponse == null) CacheStrategy(request, null); } // Drop the cached response if it'S missing a required handshake. // If the CACHED TLS handshake is missing, return to connect directlyif (request.isHttps() && cacheResponse.handshake() == null) {
        returnnew CacheStrategy(request, null); } // Check the status code of response,Expired time, and whether there is a no-cache tagif(! isCacheable(cacheResponse, request)) {returnnew CacheStrategy(request, null); } CacheControl requestCaching = request.cacheControl(); // If the request specifies no cached response and the current request is an optional GET requestif(requestCaching noCache () | | hasConditions (request)) {/ / request againreturnnew CacheStrategy(request, null); } CacheControl responseCaching = cacheResponse.cacheControl(); // If the immutable flag in the cached response istrue, the network is not requestedif (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }

      long ageMillis = cacheResponseAge();
      long freshMillis = computeFreshnessLifetime();

      if(requestCaching.maxAgeSeconds() ! = -1) { freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds())); } long minFreshMillis = 0;if(requestCaching.minFreshSeconds() ! = -1) { minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds()); } long maxStaleMillis = 0;if(! responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() ! = -1) { maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds()); }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\"");
        }
        return new CacheStrategy(null, builder.build());
      }

      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }
Copy the code

The CacheStrategy cache policy maintains two variables, networkRequest and cacheResponse, whose internal Factory class, the getCandidate method, uses logical judgment to select the best policy. If networkRequest is null, no networkRequest is made. If cacheResponse is returned as NULL, there is no valid cache.

3. Immediately following the above, trackResponse is called to determine whether the cache is empty (if there is cache, update statistical indicators according to the cache strategy: times of requests, times of network requests, times of cache)

4. If the cache is invalid, disable the cache

5. If networkRequest and cacheResponse are both null, no networkRequest is made and the cache is null, 504 is returned, indicating that the request fails

6. If the current network return request is empty and cache exists, the response is directly returned

// Response networkResponse = null; Try {// Next interceptor networkResponse = chain.proceed(networkRequest); } finally { // If we're crashing on I/O or otherwise, don't leak the cache body.
      if(networkResponse == null && cacheCandidate ! = null) { closeQuietly(cacheCandidate.body()); } } // If we have a cache response too,then we're doing a conditional get. if (cacheResponse ! = null) { if (networkResponse.code() == HTTP_NOT_MODIFIED) { 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 cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). cache.trackConditionalCacheHit(); cache.update(cacheResponse, response); return response; } else { closeQuietly(cacheResponse.body()); } } Response response = networkResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (cache ! = null) { if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) { // Offer this request to the 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. } } } return response; }Copy the code

The next section of code does a few things:

1) Execute the next interceptor, the ConnectInterceptor

2) After the completion of the responsibility chain, the final response data will be returned. If the returned result is empty, that is, in the case of no network, the cache will be closed

3) If the cacheResponse cache is not empty and the return code of the final response data is 304, the data is read directly from the cache, otherwise the cache is turned off

4) Directly return the final response data in network state

5) If the Http header has a response body and the Cache policy can Cache it, true= write the response body to the Cache and call it directly next time

6) Check whether the final response data is an invalid Cache method. If true, it will be removed from the Cache

7) Return Response

ConnectInterceptor

Responsible for establishing links with the server

/** Opens a connection to the target server and proceeds to the next interceptor. */ public final class ConnectInterceptor implements Interceptor { public final OkHttpClient client; public ConnectInterceptor(OkHttpClient client) { this.client = client; } @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); // Set up the object required to execute the Http request. // Mainly used for ① obtaining the Connection to the server. ② Connecting the input and output streams used for data transmission on the server realChain.streamAllocation(); // We need the network to satisfy this request. Possiblyfor validating a conditional GET.
    boolean doExtensiveHealthChecks = ! request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    returnrealChain.proceed(request, streamAllocation, httpCodec, connection); }}Copy the code

We talked about above redirect interceptors, found RetryAndFollowUpInterceptor StreamAllocation create is initialized, but no use, just follow the interceptor chain is passed to the next interceptor, will eventually spread to ConnectInterceptor in use. The ConnectInterceptor provides the following flow:

1. ConnectInterceptor Interceptor over StreamAllocation, StreamAllocation. NewStream.

2. Pass the RealConnection object just created for network IO and HttpCodec, which is the most critical for interacting with the server, to the interception behind.

ConnectInterceptor Interceptor code is very simple, but the key code or in the streamAllocation. NewStream (…). Method, in which all links are created.

public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        returnresultCodec; } } catch (IOException e) { throw new RouteException(e); }}Copy the code

The process here is basically

1. Call findHealthyConnection to create RealConnection for actual network connection (reuse if you can, create a new one if you can’t)

2. Create an HttpCodec object from the RealConnection obtained and return it in sync code

Let’s look specifically at findHealthyConnection

/**
   * Finds a connection and returns it ifIt is a healthy connection. If it is unhealthy the process is repeated * Until a healthy connection is found. If not, repeat the process until a healthy connection is found. */ private RealConnection findHealthyConnection(int connectTimeout, intreadTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled); // If this is a brand new connection, we can skip the extensive health checks. We can skip the extensive health check synchronized (connectionPool) {if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again. // Do a (potentially slow) check to confirm that the pooled connection is still good. If not, take it out of the pool and start over. if (! candidate.isHealthy(doExtensiveHealthChecks)) { noNewStreams(); continue; } return candidate; }}Copy the code

1. Start the while(true){} loop, which continuously fetches the Connection object from the findConnection method

2. Then synchronize the code block and return if candidate. SuccessCount ==0

3. Then determine if the candidate is unhealthy, execute destroy and re-call findConnection to get the Connection object under one of the following RealConnection conditions:

① The socket of the RealConnection object is not closed ② the input stream of the socket is not closed ③ the output stream of the socket is not closed ④ The connection is not closed at HTTP2

Let’s look at what’s going on in findConnection

 /**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    Connection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if(codec ! = null) throw new IllegalStateException("codec ! = null");
      if (canceled) throw new IOException("Canceled"); // Attempt to use an already-allocated connection. We need to be careful here because our // already-allocated Connection may have been restricted from creating new streams. // Select connection releasedConnection = this.connection; toClose = releaseIfNoNewStreams(); // Determine whether the reusable Connection is emptyif(this.connection ! = null) { // We had an already-allocated connection and it's good. result = this.connection; releasedConnection = null; } if (! reportedAcquired) { // If the connection was never reported acquired, don't report it as released! ReleasedConnection = null; releasedConnection = null; } // If RealConnection cannot be reused, it is retrieved from the connection poolif (result == null) { 
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if(connection ! = null) { foundPooledConnection =true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    closeQuietly(toClose);

    if(releasedConnection ! = null) { eventListener.connectionReleased(call, releasedConnection); }if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
    }
    if(result ! = null) { // If we found an already-allocated or pooled connection, we're done. return result; } // If we need a route selection, make one. This is a blocking operation. boolean newRouteSelection = false; if (selectedRoute == null && (routeSelection == null || ! routeSelection.hasNext())) { newRouteSelection = true; routeSelection = routeSelector.next(); } synchronized (connectionPool) { if (canceled) throw new IOException("Canceled"); if (newRouteSelection) { // Now that we have a set of IP addresses, make another attempt at getting a connection from // the pool. This could match due to connection coalescing. // Try again to get connection List
      
        routes = routeselection.getall () from ConnectionPool; for (int i = 0, size = routes.size(); i < size; i++) { Route route = routes.get(i); Internal.instance.get(connectionPool, address, this, route); if (connection ! = null) { foundPooledConnection = true; result = connection; this.route = route; break; } } } if (! foundPooledConnection) { if (selectedRoute == null) { selectedRoute = routeSelection.next(); } // Create a connection and assign it to this allocation immediately. This makes it possible // for an asynchronous cancel() to interrupt the handshake we'
      re about to do.
        route = selectedRoute;
        refusedStreamCount = 0;
        result = new RealConnection(connectionPool, selectedRoute);
        acquire(result, false);
      }
    }

    // If we found a pooled connection on the 2nd time around, we're done. if (foundPooledConnection) { eventListener.connectionAcquired(call, result); return result; } // Do TCP + TLS handshakes. This is a blocking operation. writeTimeout, connectionRetryEnabled, call, eventListener); routeDatabase().connected(result.route()); Socket socket = null; synchronized (connectionPool) { reportedAcquired = true; Put (connectionPool, result); // Pool the connection. // Pool the connection. // If another multiplexed connection to the same address was created concurrently, then // release this connection and acquire that one. if (result.isMultiplexed()) { socket = Internal.instance.deduplicate(connectionPool, address, this); result = connection; } } closeQuietly(socket); eventListener.connectionAcquired(call, result); return result; }Copy the code

There is a lot of code, and I have marked the key points for easy understanding. The general process is as follows:

1. Check whether the current StreamAllocation object has a Connection object. If so, return it.

2. If 1 is not obtained, obtain it from ConnectionPool

3. If 2 does not get the route address, the system iterates through all the route addresses and tries to get it from the ConnectionPool again

4. If 3 still cannot be obtained, try to create a new Connection for the actual network Connection

5. Add the new Connection to the ConnectionPool

As can be seen from the process, findConnection confirms its naming, which is mainly about how to find reusable Connection connections. If they can be reused, they can be reused, and if they can’t be reused, they can be rebuilt. Here we pay attention to result.connect(). How does it create a Connection that can actually make a network Connection

  public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener EventListener) {// Check whether the link has been established. The protocol identifier represents the request protocolif(protocol ! = null) throw new IllegalStateException("already connected"); RouteException routeException = null; List<ConnectionSpec> connectionSpecs = route.address().connectionspecs (); // Select link (tunnel link or Socket link) ConnectionSpecSelector ConnectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);while (true) {try {// Whether to establish a Tunnel linkif (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break; }}else {
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source= null; sink = null; handshake = null; protocol = null; http2Connection = null; eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e); }}}Copy the code

We leave the key code here to explain

1. Check whether the current protocol is null. If not, throw an exception (link already exists).

2. Create a list of Socket link configurations

3. Construct a ConnectionSpecSelector from the list created in 2 (select tunnel link or Socket link)

4. Then execute the while loop, route.requirestunnel () to determine whether a tunnel link is required, or establish the Socket link

5. Set the protocol protocol and perform the network request

ConnectionPool

Regardless of the protocol agreement is http1.1 Keep – Live or http2.0 Multiplexing mechanism need introduce the Connection pool to maintain the whole links, and OkHttp will link on the client and service side as a Connection class, RealConnection is the implementation class of Connection, ConnectionPool is responsible for the maintenance and management of all connections, and within a limited period of time to choose whether to reuse or keep open the Connection, timeout will be cleared in time

The Get method

// Whenever you want to reuse Connection, @Nullable RealConnection GET (Address Address StreamAllocation StreamAllocation, Route route) { assert (Thread.holdsLock(this));for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        returnconnection; }}return null;
  }
Copy the code

The for loop iterates through the Connections queue to determine whether the link is reusable based on address and route. If true, streamallocation. acquire returns connection. How to find and return a reusable Connection from a ConnectionPool

  /**
   * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to
   * {@link #release} on the same connection.
   */
  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if(this.connection ! = null) throw new IllegalStateException(); this.connection = connection; this.reportedAcquired = reportedAcquired; connection.allocations.add(new StreamAllocationReference(this, callStackTrace)); } public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();Copy the code

First, we assign the RealConnection object fetched from the connection pool to the Connection property of StreamAllocation, and then add a weak reference to the StreamAllocation object to the Allocations set of RealConnection objects The size of this array is used to determine whether a link’s load exceeds the maximum number of times specified by OkHttp.

Put method

When we talked about findConnection, if a ConnectionPool cannot find a reusable Connection, it creates a new Connection and adds it to the ConnectionPool using the Put method

 void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if(! cleanupRunning) { cleanupRunning =true; Execute (cleanupRunnable); // Execute (cleanupRunnable); } connections.add(connection); } private final Deque<RealConnection> connections = new ArrayDeque<>();Copy the code

Before adding to the queue, the code determines the current cleanupRunning (whether the current cleanup is in progress) and then performs the cleanupRunnable asynchronous cleanup task to retrieve invalid connections from the pool.

private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {// Next cleanup interval longwaitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L); Synchronized (connectionPool.this) {try {// Synchronize (connectionPool.this.wait)waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };
Copy the code

Here we focus on how the cleanUp method cleans up invalid connections

long cleanup(long now) {
    int inUseConnectionCount = 0; int idleConnectionCount = 0; RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // Find either a connection to evict, Or the time that the next eviction is due. Synchronized (this) {// loop over RealConnectionfor (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is inUse, keep searching. // Determine whether a connection is being used,truetheinUseConnectionCount+1,falseIdleConnectionCount + 1if(pruneAndGetAllocationCount (connection, now) > 0) {/ / are using the number of connectionsinUseConnectionCount++;
          continue; } // idleConnectionCount++; // If the connection is ready to be evicted, we're done. long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } // if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { // If more than 5 connections are marked, remove the connection.ve found a connection to evict. Remove it from the list, then close it below (outside
        // of the synchronized block).
        connections.remove(longestIdleConnection);
      } else if(idleConnectionCount > 0) {// If the number of free connections returned by the above processing is greater than 0. // A connection will be ready to evict soon.return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {// If all active connections are returned from the above processing. // All connections arein use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else{// No connections, idle orin use.
        cleanupRunning = false;
        return- 1; } } closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately.return 0;
  }
Copy the code
  1. The for loop traverses the Connections queue if the connection object is currently in useInUseConnectionCount +1 (number of active connections), jump out of the current judgment logic, execute the next judgment logic, otherwiseIdleConnectionCount +1 (number of free connections)
  2. judgeWhether the current connection object is idle for longer than known, true records
  3. ifThe number of idle connections exceeds 5 and the duration of idle connections exceeds 5, remove the socket from the pool, close the underlying socket, return wait time 0, and iterate over the pool again (this is an infinite loop)
  4. If 3 doesn’t satisfy,Check whether idleConnectionCount is greater than 0 (the number of free connections returned is greater than 0). Returns the difference between the keepalive time and the idle time
  5. If 4 does not meet the requirement, check whether the connection is in use and return the keepalive time if so
  6. If 5 is also not satisfied, there is no connection in the connection pool.Breaking out of the loop returns -1

This is the core code, referring to the mark-erase algorithm in the Java GC algorithm. The least active connection at the tag is cleared. Each time a new Connection is created, the ConnectionPool loops through the connections queue, flags inactive connections and clears them when they reach a certain number, which is at the heart of OkHttp Connection multiplexing.

Also note that pruneAndGetAllocationCount method is to judge the current connection object is idle or active

private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List<Reference<StreamAllocation>> references = connection.allocations;
    for (int i = 0; i < references.size(); ) {
      Reference<StreamAllocation> reference = references.get(i);

      if(reference.get() ! = null) { i++;continue;
      }

      // We've discovered a leaked allocation. This is an application bug. StreamAllocation.StreamAllocationReference streamAllocRef = (StreamAllocation.StreamAllocationReference) reference; String message = "A connection to " + connection.route().address().url() + " was leaked. Did you forget to close a response body?" ; Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace); references.remove(i); connection.noNewStreams = true; // If this was the last allocation, the connection is eligible for immediate eviction. if (references.isEmpty()) { connection.idleAtNanos = now - keepAliveDurationNs; return 0; } } return references.size(); }Copy the code

1. The for loop iterates through RealConnection’s StreamAllocationList to determine whether each StreamAllocation is empty. False indicates that no object references the StreamAllocationList. Perform the next logical decision (there is a pit, consider it an Easter egg)

If the current StreamAllocation set is empty due to the remove of 1, that is, there are no object references, return 0 to end

3. Return the result

CallServerInterceptor

Responsible for initiating network requests and receiving server responses

/** This is the last interceptor in the chain. It makes a network call to the server. */
public final class CallServerInterceptor implements Interceptor {
  private final boolean forWebSocket;

  public CallServerInterceptor(boolean forWebSocket) {
    this.forWebSocket = forWebSocket; } @override public Response Intercept (Chain Chain) throws IOException {// RealInterceptorChain realChain = (RealInterceptorChain) chain; HttpCodec HttpCodec = realchain.httpStream (); / / components used for network Http request need StreamAllocation StreamAllocation = realChain. StreamAllocation (); RealConnection Connection = (RealConnection) realchain.connection (); Request request = realChain.request(); long sentRequestMillis = System.currentTimeMillis(); realChain.eventListener().requestHeadersStart(realChain.call()); / / to write requests in the Socket head first information httpCodec. WriteRequestHeaders (request); realChain.eventListener().requestHeadersEnd(realChain.call(), request); Response.Builder responseBuilder = null; // Asks the server if it can send the message with the request bodyif(HttpMethod.permitsRequestBody(request.method()) && request.body() ! = 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 't get that, return// What we did get (such as a 4xx response) without ever transmitting the request body. If the server allows the request header to carry an Expect or 100-continue field, the response information can be obtained directlyif ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue"expectation was met. realChain.eventListener().requestBodyStart(realChain.call()); long contentLength = request.body().contentLength(); CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength)); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); // Write the request to the Socket. Request.body ().writeto (bufferedRequestBody); bufferedRequestBody.close(); realChain.eventListener() .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount); }else if(! connection.isMultiplexed()) { // If the"Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection // from being reused. Otherwise we're still obligated to transmit the request body to
        // leave the connection ina consistent state. streamAllocation.noNewStreams(); } // Complete the network request to write httpCodec.finishRequest();if(responseBuilder == null) { realChain.eventListener().responseHeadersStart(realChain.call()); / / read response information of the head responseBuilder = httpCodec. ReadResponseHeaders (false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    realChain.eventListener()
        .responseHeadersEnd(realChain.call(), response);

    int code = response.code();
     //
    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else{ response = response.newBuilder() .body(httpCodec.openResponseBody(response)) .build(); } / /if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }
Copy the code

In fact, the code is very simple, we only do OkHttp here on the explanation, about HttpCodec (HttpCodec interface, encapsulating the underlying IO can be directly used to send and receive data component flow object) can go to the relevant literature. The process is as follows

1. The first initialize the object, at the same time call httpCodec. WriteRequestHeaders (request) to write the Header information in the Socket

2. Check whether the server can send the message with the request body. If the return value is True, a special processing will be performed

If the body information is empty, write the request body information. If the body information is not multiplexed, close the write flow and Connection

4. Write the network request

5. Determine whether the body information is empty (that is, no special case is processed), read the header information of the Response directly, and write the original request, handshake condition, request time and the Response of the result time through the constructor mode

6. Check whether an empty body is returned by the status code and whether webSocket is used. If true, an empty body is returned; otherwise, the body information is read

7. If close is set, disconnect the Connection and close the write flow and Connection

8. Returns the Response

So far, the five interceptors inside OkHttp have been explained, and the flow is as follows