On the Android side, the better known web request frameworks are OkHttp and Retrofit, which in turn relies on OkHttp for web requests. So it’s no exaggeration to say that OkHttp is the most famous framework in the Android world, and today we’re going to take a closer look at it. In my pragmatic style, this post is definitely not about reading source code for the sake of reading it.

HTTP profile

To analyze the Http framework, let’s start with Http, which is the most common communication protocol used on the Internet. The so-called communication protocol is the format of data transmission agreed by both parties. So to understand Http, you only need to understand the format of Http data transmission. Here is the general format of Http data transmission on both sides.

The above list is not detailed enough because we are not looking at Http per se.

As you can see from the figure above, an Http request itself is pretty simple.

From the client’s point of view

  • Assemble Request (involving Request methods, urls, cookies, etc.)
  • The Client sends a request
  • Receives Response data from the server

Is it frighteningly simple? It’s kind of like an elephant filling a refrigerator.


A simple OkHttp request

Combining the above steps, let’s see how a simple network request can be done in the OkHttp framework.

//构造Request
Request req=new Request.Builder()
                        .url(url)
                        .build();
// Construct an HttpClient
OkHttpClient client=new OkHttpClient();
// Send the request
client.newCall(req)
        .enqueue(new Callback() {
            // Get the Response data from the server
            @Override
            public void onResponse(Call arg0, Response arg1) throws IOException {

            }

            @Override
            public void onFailure(Call arg0, IOException arg1) {
                // TODO Auto-generated method stub}});Copy the code

You see, the steps are exactly what we expected them to be. OKHttp encapsulates the complexity for you and gives you only what you want and what you need.

Since it encapsulates Http requests so simply, is its internal design very rigorous?

First, the OkHttp framework process is presented:

As you can see, constructing the Request and HttpClient are fairly simple, while sending the Request is quite complicated, which is certainly the best part of OKHttp.

Let’s take a look at the source code of the OKHttp framework by following the flow of the client Http request.

To construct the Request

public final class Request {... . publicstatic class Builder {
    private HttpUrl url;
    private String method;
    private Headers.Builder headers;
    private RequestBody body;
    private Objecttag; }}Copy the code

Use the Builder mode to insert the Request data and construct the Request. The Builder mode is believed to be very familiar, so only the parameters that can be constructed are given here.

It’s an overstatement to say that you’re building a Request, because you can see that there’s not enough data to actually build a legitimate Request, and OkHttp will add the rest of the information later, but at least from the developer’s point of view, the first step of building a Request is now complete.

Tectonic OKHttpClient

public class OkHttpClient implements Cloneable.Call.Factory.WebSocketCall.Factory {... . publicstatic final class Builder {
    Dispatcher dispatcher;
    Proxy proxy;
    List<Protocol> protocols;
    List<ConnectionSpec> connectionSpecs;
    final List<Interceptor> interceptors = new ArrayList<>();
    final List<Interceptor> networkInterceptors = newArrayList<>(); ProxySelector proxySelector; CookieJar cookieJar; Cache cache; InternalCache internalCache; SocketFactory socketFactory; SSLSocketFactory sslSocketFactory; CertificateChainCleaner certificateChainCleaner; HostnameVerifier hostnameVerifier; CertificatePinner certificatePinner; Authenticator proxyAuthenticator; Authenticator authenticator; ConnectionPool connectionPool; Dns dns; boolean followSslRedirects; boolean followRedirects; boolean retryOnConnectionFailure; int connectTimeout; int readTimeout; int writeTimeout; }... . @Override public Call newCall(Request request) {return new RealCall(this, request, false /* for web socket */); }... . }Copy the code

OKHttpClient implements the Call.Factory interface, creating an instance of the RealCall class (the Call implementation class). At the beginning, we saw that before sending the Request, we need to Call the newCall() method to create a Call object that points to the RealCall implementation class, which actually wraps instances of the Request and OKHttpClient classes. This makes it easy to use both in the following methods.

We can look at RealCall’s upper interface, Call:

public interface Call extends Cloneable {
  Request request();

  Response execute() throws IOException;

  void enqueue(Callback responseCallback);

  voidcancel(); boolean isExecuted(); boolean isCanceled(); Call clone(); interface Factory { Call newCall(Request request); }}Copy the code

Basically, most of the operations we’ll use are defined in this interface, which is the core of the OkHttp framework. After constructing the HttpClient and Request, we can manipulate the Request simply by holding a reference to the Call object.

Continuing with the above flow chart, we put the request into the request queue by calling realCall.enQueue ();

RealCall.java

 @Override 
 public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    // Do a bit of structural transformation to the code to help read
   Dispatcher dispatcher=client.dispatcher()
   // From here we can also feel that we should maintain an OkHttpClient instance globally,
    // Because each instance has a request queue, we only need one request queue
   dispatcher.enqueue(new AsyncCall(responseCallback));

    /* * This AsyncCall class inherits from Runnable *AsyncCall(responseCallback) which builds a Runnable thread *responseCallback is the response callback we expect */
  }Copy the code

We can go inside the Dispatcher to see the details of the enqueue() method and back to see what AsyncCall is doing.

Dispatcher.java

. ./** Queue waiting for asynchronous execution */
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
  private final Deque<RealCall> runningSyncCalls = newArrayDeque<>(); . . synchronizedvoid enqueue(AsyncCall call) {
    // If the request being executed is less than the set value,
    // The number of requests to the same host is smaller than the set value
    if (runningAsyncCalls.size() < maxRequests &&
            runningCallsForHost(call) < maxRequestsPerHost) {
        // Add to the queue to start executing the request
      runningAsyncCalls.add(call);
      // Get the current thread pool, or create one
      ExecutorService mExecutorService=executorService();
      // Execute thread
      mExecutorService.execute(call);
    } else {
        // Add to wait queuereadyAsyncCalls.add(call); }}Copy the code

In the dispatcher, it decides whether to queue a call or wait for it, and in the request queue, the request is executed in the thread pool.

Well, now we can go back to AsyncCall, the Runnable implementation class

RealCall.java

// It is an inner class of RealCall
//NamedRunnable implements the Runnable interface, encapsulating the run() method as execute().
final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    AsyncCall(Callback responseCallback) {
      super("OkHttp %s", redactedUrl());
      this.responseCallback = responseCallback;
    }

    String host() {
      return originalRequest.url().host();
    }

    Request request() {
      return originalRequest;
    }

    RealCall get() {
      return RealCall.this;
    }

    @Override 
    protected void execute() {
      boolean signalledCallback = false;
      try {
        // Return a Response if you don't agree with it
        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); }}}... .// Apparently the request is happening here
    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));
    // Wrap the request chain
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null.null.null.0, originalRequest);
    return chain.proceed(originalRequest);
  }Copy the code

In this getResponseWithInterceptorChain () method, we see a lot of Interceptor, according to the flowchart above, means that the network request process may in the end, is finally arrived I introduce the key, Because the Interceptor design is awesome.

Before Interceptor comes in, let’s be clear. So far, we’ve only had one incomplete Request, and the framework hasn’t done much. Rather than saying we’re coming to the end of the network Request, we’re just getting started. Adding the necessary information to the Request, failing to reconnect the Request, caching, getting the Response, etc., does nothing, which means that all work is left to the Interceptor, which is quite complicated as you can imagine.

Interceptor: You hot chickens.


Interceptor,

An Interceptor is an Interceptor that processes a Request or Response, while OkHttp links all interceptors together through a “Chain” that ensures they are executed one after another.

The design suddenly breaks down the problem. In this way, all the miscellaneous things can be grouped, and each Interceptor only performs a small class of things. This way, each Interceptor focuses only on its own work, and the complexity of the problem is reduced several times. And the design of this plug, greatly improve the scalability of the program.

Why the hell didn’t I think of that?

Calm the mood……

Let’s take a look at the Interceptor interface:

public interface Interceptor {
  // There is only one interface method
  Response intercept(Chain chain) throws IOException;
    //Chain
  interface Chain {
    // Chain wraps a Request
    Request request();
    / / get the Response
    Response proceed(Request request) throws IOException;
    // Get the current network connectionConnection connection(); }}Copy the code

The concept of a “chain” is not very easy to understand, so I’ll use a more vivid example of an annular pipeline to help you get a complete conceptual understanding of Interceptor.

“Wrapping the Request Chain recursively through each Interceptor, the Response received by the Request network will come back from each Interceptor in reverse order, returning the Response to the developer.”

I believe that the OkHttp Interceptor and its relationship to Chain should be conceptually clear from this example.

But I still want to talk about what each Intercept does and how they are called in turn at the code level.

So let’s go ahead and look at the code

    Response getResponseWithInterceptorChain() throws IOException {

    List<Interceptor> interceptors = new ArrayList<>();
    // Add the developer application layer's custom Interceptor
    interceptors.addAll(client.interceptors());
    // This Interceptor is a redirection that handles request failures
    interceptors.add(retryAndFollowUpInterceptor);
    // The Interceptor's job is to add headers or other information to the request
    // Do something nice with the returned Response (some information you may not need)
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // The Interceptor's responsibility is to check whether the cache exists, read the cache, update the cache, etc
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // The Interceptor's job is to establish a connection between the client and the server
    interceptors.add(new ConnectInterceptor(client));
    if(! forWebSocket) {// Add a developer - defined netlayer interceptor
      interceptors.addAll(client.networkInterceptors());
    }
    // The Interceptor's job is to send data to the server,
    // And receives a Response from the server
    interceptors.add(new CallServerInterceptor(forWebSocket));

    // a chain that wraps this request
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null.null.null.0, originalRequest);
    Pass the chain to the first Interceptor
    return chain.proceed(originalRequest);
  }Copy the code

So far, we can summarize some problems in development through the source code:

  • Interceptor’s execution is sequential, which means that when defining interceptors ourselves, should we pay attention to the order in which they are added?
  • When developers customize interceptors, there are two different types of interceptors that can be customized.

Starting with the last two lines above:

We first create a chain reference to the RealInterceptorChain implementation class, and then call the proceed (Request) method.

RealInterceptorChain.java

public final class RealInterceptorChain implements Interceptor.Chain {
  private final List<Interceptor> interceptors;
  private final StreamAllocation streamAllocation;
  private final HttpCodec httpCodec;
  private final Connection connection;
  private final int index;
  private final Request request;
  private int calls;

  public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
      HttpCodec httpCodec, Connection 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 Response proceed(Request request) throws IOException {Proceed (.....) Methods.
    return proceed(request, streamAllocation, httpCodec, connection);
  }

    This method is used to get the next Interceptor in the list and call its Intercept () method
  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      Connection connection) throws IOException {
    if (index >= interceptors.size()) throw newAssertionError(); calls++; . . .// Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    Get the first Interceptor from the list
    Interceptor interceptor = interceptors.get(index);
    // Then call the Interceptor's Intercept () method and wait for a ResponseResponse response = interceptor.intercept(next); . . return response; }Copy the code

From the above knowable, if there is no developers custom Interceptor, first call RetryAndFollowUpInterceptor, reconnection operation is responsible for the failure

RetryAndFollowUpInterceptor.java

. .// Call its own intercept() method directly@Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); . . Response response =null;
      boolean releaseConnection = true;
      try {
        / / here by continuing to call RealInterceptorChain. Proceed () this method
        Get the next Interceptor from the RealInterceptorChain list
        // Then continue calling interceptor.Intercept () and wait for a Response to return
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null.null);
        releaseConnection = false;
      } catch(RouteException e) { .... . }catch(IOException e) { .... . }finally{... . }}}... .Copy the code

Well, Interceptor is pretty much Interceptor, and OKHttp is pretty much OKHttp. If you want to study the details of each Interceptor, you’re welcome to read the source code. Now you won’t have any problems with the framework. There is too much space here to go on.

If you’re still wondering how OKHttp actually makes requests?

I’ll make a brief introduction: This request is provided by the CallServerInterceptor (the last Interceptor), and it also involves Okio, an IO framework that encapsulates read and write operations for streams, making it easier to access, store, and process data quickly. Eventually the request gets called to the socket level and gets a Response.


conclusion

This Interceptor from OKHttp is an excellent design. It not only breaks down problems, reduces complexity, but also improves scalability and maintainability. In short, it is worth studying carefully.

I remember a senior once told me that a good engineer is not a code writer who writes quickly and neatly, but a code designer who designs perfectly. Too often, we get so wrapped up in writing code that we forget how to design the code and how to effectively break down the difficulty and problems at the code level that the complexity of the project stays the same even as the requirements increase, and this is something we should all think about.

Every great engineer is a great designer.

‘.


errata

no