CoolCoder, I will publish my latest article as soon as possible, welcome to follow. Interested in open source project analysis? Welcome to Android-open-source-Project-Cracking

Open source project OkHttp

In the Android/Java development world, I’m sure you’ve all heard of or used Square’s well-known web request library — OkHttp — github.com/square/okht… It is a testament to its quality that OkHttp is now used by most of the most famous open source projects such as Fresco, Glide, Picasso, Retrofit, and is still a work in progress.

Second, the problem of

Before analyzing the okHTTP source code, I want to ask a question. If we were to design a network request library ourselves, what would the library look like? What is the general structure?

I’m going to build a web request library with some of the core okHTTP design ideas, and hopefully give readers a sense of what okHTTP is all about and learn from it, not just its implementation.

I believe that if you read this patiently, you will not only have a better understanding of THE HTTP protocol, but you will also be able to learn the essence of world-class project thinking and improve your own way of thinking.

Three, think

First, we assume that the network request library to be built is called WingjayHttpClient, so, as a network request library, what is its basic function?

Receive the user’s request -> send the request -> receive the response result and return it to the user.

So from the user’s perspective, what you need to do is:

  1. To create aRequest: Set the destination URL inside; Request method, such as GET/POST. Some headers, such as Host and user-agent; If you upload a form in POST, you also need the body.
  2. Will create goodRequestPassed to theWingjayHttpClient.
  3. WingjayHttpClientTo carry outRequestAnd encapsulate the returned result into oneResponseTo the user. And aResponseIt should include statusCode like 200, some headers like Content-Type, maybe body

This is the beginning of a complete request. So let’s implement these three steps.

Four, prototype realization

Let’s start with a prototype httpClient that has only the most basic functionality.

1. CreateRequestclass

First, we need to create a Request class, using the Request class users can pass in their required parameters, the basic form is as follows:

class Request { String url; String method; Headers headers; Body requestBody; public Request(String url, String method, @Nullable Headers headers, @Nullable Body body) { this.url = url; . }}Copy the code

2.RequestObject passed toWingjayHttpClient

We can design WingjayHttpClient as follows:

class WingjayHttpClient { public Response sendRequest(Request request) { return executeRequest(request); }}Copy the code

3. PerformRequestAnd encapsulate the returned result into oneResponsereturn

class WingjayHttpClient { ... private Response executeRequest(Request request) { Socket socket = new Socket(request.getUrl(), 80); ResponseData data = socket.connect().getResponseData(); return new Response(data); }... } class Response { int statusCode; Headers headers; Body responseBody ... }Copy the code

Five, function expansion

Using the above prototype, it can be used as follows:

Request request = new Request("https://wingjay.com");
WingjayHttpClient client = new WingjayHttpClient();
Response response = client.sendRequest(request);
handle(response);
Copy the code

However, the above prototype is far from adequate for the general application requirements, so let’s add some common functional modules to it.

1. Reassemble the humble User Request into a regular HTTP Request

A simple user request is not enough to be an HTTP request. We need to add some headers to it. Such as Content-Length, Transfer-Encoding, user-Agent, Host, Connection, and Content-Type, if the request uses a cookie, So we’re also going to add cookies to this request.

We can extend the sendRequest(request) method above:

[class WingjayHttpClient]public Response sendRequest(Request userRequest) { Request httpRequest = expandHeaders(userRequest) return executeRequest(httpRequest)}private Request expandHeaders(Request userRequest) { if (userRequest.header("Connection") == null) { requestBuilder.header("Connection", "Keep-Alive") } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", Version.userAgent()) } ... }Copy the code

2. Automatic redirection is supported

Sometimes the URL we requested has been removed, and the server will return a 301 status code and a redirected new URL. In this case, we need to be able to support automatic access to the new URL without reporting an error to the user.

To redirect here have a testability URL:www.publicobject.com/helloworld…. After accessing and capturing packets, you can see the following information:

Therefore, after receiving the Response, we need to determine whether the status_code is redirected. If so, we need to resolve the new URl-location from the Response Header and automatically request a new URL. So, we can continue rewriting the sendRequest(Request) method:

[class WingjayHttpClient] private boolean allowRedirect = true; // user can set redirect status when building WingjayHttpClient public void setAllowRedirect(boolean allowRedirect) { this.allowRedirect = allowRedirect; } public Response sendRequest(Request userRequest) { Request httpRequest = expandHeaders(userRequest); Response response = executeRequest(httpRequest); switch (response.statusCode()) { // 300: multi choice; 301: moven permanently; // 302: moved temporarily; 303: see other; // 307: redirect temporarily; 308: redirect permanently case 300: case 301: case 302: case 303: case 307: case 308: return handleRedirect(response); default: return response; } } // the max times of followup request private static final int MAX_FOLLOW_UPS = 20; private int followupCount = 0; private Response handleRedirect(Response response) { // Does the WingjayHttpClient allow redirect? if (! client.allowRedirect()) { return null; } // Get the redirecting url String nextUrl = response.header("Location"); // Construct a redirecting request Request followup = new Request(nextUrl); // check the max followupCount if (++followupCount > MAX_FOLLOW_UPS) { throw new Exception("Too many follow-up requests:  " + followUpCount); } // not reach the max followup times, send followup request then. return sendRequest(followup); }Copy the code

Using the code above, we take the result of the original userRequest, determine whether the result is redirected, and automatically followup.

200 to 299: indicates that the request is successfully received, understood, and accepted. 300 to 399: indicates that further operations must be performed to complete the request. Client error, request has syntax error or request cannot be realized 500 to 599: server error, the server failed to realize a valid request

3. Supports the retry mechanism

The so-called retry is very similar to redirection, that is, by judging the Response state, if the connection to the server fails, etc., then you can try to get a new path to reconnect. The general implementation is very similar to redirection, which will not be described here.

4. Request & Response interception mechanism

This is a very core part.

Through the reassembly of request and redirection mechanism above, we can feel that after a request is created from the user, it will go through layers of processing before being sent out, and a response will also go through various processing and finally be returned to the user.

The author thinks that this is very similar to the network protocol stack. Users send simple data in the application layer, and then through the transmission layer and network layer, the request is really sent from the physical layer after layer of encapsulation. When the request result comes back, it is analyzed layer by layer, and finally the most direct result is returned to users.

Most importantly, each layer is abstract and unrelated!

By setting Interceptor, each Interceptor does two things:

  1. Receive the request encapsulated by the previous layer interceptor, and process the request itself. For example, add some headers, and pass them down after processing.
  2. Receive the response returned by the interceptor at the next level, then process the response itself, such as determining the returned statusCode, and proceed further.

So, we can define an abstract interface for the interceptor and then implement the concrete interceptor.

interface Interceptor {
	Response intercept(Request request);
	}
Copy the code

Is there a problem with the interceptor design?

Let’s imagine the interceptor receiving a request, intercepting it, and returning the result.

In practice, however, it can’t return a result, and it can’t pass down after processing the request because it doesn’t know where the next Interceptor is.

So, how to get all the interceptors together and pass them along in turn?

public interface Interceptor { Response intercept(Chain chain); interface Chain { Request request(); Response proceed(Request request); }}Copy the code

Let’s say we have three interceptors that need to be intercepted one by one:


List interceptors = new ArrayList<>();
interceptors.add(new MyInterceptor1());
interceptors.add(new MyInterceptor2());
interceptors.add(new MyInterceptor3());
Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, 0, originalRequest);
        chain.proceed(originalRequest);
Copy the code

The basic idea of the RealInterceptorChain is that we pass all the interceptors in, and then the chain passes a request to each interceptor in turn.

The interception process can be clearly seen in the following diagram:

