In the previous blog post, we introduced the overall architecture of OkHttp, and we’ll walk you through the details of OkHttp’s network access process with a specific network request. Because this section is closely tied to OkHttp’s interceptor concept, the two sections are presented together.

And a brief introduction to each interceptor:

  • RetryAndFollowUpInterceptor
  • BridgeInterceptor

  • CacheInterceptor

  • ConnectInterceptor

  • CallServerInterceptor

1. The Demo

Start with a simple asynchronous network access Demo:

OkHttpClient client = new OkHttpClient();        
Request request = new Request.Builder()
  .url("https://github.com")
  .build();

client.newCall(request).enqueue(new Callback() {
  @Override
  public void onFailure(Call call, IOException e) {
    Log.d("OkHttp"."Call Failed:" + e.getMessage());
  }

  @Override
  public void onResponse(Call call, Response response) throws IOException {
    Log.d("OkHttp"."Call succeeded:"+ response.message()); }});Copy the code

2. Initiate a request

Okhttpclient. newCall actually creates a RealCall instance:

  /**
   * Prepares the {@code request} to be executed at some point in the future.
   */
  @Override public Call newCall(Request request) {
    return new RealCall(this, request, false/ *for web socket */);
  }Copy the code

Realcall. enqueue: RealCall.enqueue: RealCall.enqueue: RealCall.enqueue: RealCall.enqueue: RealCall.enqueue: RealCall.enqueue: RealCall.enqueue: RealCall.enqueue

  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }Copy the code

As you can see from the code, RealCall is finally converted into an AsyncCall and put into the task queue. The distribution logic of the task queue is not mentioned here, but the implementation will be described later. All you need to know is that AsyncCall’s excute method will eventually be executed:

[RealCall.java]    
@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else{ responseCallback.onFailure(RealCall.this, e); } } finally { client.dispatcher().finished(this); }}}Copy the code

The logic of the execute method is not complicated. It can be simply described as:

  • callgetResponseWithInterceptorChainObtaining the server
  • Notify the task distributor (client.dispatcher) The task has ended

GetResponseWithInterceptorChain constructed a chain of interceptors, executed in sequence through the interceptor in the chain each blocker eventually get returned from the server.

3. Build a chain of interceptors

First of all look at getResponseWithInterceptorChain implementation:

[RealCall.java]  
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }Copy the code

Its logic is roughly divided into two parts:

  • Create a series of interceptors and place them in an array of interceptors. This section of interceptors includes both user-defined interceptors and framework interceptors
  • Create a chain of interceptorsRealInterceptorChainAnd execute the interceptor chainproceedmethods

Let’s take a look at the RealInterceptorChain implementation logic:

[RealInterceptorChain.java]
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 int calls;

  public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
                              HttpCodec httpCodec, RealConnection connection, int index, Request request) {
    this.interceptors = interceptors;
    this.connection = connection;
    this.streamAllocation = streamAllocation;
    this.httpCodec = httpCodec;
    this.index = index;
    this.request = request;
  }

  @Override public Connection connection() {
    return connection;
  }

  public StreamAllocation streamAllocation() {
    return streamAllocation;
  }

  public HttpCodec httpStream() {
    return httpCodec;
  }

  @Override public Request request() {
    return request;
  }

  @Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
  }

  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {

    ......
    // Call the next interceptor inthe chain. RealInterceptorChain next = new RealInterceptorChain( interceptors, streamAllocation, httpCodec, connection, index + 1, request); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); .returnresponse; }}Copy the code

As you can see from the core code in the proceed method, proceed actually does two things as well:

  • Create the next chain of interceptors. The incomingindex + 1Such that the next interceptor chain can only be accessed from the next interceptor
  • Execute index isindexIntercept method, and passes the next interceptor chain to that method

Then look at the first interceptor RetryAndFollowUpInterceptor intercept method:

[RetryAndFollowUpInterceptor.java]

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

    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response = null;
      boolean releaseConnection = true; 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(! 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. ...... Request followUp = followUpRequest(response); closeQuietly(response.body()); . }}}Copy the code

The key to this code is:

response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);Copy the code

This line of code is the PROCEED method that executes the next interceptor chain. We know that the intercept method of the next interceptor will be executed in the next interceptor chain. So the chain of execution alternates between interceptors and interceptors to complete all interceptor operations. This is also the chain execution logic of the OkHttp interceptor. The logic executed by an intercept method can be roughly divided into three parts:

  • The request is processed before the request is initiated
  • Call the next interceptor and get response
  • Response is processed and returned to the previous interceptor

This is the core logic of the OkHttp interceptor mechanism. So a network request is actually a process in which one interceptor executes its intercept method. In addition to the user-defined interceptors, there are several core interceptors that complete the core logic of network access, in order:

  • RetryAndFollowUpInterceptor
  • BridgeInterceptor
  • CacheInterceptor
  • ConnectIntercetot
  • CallServerInterceptor

4 RetryAndFollowUpInterceptor

As shown in the above code, RetryAndFollowUpInterceptor logic is responsible for the two parts:

  • Retry after the network request fails
  • When the server returns that the current request needs to be redirected, it directly initiates a new request and re-uses the current connection if conditions permit

5 BridgeInterceptor

public final class BridgeInterceptor implements Interceptor {
  private final CookieJar cookieJar;

  public BridgeInterceptor(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Log.e("haha"."BridgeInterceptor.intercept");
    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<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());
    }

    Response networkResponse = chain.proceed(requestBuilder.build());

    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);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }
}
Copy the code

BridgeInterceptor is responsible for the following:

  • Set the content length and content encoding
  • Set up gzip compression and unzip the content when it is received. Save the application layer processing data decompression trouble
  • Add a cookie
  • Set other headers, such asUser-Agent.Host.Keep-aliveAnd so on. Among themKeep-AliveIs a necessary step to achieve multiplexing

6. CacheInterceptor

[CacheInterceptor.intercept]

  @Override public Response intercept(Chain chain) throws IOException {
    Log.e("haha"."CacheInterceptor.intercept"); Response cacheCandidate = cache ! = null ? cache.get(chain.request()) : null; long now = System.currentTimeMillis(); CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse;if(cache ! = null) { cache.trackResponse(strategy); }if(cacheCandidate ! = null && cacheResponse == null) { closeQuietly(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 (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.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      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 CacheInterceptor’s responsibility is clear: it manages the Cache

  • If a network request has a valid Cache, the Cache is returned
  • Update the current cache when the server returns changes
  • If the current cache is invalid, delete it

7 ConnectInterceptor

[ConnectInterceptor.java]
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 {
    Log.e("haha"."ConnectInterceptor.intercept");
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = ! request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

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

The ConnectInterceptor intercept method has only one key line of code:

RealConnection connection = streamAllocation.connection();Copy the code

If the appropriate connection is found for the current request, it may be reused or recreated, and the connection returned is determined by the connection pool.

8. CallServerInterceptor

[CallServerInterceptor.java] @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; HttpCodec httpCodec = realChain.httpStream(); StreamAllocation streamAllocation = realChain.streamAllocation(); RealConnection connection = (RealConnection) realChain.connection(); Request request = realChain.request(); long sentRequestMillis = System.currentTimeMillis(); httpCodec.writeRequestHeaders(request); Response.Builder responseBuilder = null; . httpCodec.finishRequest();if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

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

    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

The CallServerInterceptor is responsible for making the actual access request to the server and reading the response when it receives the return from the server.

8. Overall process

The above are the core steps of the entire network access, which can be summarized as follows: