1. Introduction
As an Android developer, you’ll be familiar with OkHttp, the web request framework that dominates Android’s web requests. Whether you’re using OkHttp directly, Retrofit, or any other encapsulation of OkHttp, it’s all based on OkHttp. So it’s important to learn how to use OkHttp and understand how it works.
2. Basic usage of OkHttp
To understand the principle of the premise is to be able to use, according to its use steps to further analyze the principle of learning its operation. The basic steps to use OkHttp are as follows:
OkHttpClient OkHttpClient = new OkHttpClient(); Request Request = new request.builder ().url(url).build(); Call = okHttpClient.newCall(request); // Call = okHttpClient.newCall(request); Response Response = call.execute(); // Call. Enqueue (newCallback() {@override public void onFailure(Call Call, IOException e) {@override public void onFailure(Call Call, IOException e) Throws IOException {// child thread}});Copy the code
Of course, OkHttp supports various network request-related configurations, and its interceptor mechanism makes it easy for users to configure all requests uniformly. This is just to show the basics, nothing too simple to say, and OkHttp also supports synchronous and asynchronous requests.
3. Source code operation process
All the source code in this article is based on OkHttp3.11.0, so let’s start with the source code.
3.1 Creating an OkHttpClient object
As mentioned earlier, we can read the source code for the OkHttp usage flow to understand the operation flow principle. When we use it, we first build an OkHttpClient object. Look at how the source code is constructed.
public OkHttpClient() {
this(new Builder());
}
Copy the code
The implementation of OkHttpClient is a builder pattern. Constructor creates a Builder object. Look at the Builder class.
public static final class Builder { Dispatcher dispatcher; Proxy proxy; List<Protocol> protocols; List<ConnectionSpec> connectionSpecs; final List<Interceptor> interceptors = new ArrayList<>(); final List<Interceptor> networkInterceptors = new ArrayList<>(); 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; . publicBuilder() {// dispatcher = new dispatcher (); protocols = DEFAULT_PROTOCOLS; connectionSpecs = DEFAULT_CONNECTION_SPECS; proxySelector = ProxySelector.getDefault(); cookieJar = CookieJar.NO_COOKIES; socketFactory = SocketFactory.getDefault(); hostnameVerifier = OkHostnameVerifier.INSTANCE; certificatePinner = CertificatePinner.DEFAULT; proxyAuthenticator = Authenticator.NONE; authenticator = Authenticator.NONE; ConnectionPool = new connectionPool (); dns = Dns.SYSTEM; followSslRedirects =true;
followRedirects = true;
retryOnConnectionFailure = true;
connectTimeout = 10_000;
readTimeout = 10_000; writeTimeout = 10_000; }... }Copy the code
Constructor: OkHttpClient (); constructor: OkHttpClient (); constructor: OkHttpClient (); Note that the Dispatcher request Dispatcher and the ConnectionPool ConnectionPool are important objects that will play a significant role in the subsequent process. Because the Builder pattern can be achieved by OkHttpClient. Builder. The build method to obtain, so to see the build method of it.
public OkHttpClient build() {
return new OkHttpClient(this);
}
Copy the code
The build method is very simple, just new an OkHttpClient object.
3.2 Creating a Request Object
public final class Request { private final HttpUrl url; private final String method; private final Headers headers; private final RequestBody body; private final Object tag; private volatile CacheControl cacheControl; // Lazily initialized. private Request(Builder builder) { this.url = builder.url; this.method = builder.method; this.headers = builder.headers.build(); this.body = builder.body; this.tag = builder.tag ! = null ? builder.tag : this; }... public BuildernewBuilder() {
returnnew Builder(this); }... public static class Builder { private HttpUrl url; private String method; private Headers.Builder headers; private RequestBody body; private Object tag; publicBuilder() {// Default Get request this.method ="GET";
this.headers = new Headers.Builder();
}
private Builder(Request request) {
this.url = request.url;
this.method = request.method;
this.body = request.body;
this.tag = request.tag;
this.headers = request.headers.newBuilder();
}
public Builder url(HttpUrl url) {
if (url == null) throw new NullPointerException("url == null");
this.url = url;
returnthis; }... }Copy the code
The Request source code shows that it is also based on Builder mode. The default Builder constructor has two lines that set the default Request mode to GET and initialize the Request header. Usually we build a Request using the new request.builder ().url().build() method to pass in parameters such as the URL.
3.3 Call okHttpClient. newCall to create a Call object
The next third step is to create a Call object using the newCall method of the OkHttpClient object. Take a look at OkHttpClient’s newCall method.
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false/ *for web socket */);
}
Copy the code
This method actually calls the newRealCall method of the RealCall class and passes in the Request. Enter the RealCall class to look at the newRealCall method.
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
Copy the code
You can see that the newRealCall method just creates a RealCall object and returns an eventListener. Next, look at the RealCall constructor.
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket; / / initialization retry redirect interceptor enclosing retryAndFollowUpInterceptor = new retryAndFollowUpInterceptor (client,forWebSocket);
}
Copy the code
RealCall constructor can see, in addition to the incoming three parameters, also initialize a new RetryAndFollowUpInterceptor so a retry redirect interceptors, here involves OkHttp interceptor mechanism, in the first no matter or write down there is a blocker is initialization here. At this point, the third step ends and the final step is to send synchronous or asynchronous requests through call.execute() or call.enqueue.
3.4 Synchronizing request method Execute
Let’s start with the execute method of synchronization.
@override public Response execute() throws IOException {synchronized (this) {// Checks whether the call has been executedif (executed) throw new IllegalStateException("Already Executed");
executed = true; } captureCallStackTrace(); eventListener.callStart(this); Call client.dispatcher().executed(this); / / call the interceptor chain returns a Response in Response to the Response result = getResponseWithInterceptorChain ();if (result == null) throw new IOException("Canceled");
returnresult; } catch (IOException e) { eventListener.callFailed(this, e); throw e; } finally {// Call dispatcher's finish method client.dispatcher().finished(this); }}Copy the code
“Executed” is used to indicate whether the Call object was executed, so only once per Call is executed before an exception is thrown. Then call the client.dispatcher().executed(this) method. The dispatcher is the request dispatcher initialized in the OkHttpClient constructor.
Public final class Dispatcher {private int maxRequests = 64; Private int maxRequestsPerHost = 5; // Request execution thread pool, lazy load private @nullable ExecutorService ExecutorService; . Private Final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); Private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); Private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>(); . }Copy the code
First look at the member variables in the Dispatcher class. The default specifies the maximum number of requests, the maximum number of requests per host, a thread pool for performing calls, a queue of asynchronous requests ready to run, a queue of asynchronous requests running, and a queue of synchronous requests running. Then go back to the code for the executed method we called earlier:
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
Copy the code
The executed method is simple: add Call to a running synchronous request queue.
@override public Response execute() throws IOException {synchronized (this) {// Checks whether the call has been executedif (executed) throw new IllegalStateException("Already Executed");
executed = true; } captureCallStackTrace(); eventListener.callStart(this); Call client.dispatcher().executed(this); / / call the interceptor chain returns a Response in Response to the Response result = getResponseWithInterceptorChain ();if (result == null) throw new IOException("Canceled");
returnresult; } catch (IOException e) { eventListener.callFailed(this, e); throw e; } finally {// Call dispatcher's finish method client.dispatcher().finished(this); }}Copy the code
. Back to call the execute method, the client. The dispatcher () executed after the (this) performed getResponseWithInterceptorChain () this method, returns the request Response as a result, This method is to call the chain of interceptors that execute OkHttp, through the execution of one interceptor after another to complete the required parameters of the assembly request, set the cache policy, and finally complete the request and return the Response result data, which involves the interceptor mechanism of OkHttp. Let’s leave it for now, simply understand that the Response is returned through this method. Finally, the client.dispatcher().finished(this) method is executed in the finally code block.
void finished(RealCall call) {
finished(runningSyncCalls, call, false);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if(! calls.remove(call)) throw new AssertionError("Call wasn't in-flight!"); // synchronize here isfalse
if(promoteCalls) promoteCalls(); RunningCallsCount = runningCallsCount(); idleCallback = this.idleCallback; }if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
Copy the code
Notice that the parameter type passed in to the FINISHED method is RealCall. As you can see, Finished again called the overloaded method, first removing the call object from the synchronous run queue, and then not executing the promoteCalls reordering queue method because the reloading promoteCalls passed in was false. The runngCallsCount method is directly executed, which is used to count the total number of requests running.
public synchronized int runningCallsCount() {// Total number of requests running = asynchronous requests running + synchronous requests runningreturn runningAsyncCalls.size() + runningSyncCalls.size();
}
Copy the code
At this point, the synchronization request process is complete.
3.5 Asynchronous request method enqueue
For asynchronous requests, call the call.enqueue method:
@override public void enqueue(Callback responseCallback) {synchronized (this)if (executed) throw new IllegalStateException("Already Executed");
executed = true; } captureCallStackTrace(); eventListener.callStart(this); Dispatcher ().enqueue(new AsyncCall(responseCallback)); }Copy the code
The AsyncCall callback is wrapped into an AsyncCall object by calling client.dispatcher().enqueue(new AsyncCall(responseCallback)). Enter the dispatcher enqueue method:
Synchronized void enqueue(AsyncCall Call) {// Check whether the number of running asynchronous requests is smaller than the maximum number of requests and the number of host requests is smaller than the maximum number of requestsif (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else{ readyAsyncCalls.add(call); }}Copy the code
The executorService () method first determines whether the maximum number of asynchronous requests in the running queue and the maximum number of requests per host is reached. If so, the call is added to the prepare queue; otherwise, it is added to the run queue and assigned to the consumer thread pool for processing. So it’s easy to think that AsyncCall is actually a Runnable. Start with the executorService method to see how the thread pool is created.
public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
}
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher".false));
}
return executorService;
}
Copy the code
If the thread pool is empty, create a thread pool with zero core threads, a maximum number of integer. MAX_VALUE, and a thread timeout of 60 seconds. Although the maximum number of threads is integer.max_value, the run queue limits the maximum number of requests to 64 by default, so there are no memory problems caused by creating new threads all the time.
Let’s look at the AsyncCall code:
final class AsyncCall extends NamedRunnable {
......
}
Copy the code
AsyncCall is an inner class of RealCall that inherits an abstract class called NameRunnable.
public abstract class NamedRunnable implements Runnable {
protected final String name;
public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);
}
@Override public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
Copy the code
The NamedRunnable abstract class inherits the Runnable interface and calls the Execute method in the Run method. The Execute method in NamedRunnable is an abstract method implemented in its subclass AsyncCall.
@Override protected void execute() {
boolean signalledCallback = false; Try {/ / call getResponseWithInterceptorChain method to obtain the Response the Response the Response = getResponseWithInterceptorChain (); // Determine whether to cancelif (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true; responseCallback.onResponse(RealCall.this, response); } } catch (IOException e) { ...... } finally { client.dispatcher().finished(this); }}Copy the code
Also performed in the execute method of AsyncCall getResponseWithInterceptorChain this method to the Response, and then determine whether a request to cancel, cancel callback onFailure throws an exception, The request response is passed without canceling the callback to the onResponse method. The dispatcher().finished(this) method is still called in the finally block.
Void finished(AsyncCall Call) {//promoteCalls istrue
finished(runningAsyncCalls, call, true);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if(! calls.remove(call)) throw new AssertionError("Call wasn't in-flight!"); // Asynchronous requests call this methodif (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
Copy the code
This time the AysncCall type is passed in, so the overloaded FINISHED method is called, again removing the current call object from the run queue, but promoteCalls will be true and the finished method will be executed.
private void promoteCalls() {// The length of the running asynchronous request queue is greater than or equal to the maximum number of requests directlyreturn
if (runningAsyncCalls.size() >= maxRequests) return; // The asynchronous request queue ready to run is emptyreturn
if (readyAsyncCalls.isEmpty()) return; // Otherwise start looping to prepare the asynchronous request queue to runfor (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if(runningCallsForHost(call) < maxRequestsPerHost) {// Remove request i.remove(); // Add requests from the ready queue to the running request queue runningAsynccalls.add (call); ExecutorService ().execute(call); // Add request tasks to the running thread pool. } // If the length of the running request queue exceeds the maximum number of requestsreturnJump out of the loopif (runningAsyncCalls.size() >= maxRequests) return; }}Copy the code
The promoteCalls method puts requests from the ready queue into the running queue and adds them to the running thread pool. First determine whether the running asynchronous request queue is full and ready to run the asynchronous request queue for null, if don’t meet, illustrate the run queue at this time under and also request, in preparation for the queue requests from preparing the queue in the run queue to thread pool and removed from the ready queue. The total number of running requests is recalculated after promoteCalls executes. The asynchronous request process ends. The following is a flow chart of OkHttp’s running logic.
The Call will be sent to the corresponding queue via Dispatcher. The asynchronous Request will be taken out and executed by the thread pool. The interceptor chain will be called to get the response result and return such a process.
4. Interceptor mechanism
Until the next term to ignore getResponseWithInterceptorChain method, the interceptor mechanism will show you how, what the interceptor chain.
Response getResponseWithInterceptorChain () throws IOException {/ / create a stored List of interceptors collection List < Interceptor > interceptors = new ArrayList<>(); // Add interceptors to the collection. 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)); Chain Chain = new RealInterceptorChain(Interceptors, null, NULL, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); // Call the proceed method of the interceptor chainreturn chain.proceed(originalRequest);
}
Copy the code
GetResponseWithInterceptorChain method in the code is also not many, the first interceptor creates a stored List of interceptors collection, and to add a lot of interceptor, Included in the RealCall constructor to create retryAndFollowUpInterceptor interceptors, created a RealInterceptorChain after the real object, the interceptor chain into just a List, The chain.proceed method is finally called to get the response Respone back. Let’s go straight to the proceed method.
@Override public Response proceed(Request request) throws IOException {
returnproceed(request, streamAllocation, httpCodec, connection); } public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException { ...... RealInterceptorChain next = new RealInterceptorChain(Interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout,readTimeout, writeTimeout); Interceptor = interceptors.get(index); // Call its Intercept method and pass the next chain of interceptors into Response Response = interceptor.intercept(next); .return response;
}
Copy the code
You can see that the Proceed (Request) method again calls a four-parameter overloaded method. Leaving aside the exception throwdown judgment and looking at the main implementation, a chain object called the Next interceptor is created again. We then fetch the index interceptor in the interceptor collection, call the intercept method of the interceptor and pass in the new next interceptor chain to get the Response returned. Here’s a closer look, starting with the index, which is passed in when the interceptor chain is created.
public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
HttpCodec httpCodec, RealConnection connection, int index, Request request, Call call,
EventListener eventListener, int connectTimeout, int readInt writeTimeout) {this. Interceptors = interceptors; this.connection = connection; this.streamAllocation = streamAllocation; this.httpCodec = httpCodec; // Pass index this.index = index; this.request = request; this.call = call; this.eventListener = eventListener; this.connectTimeout = connectTimeout; this.readTimeout =readTimeout;
this.writeTimeout = writeTimeout;
}
Copy the code
Constructor to the index and List all the interceptors, see in getResponseWithInterceptorChain method for the first time the incoming value.
/ / getResponseWithInterceptorChain method created in the Interceptor Chain / / the first incoming index of 0 Interceptor. The Chain Chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis());Copy the code
// create the next interceptor chain in the chain.proceed 'method, RealInterceptorChain next = new RealInterceptorChain(Interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout,readTimeout, writeTimeout); Interceptor = interceptors.get(index); // Interceptor = interceptors.get(index); Response response = interceptor.intercept(next);Copy the code
The first Index passed in here is 0, and the next chain of interceptors created in the chain.proceed method passes in Index +1, which is 1. And then get to the interceptor is interceptors in the collection of 0 subscript interceptors, call it intercept method, add interceptors collection can be seen from above, the default is the first RetryAndFollowUpInterceptor blocker, So let’s go back to the Intercept method.
@Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); RealInterceptorChain realChain = (RealInterceptorChain) chain; . response = realChain.proceed(request, streamAllocation, null, null); . }Copy the code
Here, the implementation code of the interceptor itself in the Intercept method is omitted in advance, only the code of the implementation of the interceptor mechanism is concerned. Proceed, which is the newly created next interceptor chain, is called back to the Proceed method of the RealInterceptorChain, which creates a new chain of interceptors. The intercept method of the interceptor set with subscript 1 is called, and the proceed method of the interceptor chain with index 2 is called, and the cycle continues until all interceptors in the interceptor set have executed. The final interceptor, CallServerInterceptor, is executed, where the result of the request is returned wrapped in a Response. This is the core logic of the interceptor mechanism in OkHttp. The code logic shows that the chain of responsibility design pattern is being used.
5. The interceptor
In addition to adding application interceptors and network interceptors to the List of interceptors, there are five interceptors that are added by default. These five interceptors are very important and are mainly functional interceptors.
5.1 retry redirection interceptor (RetryAndFollowUpInterceptor)
This interceptor is used to retry and redirect the intercept method.
@Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); RealInterceptorChain realChain = (RealInterceptorChain) chain; Call call = realChain.call(); EventListener eventListener = realChain.eventListener(); StreamAllocation StreamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; int followUpCount = 0; Response priorResponse = null; // Determine whether to cancelwhile (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true; Try {// Call the next interceptor response = realchain.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(), streamAllocation,false, request)) {
throw e.getFirstConnectException();
}
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, streamAllocation, requestSendStarted, request)) throw e; releaseConnection =false;
continue;
} finally {
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; // Request followUp = followUpRequest(Response, streamallocation.route ()); } catch (IOException e) { streamAllocation.release(); throw e; }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());
}
if(! sameConnection(response, followUp.url())) { streamAllocation.release(); streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(followUp.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; }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
To review the workflow of the interceptor, the Intercept method does several things:
1. CreateStreamAllocation
Object.
Request request = chain.request(); RealInterceptorChain realChain = (RealInterceptorChain) chain; Call call = realChain.call(); EventListener eventListener = realChain.eventListener(); StreamAllocation StreamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation;Copy the code
Method starts by getting the Request, eventListener, and Call objects of the Request from the interceptor chain, and then creates a StreamAllocation object. The constructor passes connectionPool, Address, Call, eventListener, and callStackTrace from OkhttpClient. Address is created by createAddress, which determines whether the request is HTTPS, and returns an Address object based on the url and parameters in the client.
private Address createAddress(HttpUrl url) {
SSLSocketFactory sslSocketFactory = null;
HostnameVerifier hostnameVerifier = null;
CertificatePinner certificatePinner = null;
if (url.isHttps()) {
sslSocketFactory = client.sslSocketFactory();
hostnameVerifier = client.hostnameVerifier();
certificatePinner = client.certificatePinner();
}
return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
}
Copy the code
2. Start a loop to check whether the request is cancelled.
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Copy the code
A while loop is opened, which first determines that if it is cancelled it will release the streamAllocation and throw an exception.
3. CallRealInterceptorChain
theproceed
Method goes to the next interceptor to process the request.
try {
response = realChain.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(), streamAllocation,false, request)) {
throw e.getFirstConnectException();
}
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, streamAllocation, requestSendStarted, request)) throw e; releaseConnection =false;
continue;
} finally {
if(releaseConnection) { streamAllocation.streamFailed(null); streamAllocation.release(); }}Copy the code
RouteException indicates that the routing connection fails. IOException indicates that the connection to the server fails. After any exception is captured, the RECOVER method is invoked to determine whether the connection can be retried.
4. Determine the redirection operation
Request followUp; // Request followUp = followUpRequest(Response, streamallocation.route ()); // Request followUp = followUpRequest(Response, streamAllocation. } catch (IOException e) { streamAllocation.release(); throw e; } // If the redirected request is empty, response is returned without redirectionif (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
returnresponse; } closeQuietly(response.body()); StreamAllocation throws an exception if the number of redirects exceeds the maximum numberif (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: "+ followUpCount); } // The redirected body is non-repeatable and also frees streamAllocation to throw an exceptionif (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code()); } // Check if it is the same connection. If it is not, release the original streamAllocation and create a new streamAllocationif(! sameConnection(response, followUp.url())) { streamAllocation.release(); streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(followUp.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; }else if(streamAllocation.codec() ! = null) { throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
Copy the code
Redirection will followUpRequest to determine whether to redirect.
private Request followUpRequest(Response userResponse, Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
caseHTTP_PROXY_AUTH: Proxy selectedProxy = route ! = null ? route.proxy() : client.proxy();if(selectedProxy.type() ! = Proxy.Type.HTTP) { throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// "If the 307 or 308 status code is received in response to a request other than GET // or HEAD, the user agent MUST NOT automatically redirect the request"
if(! method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// Does the client allow redirects?
if(! client.followRedirects())return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// Don't follow redirects to unsupported protocols. if (url == null) return null; // If configured, don't follow redirects between SSL and non-SSL.
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if(! sameScheme && ! client.followSslRedirects())return null;
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if(! maintainBody) { requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if(! sameConnection(userResponse, url)) { requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
case HTTP_CLIENT_TIMEOUT:
// 408 s are rare in practice, but some servers like HAProxy use this response code. The
// spec says that we may repeat the request without modifications. Modern browsers also
// repeat the request (even non-idempotent ones.)
if(! client.retryOnConnectionFailure()) { // The application layer has directed us not to retry the request.return null;
}
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
if(userResponse.priorResponse() ! = null && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) { // We attempted to retry and got another timeout. Give up.return null;
}
if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
case HTTP_UNAVAILABLE:
if(userResponse.priorResponse() ! = null && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) { // We attempted to retry and got another timeout. Give up.return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
// specifically received an instruction to retry without delay
return userResponse.request();
}
return null;
default:
returnnull; }}Copy the code
In the followUpRequest method, the Response code in the Response can be judged by the Response code in the Response. The Response code 3XX in the HTTP method needs redirection. In the switch-case method, the redirection is processed according to different Response codes. Create a new Request return that returns NULL without redirection.
After obtaining the redirected Request, we will determine whether it is empty. If it is empty, we will return Response and release streamAllocation. It then continues to determine whether the redirection exceeds the maximum number of redirects (20 by default) and whether the redirected body is non-repeatable. If the redirection exceeds the maximum number of redirects or is non-repeatable, streamAllocation is also released and an exception is thrown. Finally, the sameConnection method is called to check whether the connection is the same. If the connection is not the same, the original streamAllocation is released and a new streamAllocation object is created.
5.2 BridgeInterceptor
Again, enter the Intercept method.
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if(body ! = null) { MediaType contentType = body.contentType(); / / set the contentTypeif(contentType ! = null) { requestBuilder.header("Content-Type", contentType.toString()); } long contentLength = body.contentLength(); // Set contentLength or transfer-Encodingif(contentLength ! = -1) { requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding"."chunked");
requestBuilder.removeHeader("Content-Length"); }} // Set hostif (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false)); } // Set the Connection headerif (userRequest.header("Connection") == null) {
requestBuilder.header("Connection"."Keep-Alive");
}
boolean transparentGzip = false; // Set accept-encoding to gzipif (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding"."gzip"); } List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url()); / / set cookiesif(! cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)); } // Set user-agentif (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent()); Response networkResponse = chain.proceed(requestBuilder.build()); / / response headers HttpHeaders. ReceiveHeaders (cookieJar userRequest. Url (), networkResponse headers ()); Response.Builder responseBuilder = networkResponse.newBuilder() .request(userRequest); // If gzip compression is supported, decompress gzipif (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) { GzipSource responseBody = new GzipSource(networkResponse.body().source()); Content-encoding and content-length strippedHeaders = networkResponse.headers().newBuilder() .removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type"); responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody))); } // Build Response to returnreturn responseBuilder.build();
}
Copy the code
BridgeInterceptor’s intercept method is logically simple. As its name indicates, this interceptor adds a request header to the request, decompresses the Response body with gizp if it supports GZIP, and returns the body decompressed in Response. I don’t want to talk about it because I think it’s fairly clear.
5.3 CacheInterceptor
@override public Response Intercept (Chain Chain) throws IOException {// Cache is not null, Response 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()); } // According to the cache policy, if no network is used and the cache is empty, a 504 Response error is returnedif (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 no network is used, the cached Response is returned directlyif (networkRequest == null) {
returncacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); } Response networkResponse = null; 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()); }} // Get the network response and have a cacheif(cacheResponse ! = null) {// Cache is used according to the response code of the network response if 304if (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()); }} / / go here means to use the network to return to the Response results of the Response Response. = networkResponse newBuilder () cacheResponse (stripBody (cacheResponse)) .networkResponse(stripBody(networkResponse)) .build();if(cache ! = null) {if(HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, NetworkRequest)) {// Add network requests 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 cache interceptor, as its name implies, must set up cache-related operations, which it does.
1. Read the cache
Response Response cacheCandidate = cache! = null ? cache.get(chain.request()) : null;Copy the code
2. Create a cache policy
CacheStrategy Strategy = new cacheStrategy.factory (now, chain-.request (), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse;Copy the code
3. Determine the cache usage based on the cache policy
According to the cache policy, if no network is used and the cache is empty, a 504 Response error is returnedif (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 no network is used, the cached Response is returned directlyif (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Copy the code
If no network is used and there is no cache, a wrong Response is directly returned according to the cache policy. If no network is used and there is cache, a cached Response is returned.
4. Proceed to the next interceptor using the network response
Response networkResponse = null; 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());
}
}
Copy the code
5. The network response is obtained and cached
// Get the network response with a cacheif(cacheResponse ! = null) {// Cache is used according to the response code of the network response if 304if (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()); }}Copy the code
If the Response is 304, the cache is used and the cached Response is returned. 6. Use the network response result and add it to the cache
/ / go here means to use the network to return to the Response results of the Response Response. = networkResponse newBuilder () cacheResponse (stripBody (cacheResponse)) .networkResponse(stripBody(networkResponse)) .build();if(cache ! = null) {if(HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, NetworkRequest)) {// Add network requests 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.
}
}
}
Copy the code
At this point, the network response results are used, and the network response results are stored in the cache if the cache is not empty and the response body cache policy allows caching.
5.4 ConnectInterceptor
@Override public Response intercept(Chain chain) throws IOException {
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, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
Copy the code
The ConnectInterceptor intercept method has surprisingly little code. It does three things. First, it gets an HttpCodec object through the newStream method of streamAllocation. Second, the Connection method of streamAllocation is called to obtain a RealConnection connection object. Finally, the usual call to realchain.proceed goes to the next interceptor. Remember the StreamAllocation object? Is the created instance in RetryAndFollowUpInterceptor intercept method, and in realChain. Incoming proceed method.
@Override public Response intercept(Chain chain) throws IOException { ...... StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; . response = realChain.proceed(request, streamAllocation, null, null); . }Copy the code
Now take a closer look at the four arguments to the PROCEED method of the interceptor chain.
@override public Response proceed(Request Request) throws IOException {returnproceed(request, streamAllocation, httpCodec, connection); } // Proceed public Response proceed(Request Request, StreamAllocation StreamAllocation, HttpCodec HttpCodec, RealConnection connection) throws IOException { ..... RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout,readTimeout, writeTimeout); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); .returnresponse; } RealInterceptorChain constructor public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection, int index, Request request, Call call, EventListener eventListener, int connectTimeout, intreadTimeout, 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;
}
Response getResponseWithInterceptorChain() throws IOException {
......
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
returnchain.proceed(originalRequest); } / / intercept method in RetryAndFollowUpInterceptor @ Override public Response intercept (Chain Chain) throws IOException {... StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; . response = realChain.proceed(request, streamAllocation, null, null); . } @override Public Response Intercept (Chain Chain) throws IOException {...... Response networkResponse = chain.proceed(requestBuilder.build()); .returnresponseBuilder.build(); } // In the CacheInterceptor @override public Response Intercept (Chain Chain) throws IOException {...... networkResponse = chain.proceed(networkRequest); .returnresponse; } //ConnectInterceptor @override Public Response Intercept (Chain Chain) throws IOException {....... HttpCodec httpCodec = streamAllocation.newStream(client, chain,doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
Copy the code
The RealInterceptorChain has two overloaded proceed methods. The four-parameter proceed method takes four types: Request, StreamAllocation, httpCodec, and RealConnection. The single-parameter method passes only one Request, and the remaining three parameters pass all the corresponding values in the member variable. The Proceed method creates a new chain of next interceptors, passing the four received objects to the new chain. And because members of the value of the variable is initialized in the incoming object, you can see in getResponseWithInterceptorChain approach to initialize the first interceptor chain in addition to Request the rest three types all pass null, In RetryAndFollowUpInterceptor intercept method created StreamAllocation object, then joined the chain in it called four. Proceed method Request and StreamAllocation into, The other two parameters are still empty. In the BridgeInterceptor and CacheInterceptor Intercept methods, no new type object is created, only a modified wrapper around the Request, so both are called as a pass-the-parameter method. The intercept method of the ConnectInterceptor provides the httpCodec object and the RealConnection object. The intercept method of the ConnectInterceptor provides the httpCodec and RealConnection object. Finally, it passes to the last CallServerInterceptor interceptor to send network requests to the server.
Returning to the ConnectInterceptor, we’ll start with the StreamAllocation class.
/** * This class coordinates the relationship between three entities: * * <ul> * <li><strong>Connections:</strong> physical socket connections to remote servers. These are * potentially slow to establish so it is necessary to be able to cancel a connection * currently being connected. * <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on * connections. Each connection has its own allocationlimit.which defines how many
* concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
* at a time, HTTP/2 typically carry multiple.
* <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
* its follow up requests. We prefer to keep all streams of a single call on the same
* connection for better behavior and locality.
* </ul>
**/
Copy the code
As you can see from the class comment, this class is used to coordinate Connections, Streams, and Calls. Look at the newStream method.
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {/ / get connection timeout setting time, read and write timeout timeout time time int connectTimeout = chain. ConnectTimeoutMillis (); intreadTimeout = chain.readTimeoutMillis(); int writeTimeout = chain.writeTimeoutMillis(); / / ping time-consuming int pingIntervalMillis = client. PingIntervalMillis (); / / connection failure whether reconnection Boolean connectionRetryEnabled = client. RetryOnConnectionFailure (); RealConnection resultConnection = findHealthyConnection(connectTimeout,readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks); / / from the connection gets a HttpCodec flow HttpCodec resultCodec = resultConnection. NewCodec (client, chain, this); synchronized (connectionPool) { codec = resultCodec;returnresultCodec; } } catch (IOException e) { throw new RouteException(e); }}Copy the code
As you can see from the method name, this method is used to create a stream that returns an HttpCodec type. The first step in the findHealthyConnection method is to get a connection by calling it.
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {// Start the loopwhile (trueRealConnection candidate = findConnection(connectTimeout,readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled); Synchronized (connectionPool) {if successCount equals 0, synchronized (connectionPool) {if (candidate.successCount == 0) {
returncandidate; }} // If it is not a new connection, check whether it is a healthy connection first. If it is not a healthy connection, skip this time and continue searchingif(! candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue; } // To this is a healthy connection then returnreturncandidate; }}Copy the code
FindHealthyConnection (); findConnection (); findConnection (); findConnection (); findConnection (); If not, skip the loop and continue looking. Then go to findConnection to see how the connection was found.
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false; RealConnection result = null; Route selectedRoute = null; Connection releasedConnection; Socket toClose; Synchronized (connectionPool) {// Exception detectionif (released) throw new IllegalStateException("released");
if(codec ! = null) throw new IllegalStateException("codec ! = null");
if (canceled) throw new IOException("Canceled"); ReleasedConnection = this.connection; Socket toClose = releaseIfNoNewStreams(); socket toClose = releaseIfNoNewStreams(); // Assign the connection to result if the current connection is not emptyif(this.connection ! = null) { // We had an already-allocated connection and its good. result = this.connection; releasedConnection = null; }if(! reportedAcquired) { // If the connection was never reported acquired, dont report it as released! releasedConnection = null; }if(result == null) {// if result is null, get a connection from the connectionPool.if(connection ! // Find the connection from the connection pool and assign it to result foundPooledConnection =true;
result = connection;
} else{// Set route to selectedRoute = route; } } } closeQuietly(toClose);if(releasedConnection ! = null) { eventListener.connectionReleased(call, releasedConnection); }if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if(result ! = null) {// If it is found in the connection pool, return it directlyreturnresult; } // Create a Boolean newRouteSelection = if a routing option is requiredfalse;
if(selectedRoute == null && (routeSelection == null || ! routeSelection.hasNext())) { newRouteSelection =true;
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if(newRouteSelection) {List<Route> routes = routeselection.getall (); // Loop the routes to get connections from the connection poolfor (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; // Assign connection to result result = connection; this.route = route;break; }}}if(! foundPooledConnection) {if(selectedRoute == null) { selectedRoute = routeSelection.next(); } route = selectedRoute; refusedStreamCount = 0; Result = new RealConnection(connectionPool, selectedRoute); acquire(result,false);
}
}
// If we found a pooled connection on the 2nd time around, were done.
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
returnresult; } // Do TCP + TLS handshakes. This is a blocking operation. Result. connect(connectTimeout, connectTimeout)readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true; // add the connection to the connectionPool internal.instance. put(connectionPool, result); // 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
The findConnection method is a long one, and the general logic is to search the connection pool first and create a connection if it cannot find one. Let’s look at it bit by bit.
ReleasedConnection = this.connection; Socket toClose = releaseIfNoNewStreams(); socket toClose = releaseIfNoNewStreams(); // Assign the connection to result if the current connection is not emptyif(this.connection ! = null) { // We had an already-allocated connection and its good. result = this.connection; releasedConnection = null; }Copy the code
We first use the current connection, which is not empty and can create a new stream, and assign it to result.
if(result == null) {// if result is null, get a connection from the connectionPool.if(connection ! // Find the connection from the connection pool and assign it to result foundPooledConnection =true;
result = connection;
} else{// Set route to selectedRoute = route; }}Copy the code
The second step is to retrieve a connection from the connection pool if the current connection is not available. Here, the internal.instance. get method looks for connections from the connection pool and dives in.
/**
* Escalate internal APIs in {@code okhttp3} so they can be used from OkHttp's implementation * packages. The only implementation of this interface is in {@link OkHttpClient}. */ public abstract class Internal { }Copy the code
Internal is an abstract class whose only implementation is annotated in OkHttpClient.
Internal.instance = new Internal() {... @Override public RealConnection get(ConnectionPool pool, Address address, StreamAllocation streamAllocation, Route route) {returnpool.get(address, streamAllocation, route); }... }Copy the code
The Internal. Instance get method in OkHttpClient calls the connection pool get method to get a 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 streamAllocation. Acquire method is called after the connection is obtained in the Get method of ConnetionPool, which then returns to the streamAllocation class.
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)); }Copy the code
The acquire method in turn assigns the connection to the member variable, so that the connection from the pool is acquired in StreamAllocation. Go back to the findConnection method and continue with step 3.
if(result ! = null) {// If it is found in the connection pool, return it directlyreturnresult; } // Create a Boolean newRouteSelection = if a routing option is requiredfalse;
if(selectedRoute == null && (routeSelection == null || ! routeSelection.hasNext())) { newRouteSelection =true;
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if(newRouteSelection) {List<Route> routes = routeselection.getall (); // Loop the routes to get connections from the connection poolfor (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; // Assign connection to result result = connection; this.route = route;break; }}}Copy the code
If it finds a connection from the pool, it returns the result, otherwise it goes down to create a routing selection and then loops through all the routes to get a connection from the pool again.
Result = new RealConnection(connectionPool, selectedRoute); . // Do TCP + TLS handshakes. This is a blocking operation. Result. connect(connectTimeout, connectTimeout)readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true; // add the connection to the connectionPool internal.instance. put(connectionPool, result);Copy the code
If it is not already found, a new connection is created, the connect method is called, and the new connection is pooled. This completes the process of finding and retrieving connections and continues with the connect method of RealConnection.
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
EventListener eventListener) {
......
while (true) {try {// The route is used to determine whether tunnel transmission is performedif(route.requirestunnel ()) {// If yes, connect to the tunnel.readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break; }}elseConnectSocket (connectTimeout,readTimeout, call, eventListener); }... }Copy the code
The connect method omits all kinds of judgment and only looks at the connection-related code. Here, it determines whether a tunnel connection is needed from the route first, and calls the connection tunnel or connection socket according to the result. I’m just looking at the socket connection here, and I’m going to go to the connectSocket method.
private void connectSocket(int connectTimeout, int readTimeout, Call Call, EventListener EventListener) throws IOException {// Obtain Proxy Proxy = route.proxy(); Address = route.address(); / / create a socket depending on the type of agent rawSocket = proxy. The type () = = proxy. The DIRECT | | proxy. The type () = = proxy. The HTTP? address.socketFactory().createSocket() : new Socket(proxy); eventListener.connectStart(call, route.socketAddress(), proxy); rawSocket.setSoTimeout(readTimeout); Socket platform.get ().connectSocket(rawSocket, route.socketAddress(), connectTimeout); } catch (ConnectException e) { ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCause(e);
throw ce;
}
// The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
// More details:
// https://github.com/square/okhttp/issues/3245
// https://android-review.googlesource.com/#/c/271775/Try {// Get socket input and output through Okiosource = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
if(NPE_THROW_WITH_NULL.equals(npe.getMessage())) { throw new IOException(npe); }}}Copy the code
The connectSocket method is platform.get (). ConnectSocket. The Platform class is designed for multi-platform adaptation and will eventually call the connectSocket method in the AndroidPlatform. That calls the socket’s connect method.
@Override public void connectSocket(Socket socket, InetSocketAddress address, int connectTimeout) throws IOException { ...... socket.connect(address, connectTimeout); . }Copy the code
Once you know how to establish a connection, return to the newStream method of StreamAllocation.
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
......
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
returnresultCodec; }... }Copy the code
The newCodec method of the resultConnection is then called.
public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
StreamAllocation streamAllocation) throws SocketException {
if(http2Connection ! = null) {return new Http2Codec(client, chain, streamAllocation, http2Connection);
} else {
socket.setSoTimeout(chain.readTimeoutMillis());
source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
return new Http1Codec(client, streamAllocation, source, sink); }}Copy the code
The newCodec method says if you’re using HTTP2.0 create Http2Codec and return otherwise create Http1Codec and return, both of these classes are implementations of HttpCodec. Return to the original ConnectInterceptor intercept method.
@Override public Response intercept(Chain chain) throws IOException {
......
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
Copy the code
Call after the newStream streamAllocation. The connection method to obtain the required connection.
public synchronized RealConnection connection() {
return connection;
}
Copy the code
5.5 Sending the CallServerInterceptor to the server for the response
The last interceptor sends a request to the server to read the response.
@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(); realChain.eventListener().requestHeadersStart(realChain.call()); / / write requests head httpCodec writeRequestHeaders (request); realChain.eventListener().requestHeadersEnd(realChain.call(), request); Response.Builder responseBuilder = null; // Determine whether the request body is allowed to be sent and whether there is a request bodyif(HttpMethod.permitsRequestBody(request.method()) && request.body() ! = null) {// Expect:100-continueif ("100-continue".equalsIgnoreCase(request.header("Expect"))) {// send the request header httpCodec.flushRequest(); realChain.eventListener().responseHeadersStart(realChain.call()); / / receive response headers responseBuilder = httpCodec. ReadResponseHeaders (true);
}
if(responseBuilder == null) {// If the server is allowed to send the request body realchain-.eventListener ().requestBodyStart(realchain-.call ()); long contentLength = request.body().contentLength(); CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength)); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); // Write 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(); } // End the request by sending httpCodec.finishRequest();if(responseBuilder == null) { realChain.eventListener().responseHeadersStart(realChain.call()); / / read response headers responseBuilder. = httpCodec readResponseHeaders (false); Response Response Response = responseBuilder.request (request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); Int code = response.code(); // If the response code is 100, read the response header again to construct the response resultif (code == 100) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
responseBuilder = httpCodec.readResponseHeaders(false); response = responseBuilder .request(request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); code = response.code(); } realChain.eventListener() .responseHeadersEnd(realChain.call(), response); //WebSocket or response code 101 builds a response with an empty response bodyif (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{/ / will be added to the response body response in the response object = response. NewBuilder (). The body (httpCodec. OpenResponseBody (response)). The build (); }...return response;
}
Copy the code
Again, take a look at the main process and see what steps the Intercept method takes.
1. Write the request header
/ / write requests head httpCodec writeRequestHeaders (request);Copy the code
The request header is first written to the httpCodec object.
2. Send the request body
Response.Builder responseBuilder = null; // Determine whether the request body is allowed to be sent and whether there is a request bodyif(HttpMethod.permitsRequestBody(request.method()) && request.body() ! = null) {// Expect:100-continueif ("100-continue".equalsIgnoreCase(request.header("Expect"))) {// send the request header httpCodec.flushRequest(); realChain.eventListener().responseHeadersStart(realChain.call()); / / receive response headers responseBuilder = httpCodec. ReadResponseHeaders (true);
}
if(responseBuilder == null) {// If the server can receive the request body realchain-.eventListener ().requestBodyStart(realchain-.call ()); long contentLength = request.body().contentLength(); CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength)); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); // Write 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(); } // End the request by sending httpCodec.finishRequest();Copy the code
Before sending the request body, check whether the request method runs the sending request body and whether there is a request body. If there is a request body and it supports sending, check whether there is Expect:100-continue in the request header. If there is a request header, read the server’s response header first. The purpose of this request header is to ask the server if it can receive data to process the request body, as explained in this article. The server accepts it, writes the request body, and finally calls httpCodec.finishRequest to end sending the request.
3. Obtain the response
if(responseBuilder == null) { realChain.eventListener().responseHeadersStart(realChain.call()); / / read response headers responseBuilder. = httpCodec readResponseHeaders (false); Response Response Response = responseBuilder.request (request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); Int code = response.code(); // If the response code is 100, read the response header again to construct the response resultif (code == 100) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
realChain.eventListener()
.responseHeadersEnd(realChain.call(), response);
//forWebSocket istrueAnd the response code is 101, construct a response with an empty response bodyif (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{/ / will be added to the response body response in the response object = response. NewBuilder (). The body (httpCodec. OpenResponseBody (response)). The build (); }...return response;
Copy the code
If the Response code is 100, read the Response header again. If the Response code is 100, read the Response header again. Rebuild a Response result object Response to obtain the Response code. Finally, check whether the forWebSocket and Response code are 101. If forWebS is true and the Response code is 101, add an empty Response body to the previously built Response object; otherwise, add the decoded Response body to the Response object. Method returns a Response object.
6. Cache
In the previous section of 5.3 CacheInterceptor, we described how the cache bar resolver works without delving into the cache itself. OkHttp caching is based on the HTTP protocol caching mechanism. For advice on HTTP protocol caching that is not clear, see this article and go straight to the relevant implementation classes in OkHttp.
6.1 CacheControl class
The CacheControl class describes the CACHE-Control header of the HTTP protocol in OkHttp. It encapsulates the values of the cache-Control header.
private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds,
boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds,
int minFreshSeconds, boolean onlyIfCached, boolean noTransform, boolean immutable,
@Nullable String headerValue) {
this.noCache = noCache;
this.noStore = noStore;
this.maxAgeSeconds = maxAgeSeconds;
this.sMaxAgeSeconds = sMaxAgeSeconds;
this.isPrivate = isPrivate;
this.isPublic = isPublic;
this.mustRevalidate = mustRevalidate;
this.maxStaleSeconds = maxStaleSeconds;
this.minFreshSeconds = minFreshSeconds;
this.onlyIfCached = onlyIfCached;
this.noTransform = noTransform;
this.immutable = immutable;
this.headerValue = headerValue;
}
CacheControl(Builder builder) {
this.noCache = builder.noCache;
this.noStore = builder.noStore;
this.maxAgeSeconds = builder.maxAgeSeconds;
this.sMaxAgeSeconds = -1;
this.isPrivate = false;
this.isPublic = false;
this.mustRevalidate = false;
this.maxStaleSeconds = builder.maxStaleSeconds;
this.minFreshSeconds = builder.minFreshSeconds;
this.onlyIfCached = builder.onlyIfCached;
this.noTransform = builder.noTransform;
this.immutable = builder.immutable;
}
Copy the code
Two default implementations, FORCE_NETWORK and FORCE_CACHE, are provided in the CacheControl class, with separate tables representing forcing only network responses and forcing only cached responses.
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
Copy the code
You can set CacheControl for each request at request creation time.
Request request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
Copy the code
6.2 CacheStrategy class
CacheStrategy represents a caching policy. An instance of this object is created in the Intercept method of the CacheInterceptor, and a caching strategy is determined based on networkRequest and cacheResponse in this object.
CacheStrategy CacheStrategy Strategy = new cacheStrategy.factory (now, chain-.request (), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse;Copy the code
Here’s how to create it.
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if(cacheResponse ! = null) { this.sentRequestMillis = cacheResponse.sentRequestAtMillis(); this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis(); Headers headers = cacheResponse.headers();for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) { ageSeconds = HttpHeaders.parseSeconds(value, -1); }}}}Copy the code
Creating cacheStrategy.factory simply assigns a value to the parameter passed in and takes the value of the relevant response header from the cached response passed in and assigns it to the member variable. The get method is then called to get the CacheStrategy instance.
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; }Copy the code
The candidate.networkRequest is not empty and the requested CacheControl Settings are cache only. The candidate.networkRequest is not empty and the requested CacheControl Settings are cache only. Return a cache policy with both networkRequest and cacheResponse empty, otherwise return the policy obtained by the getCandidate method. So what do networkRequest and cacheResponse mean?
/** 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;
Copy the code
NetworkRequest is null if the call does not use a network. CacheResponse is null if the call does not use a cache. So what we were saying is if the cache policy is going to use the network but the cacheControl setting for this request is cache only, return a policy where networkRequest and cacheResponse are both empty, Thus, the CacheInterceptor recognizes that the cache policy uses neither network nor cache and returns a 504 error response.
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();
}
Copy the code
Next, enter the getCandidate method.
private CacheStrategy getCandidate() {// cacheResponse itself is nullif(cacheResponse == null) {// Returns a CacheStrategy with cacheResponse nullreturnnew CacheStrategy(request, null); } // If the request is HTTPS and the required handshake is missingif(Request.ishttps () && Cacheresponse.Handshake () == null) {// Returns a CacheStrategy with cacheResponse emptyreturnnew CacheStrategy(request, null); } // If caching is not allowedif(! IsCacheable (cacheResponse, Request) {// Returns a CacheStrategy with cacheResponse emptyreturnnew CacheStrategy(request, null); } CacheControl requestCaching = request.cacheControl(); // If CacheControl is set to no cache in the requestif(requestCaching noCache () | | hasConditions (request)) {/ / return a cacheResponse CacheStrategy is emptyreturnnew CacheStrategy(request, null); } CacheControl responseCaching = cacheResponse.cacheControl(); // If the CacheControl Settings in the response are unchangedif(responseCaching. Immutable ()) {/ / return a networkRequest CacheStrategy is emptyreturnnew CacheStrategy(null, cacheResponse); Long ageMillis = cacheResponseAge(); Long freshMillis = computeFreshnessLifetime(); 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 no noCache is specified, noCache is enforced and ageMillis + minFreshMillis < freshMillis + maxStaleMillis is specifiedif(! 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 a CacheStrategy with networkRequest emptyreturn new CacheStrategy(null, builder.build());
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
String conditionName;
String conditionValue;
if(etag ! = null) { conditionName ="If-None-Match";
conditionValue = etag;
} else if(lastModified ! = null) { conditionName ="If-Modified-Since";
conditionValue = lastModifiedString;
} else if(servedDate ! = null) { conditionName ="If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
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 cache policy is created based on the incoming networkRequest and cacheResponse headers. The logic here is based on HTTP document requirements.
6.3 Cache class
A Cache is a class for caching. When you initialize OkHttpClient, you can set a Cache for it and set the Cache location and Cache size.
OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(new File(getExternalCacheDir(), "cache"), 10 * 1024 * 1024))
.build();
Copy the code
Let’s start with the Cache constructor.
final DiskLruCache cache;
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
Copy the code
See that the constructor ends up calling the DiskLruCache. Create method to create a cache, which shows that the cache class is implemented based on DiskLruCache. So let’s look at how it adds.
@Nullable CacheRequest put(Response response) { String requestMethod = response.request().method(); // Check if the cache is invalidif(HttpMethod invalidatesCache (response. The request () method ())) {try {/ / invalid then remove remove (response) the request ()); } catch (IOException ignored) { // The cache cannot be written. }returnnull; } // No get request returns NULL directly without cachingif(! requestMethod.equals("GET")) {
returnnull; } // Return null if the request header contains *if (HttpHeaders.hasVaryAll(response)) {
returnnull; } // Create an Entry and pass in response Entry Entry = new Entry(response); DiskLruCache.Editor editor = null; try { editor = cache.edit(key(response.request().url()));if (editor == null) {
returnnull; } // writeTo cache entry.writeto (editor);return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
returnnull; }}Copy the code
In the PUT method, the invalid cache is removed. In the invalidatesCache method, the request type is determined. If the request type is POST, PATCH, PUT, DELETE, or MOVE, the response is removed.
public static boolean invalidatesCache(String method) {
return method.equals("POST")
|| method.equals("PATCH")
|| method.equals("PUT")
|| method.equals("DELETE")
|| method.equals("MOVE"); // WebDAV
}
Copy the code
After that, if the request is not GET, it will not be cached, and the request header with * will not be cached. After that, the Entry entity is created, the key method is called to generate the key, and then the key is written to the cache.
So we’re done adding the cache and we’re going to look at the cache get method.
@nullable Response get(Request Request) {String key = key(request.url()); DiskLruCache.Snapshot snapshot; Entry entry; Try {// Obtain snapshot by key in cache snapshot = cache.get(key); // Returns null if the snapshot is nullif (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
returnnull; } try {// Obtain cache contents from snapshot create Entry object Entry = new Entry(snapshot.getSource(ENTRY_METADATA)); } catch (IOException e) { Util.closeQuietly(snapshot);returnnull; } // Call entry.response to get the cached response response = entry.response(snapshot); // Request and Response are matchedif(! entry.matches(request, response)) { Util.closeQuietly(response.body());returnnull; } // return cached resultsreturn response;
}
Copy the code
In the get method, the key is generated according to the requested URL, and then a snapshot snapshot is obtained from the cache according to the key, and the cache content is extracted from the snapshot to create an Entry. Then the entry.response method is called to pass the snapshot to the cached response. Verify the matching between request and Response, and return the cached result.
7. To summarize
From reading the source code flow above, you should have a basic understanding of how OkHttp works and how it works. It can be found that OkHttp is a network request framework encapsulated according to HTTP protocol specification based on Socket connection. OkHttp is much more “low-level” than Volley, which is based on HttpURLonnection and HttpClient, or Retrofit, which is based on OkHttp, and OkHttp uses Okio to read and write more efficiently and quickly.