RetryAndFollowupInterceptor is used for automatic retry and automatic redirection interceptor; BridgeInterceptor is an interceptor that extends the header of the Request. These two interceptors exist in OKHTTP, and in fact there are several interceptors in OKHTTP, but I won’t go into the details here.

  1. CacheInterceptor this layer is used to intercept requests and provide caching. When a request enters this layer, it automatically checks the cache and returns cached results. Otherwise, the request is passed down. Moreover, when the lower layer returns the response to this layer, it caches it on demand;

  2. The ConnectInterceptor layer is used to establish a connection with the target server

  3. The CallServerInterceptor layer is located at the bottom level. It makes requests directly to the server, receives a response from the server, and passes it up.

The above several are okhttp built-in, that is, need to WingjayHttpClient implementation. In addition to these functional interceptors, we also support the following two types of custom interceptors:

  1. Interceptors here are interceptors that intercept the user’s original request.

  2. NetworkInterceptor This is the lowest level request interceptor.

How do you distinguish the two? For example, I create two LoggingInterceptors, one on the Interceptors layer and the other on the NetworkInterceptor layer, and then access a redirected URL_1. When I’m done accessing URL_1, I access the redirected URL_2. For this process, the interceptors interceptor intercepts only URL_1 requests, while the NetworkInterceptor interceptors intercepts both URL_1 and URL_2 requests. See the picture above for specific reasons.

5. Management mechanism of synchronous and asynchronous Request pools

This is a very core part.

From the work above, we modified WingjayHttpClient to look like this:

class WingjayHttpClient { public Response sendRequest(Request userRequest) { Request httpRequest = expandHeaders(userRequest); Response response = executeRequest(httpRequest); switch (response.statusCode()) { case 300: case 301: case 302: case 303: case 307: case 308: return handleRedirect(response); default: return response; } } private Request expandHeaders(Request userRequest) {... } private Response executeRequest(Request httpRequest) {... } private Response handleRedirect(Response response) {... }}Copy the code

That is, WingjayHttpClient can now process individual requests synchronously.

However, in practice, a WingjayHttpClient may be used to process dozens of user requests at the same time, and these requests are divided into synchronous and asynchronous two different request modes. So we obviously can’t just stuff a request to WingjayHttpClient.

As we know, a request should be synchronous and asynchronous in addition to the HTTP protocol defined above. So where should this information be stored? There are two options:

  1. Putting Request directly in is fine in theory, but defeats the purpose. We initially hoped to use Request to construct a Request that conforms to THE HTTP protocol, which should contain the destination URL of the Request, the Request port, the Request method and so on, and the HTTP protocol does not care whether the Request is synchronous or asynchronous information

  2. It would be more appropriate to create a class that manages the state of the Request, so we can better split the responsibilities.

Therefore, we chose to create two classes, SyncCall and AsyncCall, to distinguish synchronous from asynchronous.

class SyncCall { private Request userRequest; public SyncCall(Request userRequest) { this.userRequest = userRequest; } } class AsyncCall { private Request userRequest; private Callback callback; public AsyncCall(Request userRequest, Callback callback) { this.userRequest = userRequest; this.callback = callback; } interface Callback { void onFailure(Call call, IOException e); void onResponse(Call call, Response response) throws IOException; }}Copy the code

Based on the above two classes, our usage scenarios are as follows:

WingjayHttpClient client = new WingjayHttpClient(); Request syncRequest = new Request("https://wingjay.com"); SyncCall syncCall = new SyncCall(request); Response response = client.sendSyncCall(syncCall); handle(response); AsyncCall asyncCall = new AsyncCall(request, new CallBack() { @Override public void onFailure(Call call, IOException e) {} @Override public void onResponse(Call call, Response response) throws IOException { handle(response); }}); client.equeueAsyncCall(asyncCall);Copy the code

As you can see from the above code, the responsibilities of WingjayHttpClient have changed: response = client.sendrequest (request); And now it is

response = client.sendSyncCall(syncCall);
client.equeueAsyncCall(asyncCall);
Copy the code

WingjayHttpClient needs to be modified as well. The basic idea is to add an internal request pool to manage all requests. So how do we design this request pool? There are two methods:

  1. It is also theoretically possible to create several containers directly inside WingjayHttpClient. When the user passes (a)syncCall to the client, the client automatically stores the call into the corresponding container for management.

  2. Creating a separate class to manage is obviously a better way to allocate responsibility. We define WingjayHttpClient’s responsibility as receiving a call, processing it internally and returning the result. This is the task of WingjayHttpClient, so how to manage the execution order and life cycle of these requests is not required by it.

Therefore, we create a new class: Dispatcher. This class does the following:

  1. Store constantly incoming from the outside worldSyncCallandAsyncCallIf the user wants to cancel, the user can iterate over all the calls to perform the cancel operation.
  2. forSyncCallSince it runs in real time, soDispatcherOnly need toSyncCallStore it before running and remove it after running.
  3. forAsyncCall.DispatcherStart an ExecutorService and keep pulling it outAsyncCallThen, we set the maximum number of requests to 64. If 64 requests are already in progress, we store the asyncCall in the wait area.

According to the design, the Dispatcher structure can be obtained:

class Dispatcher { private final Deque runningSyncCalls = new ArrayDeque<>(); private int maxRequests = 64; private final Deque waitingAsyncCalls = new ArrayDeque<>(); private final Deque runningAsyncCalls = new ArrayDeque<>(); private ExecutorService executorService; public void startSyncCall(SyncCall syncCall) { runningSyncCalls.add(syncCall); } public void finishSyncCall(SyncCall syncCall) { runningSyncCalls.remove(syncCall); } public void enqueue(AsyncCall asyncCall) { if (runningAsyncCalls.size() < 64) { runningAsyncCalls.add(asyncCall); executorService.execute(asyncCall); } else { readyAsyncCalls.add(asyncCall); } } public void finishAsyncCall(AsyncCall asyncCall) { runningAsyncCalls.remove(asyncCall); }}Copy the code

So with this Dispatcher, we can modify WingjayHttpClient to do that

response = client.sendSyncCall(syncCall);
client.equeueAsyncCall(asyncCall);
Copy the code

These two methods. The concrete implementation is as follows

[class WingjayHttpClient]
	private Dispatcher dispatcher;
	public Response sendSyncCall(SyncCall syncCall) {
		try {
			// store syncCall into dispatcher;
			dispatcher.startSyncCall(syncCall);
			// execute
			return sendRequest(syncCall.getRequest());
		} finally {
			// remove syncCall from dispatcher
			dispatcher.finishSyncCall(syncCall);
		}
	}
	public void equeueAsyncCall(AsyncCall asyncCall) {
		// store asyncCall into dispatcher;
		dispatcher.enqueue(asyncCall);
		// it will be removed when this asyncCall be executed
	}
	Copy the code

Based on the above, we can handle both synchronous and asynchronous requests well. The application scenarios are as follows:

WingjayHttpClient client = new WingjayHttpClient(); Request syncRequest = new Request("https://wingjay.com"); SyncCall syncCall = new SyncCall(request); Response response = client.sendSyncCall(syncCall); handle(response); AsyncCall asyncCall = new AsyncCall(request, new CallBack() { @Override public void onFailure(Call call, IOException e) {} @Override public void onResponse(Call call, Response response) throws IOException { handle(response); }}); client.equeueAsyncCall(asyncCall);Copy the code

Six, summarized

So far, we have basically explained all the core mechanisms in OKHTTP. We believe that readers have a detailed understanding of the overall structure and core mechanism of OKHTTP.

Please contact me if you have any questions.

Thank you very much!

wingjay

Welcome to pay attention to my lot: github.com/wingjay Jane and I book: www.jianshu.com/users/da333… And twitter iam_wingjay: weibo.com/u/162589265… If you have any questions, leave me a comment or send me an email at [email protected]