preface
Since the previous project was built with MVP architecture, which is a combination of RxJava + Glide + OKHttp + Retrofit and other open source frameworks, it was just on the use level without in-depth research. Recently, we plan to attack all of them. Students who have not paid attention to them can pay attention to a wave first. After reading this series of articles, you’ll be much more comfortable handling questions (whether in an interview or on the job) if you know how they work.
Android picture loading framework Glide 4.9.0 (a) From the perspective of source code analysis Glide execution process
Android Image loading framework Glide 4.9.0 (2) Analysis of Glide cache strategy from the perspective of source code
From the perspective of source code analysis of Rxjava2 basic execution process, thread switching principle
OKHttp3 (a) synchronous and asynchronous execution process is analyzed from the perspective of source code
Analyze the charm of OKHttp3 (ii) interceptor from the source point of view
OKHttp3 (iii) cache strategy is analyzed from a source code perspective
Analyze Retrofit network request from source code point of view, including RxJava + Retrofit + OKhttp network request execution process
Interceptor interceptor
In the last analysis from the perspective of source OKHttp3 (a) synchronous and asynchronous execution process, finally we know in getResponseWithInterceptorChain () function to finish the last request and response, so how are internal to complete the request, And calls back the server’s response data to the calling layer.
Response getResponseWithInterceptorChain(a) throws IOException {
// Build a container stack of interceptor calls
List<Interceptor> interceptors = new ArrayList<>();
// Global interceptor added in addInterceptor when configuring OKHttpClient
interceptors.addAll(client.interceptors());
// Error, redirect interceptor
interceptors.add(new RetryAndFollowUpInterceptor(client));
// Bridge interceptor, bridge application layer and network layer, add the necessary headers
interceptors.add(new BridgeInterceptor(client.cookieJar()));
// Cache processing, last-Modified, ETag, DiskLruCache, etc
interceptors.add(new CacheInterceptor(client.internalCache()));
// Connect interceptor
interceptors.add(new ConnectInterceptor(client));
// Whether it is a webSocket
if(! forWebSocket) {/ / by okHttpClient Builder# addNetworkInterceptor ()
// The incoming interceptor only applies to non-web requests
interceptors.addAll(client.networkInterceptors());
}
// The interceptor that actually accesses the server
interceptors.add(new CallServerInterceptor(forWebSocket));
// The caller that actually executes the interceptor
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null.0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
// Start executing
Response response = chain.proceed(originalRequest);
// Whether to cancel
if (transmitter.isCanceled()) {
/ / close
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if(! calledNoMoreExchanges) { transmitter.noMoreExchanges(null); }}}Copy the code
There’s not much code in the function, but it’s really the essence. We know that from the above code and comments
- Start by creating a container to hold the interceptor
- Add global interceptor and apply interceptor
- create
RealInterceptorChain
Object interceptor and pass in some configuration of the interceptor container, emitter, request data, and so on - The last call
RealInterceptorChain
的chain.proceed(originalRequest);
Function is what really makes these interceptors work.
In the last article, I briefly introduced interceptors and mentioned the chain of responsibility mode. However, this interceptor uses the RealInterceptorChain object to open the chain of responsibility task delivery. It feels like a CEO delivering tasks and passing them layer by layer. Similar to touch feedback event passing in Android source code, the heart of OKHttp is the interceptor. Let’s start with a step-by-step analysis of the subtlety of the OKHttp interceptor.
RealInterceptorChain
Proceed (originalRequest) : The interceptor task is performed on the Chain.proceed (originalRequest) of the RealInterceptorChain
public final class RealInterceptorChain implements Interceptor.Chain {...// Omit member variable attributes
public RealInterceptorChain(List < Interceptor > interceptors, / / all Interceptor Transmitter Transmitter, / / transmitters @ Nullable Exchange, Exchange, / / encapsulation of OKIO request data operationint index, Request request, Call call,
int connectTimeout, int readTimeout,
int writeTimeout
){...// Omit the assignment code
}
/ / external getResponseWithInterceptorChain function calls
public Response proceed( Request request, Transmitter transmitter, @Nullable Exchange exchange )throws IOException {
// Index cannot exceed the interceptor container size
if (index >= interceptors.size()) throw new AssertionError();
// Throw an exception if a request connection already exists
if (this.exchange ! =null&&!this.exchange.connection().supportsUrl(request.url())) {
...// Throw exception code omitted
}
// Make sure the open call is unique, otherwise throw an exception. I think this will only make the code more robust.
if (this.exchange ! =null && calls > 1) {...// Throw exception code omitted
}
//1. Create an object for the next interceptor to execute
RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
//2. Fetch the current interceptor
Interceptor interceptor = interceptors.get(index);
Call the intercept(Chain) method of the next interceptor, pass in the newly created RealInterceptorChain, and return Response
Response response = interceptor.intercept(next);
// Limit some judgments to make sure the program is robust
if(exchange ! =null && index + 1< interceptors.size() && next.calls ! =1) {...// Throw exception code omitted
}
// If the response returned is empty, an exception is thrown
if (response == null) {...// Throw exception code omitted
}
// If the response is empty, an exception is also thrown
if (response.body() == null) {...// Throw exception code omitted
}
// Actually returns the server's response
returnresponse; }}Copy the code
Take a look at comments 1, 2, and 3 above, which are the core code for distributing interceptor execution. Create a RealInterceptorChain inside the RealInterceptorChain and pass in the index + 1 parameter. Get (index + 1) interceptor Note 2 is to fetch the current interceptor, and note 3 is to execute the interceptor.
The RealInterceptorChain class is a recursive function interceptor.Intercept (next); There is an entry point for recursion. There is an exit point, but it is not in this class. It is in the CallServerInterceptor request and response interceptor. Equivalent to exports. So the RealInterceptorChain class is personally responsible for starting/stopping interceptors, kind of like interceptor calls delegate to RealInterceptorChain.
There must be a list. The get (index = 0) RetryAndFollowUpInterceptor interceptor first performed, the following start analysis error and redirect the interceptor.
RetryAndFollowUpInterceptor
As mentioned in the introduction of interceptor, it is an interceptor for error reconnection and redirection. Let’s take a look at its core code
public final class RetryAndFollowUpInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
// Get the current request
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
// Get the Transmitter object
Transmitter transmitter = realChain.transmitter();
int followUpCount = 0;
Response priorResponse = null;
while (true) {
// Prepare the connection
transmitter.prepareToConnect(request);
// Determine whether to cancel
if (transmitter.isCanceled()) {
throw new IOException("Canceled");
}
Response response;
boolean success = false;
try {
// Pass the current request to the next interceptor
response = realChain.proceed(request, transmitter, null);
success = true;
} catch (RouteException e) {
// Check whether it can continue to use
if(! recover(e.getLastConnectException(), transmitter,false, request)) {
throw e.getFirstConnectException();
}
continue;
} catch (IOException e) {
booleanrequestSendStarted = ! (einstanceof ConnectionShutdownException);
// Check whether it can continue to use
if(! recover(e, transmitter, requestSendStarted, request))throw e;
continue;
} finally {
// If the connection is not released successfully
if (!success) {
transmitter.exchangeDoneDueToException();
}
}
// There is no exception
if(priorResponse ! =null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null) .build()) .build(); }...// omit the code
// Process the request header based on the response
Request followUp = followUpRequest(response, route);
// If it is empty, no redirection is required and the response is returned directly
if (followUp == null) {
if(exchange ! =null && exchange.isDuplex()) {
transmitter.timeoutEarlyExit();
}
return response;
}
// Not empty, need redirection
RequestBody followUpBody = followUp.body();
if(followUpBody ! =null && followUpBody.isOneShot()) {
return response;
}
closeQuietly(response.body());
if (transmitter.hasExchange()) {
exchange.detachWithViolence();
}
// The number of redirects cannot be greater than 20
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
// Try again based on the redirected requestrequest = followUp; priorResponse = response; }}}Copy the code
According to the above code analysis, the following points are mainly done
- Get the current request object, and get the Transmitter object
- Ready to connect, in fact, the real connection is in
ConnectInterceptor
The interceptor - Invoke the next interceptor, i.e
BridgeInterceptor
Pass the request to it in preprocessing. - Check whether exceptions occur during the connection and determine whether to continue the connection
- If not, release the resource
- Determine whether a reconnection operation is required based on the response code
- If the number of reconnections is greater than 20, an exception is thrown; otherwise, the redirected request is retried.
In the current RetryAndFollowUpInterceptor realChain. Proceed (request, transmitter, null); The call goes to the BridgeInterceptor, the interceptor that the application interacts with the network.
BridgeInterceptor
When the last interceptor calls proceed, it will go to the current Intercept function
public final class BridgeInterceptor implements Interceptor {
private finalCookieJar cookieJar; .// omit the constructor
@Override public Response intercept(Chain chain) throws IOException {
// Get the current Request Request
Request userRequest = chain.request();
// Get the Request configuration parameter Builder
Request.Builder requestBuilder = userRequest.newBuilder();
// Get the request body
RequestBody body = userRequest.body();
// Determine whether the request body is empty
if(body ! =null) {// Not empty
// Get the request body type
MediaType contentType = body.contentType();
if(contentType ! =null) {
// Add header to the request body type
requestBuilder.header("Content-Type", contentType.toString());
}
// Process the request body length
long contentLength = body.contentLength();
if(contentLength ! = -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding"."chunked");
requestBuilder.removeHeader("Content-Length"); }}// Add header HOST HOST
if (userRequest.header("Host") = =null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
// Add the connection state
if (userRequest.header("Connection") = =null) {
requestBuilder.header("Connection"."Keep-Alive");
}
// Whether to enable data compression -- Gzip is added by default
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") = =null && userRequest.header("Range") = =null) {
transparentGzip = true;
// Add gzip compression
requestBuilder.header("Accept-Encoding"."gzip");
}
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if(! cookies.isEmpty()) {// Add cookies to header
requestBuilder.header("Cookie", cookieHeader(cookies));
}
/ / add the user-agent
if (userRequest.header("User-Agent") = =null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
CacheInterceptor executes the next interceptor
Response networkResponse = chain.proceed(requestBuilder.build());
// Save the url and cookie
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
// Get the response and add some attributes
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
// Return the response
returnresponseBuilder.build(); }...// Omit some code
}
Copy the code
From the above code, we know that the BridgeInterceptor mainly does some pre-processing of the request header before calling the next interceptor.
CacheInterceptor
The BridgeInterceptor call ends up in the current Intercept. The BridgeInterceptor call is used to fetch and update the cache. Now let’s look at the implementation
public final class CacheInterceptor implements Interceptor {
final @NullableInternalCache cache; .// Constructor omitted
@Override public Response intercept(Chain chain) throws IOException {
// If the cache is not empty, get the cache response according to the requestResponse cacheCandidate = cache ! =null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// Get the cache policy
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
// Get the request according to the cache policy
Request networkRequest = strategy.networkRequest;
// Get the cached responseResponse cacheResponse = strategy.cacheResponse; .// Omit some code
// If the request and cache response are empty, the cache is forced and error code 504 is returned
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1) //
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// If networkRequest is empty, the cache is also forcibly fetched
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
// Invoke the next interceptor
networkResponse = chain.proceed(networkRequest);
} finally{... }// If the cache is not empty
if(cacheResponse ! =null) {
// And the response code == previously defined 304
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
// Generate a response
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 response
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else{ closeQuietly(cacheResponse.body()); }}// No cache usage, read network response
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if(cache ! =null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Cache
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
// Check whether the cache is valid
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
// Delete invalid cache
cache.remove(networkRequest);
} catch (IOException ignored) {
}
}
}
return response;
}
Copy the code
The OKHttp cache mechanism will be covered in a separate article since it only covers interceptor calls and some basic processing logic. Just know that if the external OKHttpClient is configured with a cache (see the following code block, otherwise the cache will be empty). Caches are put, GET, and update. Since there is no caching policy, we call the next interceptor, ConnectInterceptor
File file = new File(Environment.getExternalStorageDirectory() + "/T01");
Cache cache = new Cache(file, 1024 * 1024 * 10);
OkHttpClient okHttpClient = new OkHttpClient.Builder().
addInterceptor(new LoggingInterceptor())
.cache(cache).
build();
Copy the code
ConnectInterceptor
(PS: Interceptor interceptor mainly refers to:Juejin. Cn/post / 684490…
After the cache interceptor completes execution, the next call chain is to connect the interceptor. Look at the code implementation:
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;
public ConnectInterceptor(OkHttpClient client) {
this.client = client;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
// Get the request
Request request = realChain.request();
/ / get the Transmitter
Transmitter transmitter = realChain.transmitter();
booleandoExtensiveHealthChecks = ! request.method().equals("GET");
// Create a new Exchange
Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
// Call the proceed method, which calls the intercept method of the next interceptor, CallServerInterceptor
returnrealChain.proceed(request, transmitter, exchange); }}Copy the code
The ConnectInterceptor has a simple internal code. First, it gets the Request and the Transmitter object. Exchange is responsible for writing data into the IO stream that creates the connection. Finally, call the CallServerInterceptor interceptor. NewExchange (Chain, doExtensiveHealthChecks) internal code implementation
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
synchronized (connectionPool) {
// If no Exchanges throw an exception
if (noMoreExchanges) {
throw new IllegalStateException("released");
}
if(exchange ! =null) {...// omit exception code
}
// find an exchange dec using ExchangeFinder's find method
ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
// Create an Exchange and pass in the Exchange Ecodec instance codec, so the Exchange ecodec instance is held inside the Exchange
Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);
synchronized (connectionPool) {
this.exchange = result;
this.exchangeRequestDone = false;
this.exchangeResponseDone = false;
returnresult; }}Copy the code
ExchangeFinder object via the Transmitter in RetryAndFollowUpInterceptor early prepareToConnect method to create, it find methods is where the connection to create real, What is ExchangeFinder? ExchangeFinder is responsible for the creation of a connection, putting it into a pool, and taking it out of the pool if it already exists, so ExchangeFinder manages two important roles: RealConnection, RealConnectionPool, RealConnectionPool and RealConnection.
RealConnection
The real realization of the Connection, the realization of the Connection interface, internal use of Socket to establish a Connection, as follows:
public interface Connection {
// Return the Route used for this connection
Route route(a);
// Return the Socket used for this connection
Socket socket(a);
// If HTTPS is used, TLS handshake information is returned for establishing the connection, otherwise null is returned
@Nullable Handshake handshake(a);
Protocol is an enumeration, such as HTTP1.1 and HTTP2
Protocol protocol(a);
}
public final class RealConnection extends Http2Connection.Listener implements Connection {
public final RealConnectionPool connectionPool;
/ / routing
private final Route route;
// Use this rawSocket internally to establish connections at the TCP layer
private Socket rawSocket;
// If HTTPS is not used, then socket == rawSocket, otherwise the socket == SSLSocket
private Socket socket;
/ / TLS handshake
private Handshake handshake;
// Application layer protocol
private Protocol protocol;
/ / HTTP2 connections
private Http2Connection http2Connection;
// The okio library's BufferedSource and BufferedSink are javaIO's input and output streams
private BufferedSource source;
private BufferedSink sink;
public RealConnection(RealConnectionPool connectionPool, Route route) {
this.connectionPool = connectionPool;
this.route = route;
}
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
/ /...
}
/ /...
}
Copy the code
RealConnection has a connect method that can be called externally to establish a connection. The connect method looks like this:
//RealConnection.java
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
if(protocol ! =null) throw new IllegalStateException("already connected");
RouteException routeException = null;
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
// Route selection
if (route.address().sslSocketFactory() == null) {
if(! connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if(! Platform.get().isCleartextTrafficPermitted(host)) {throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy")); }}else {
if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
throw new RouteException(new UnknownServiceException(
"H2_PRIOR_KNOWLEDGE cannot be used with HTTPS")); }}// Start the connection
while (true) {
try {
if (route.requiresTunnel()) {// If it is channel mode, establish channel connection
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break; }}else {//1. Otherwise, the Socket connection is performed
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
// Establish an HTTPS connection
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
break;
}
/ /... Omit exception handling
if(http2Connection ! =null) {
synchronized(connectionPool) { allocationLimit = http2Connection.maxConcurrentStreams(); }}}Copy the code
The connectSocket method is called to establish a Socket connection:
//RealConnection.java
private void connectSocket(int connectTimeout, int readTimeout, Call call,
EventListener eventListener) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
// Create sockets based on the proxy type
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
eventListener.connectStart(call, route.socketAddress(), proxy);
rawSocket.setSoTimeout(readTimeout);
try {
//1. Establish a Socket connection
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
}
/ /... Omit exception handling
try {
// Get Socket input/output streams
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
}
/ /... Omit exception handling
}
Copy the code
Note 1: Platform is a compatible class implemented in okhttp based on different versions of Android platforms. We won’t go into details here. Platform’s connectSocket method eventually calls rawSocket’s Connect () method to establish its Socket connection. Okhttp can read data from source or write data to sink. Source and sink are BufferedSource and BufferedSink types. They come from okio, a library that encapsulates both java.io and java.nio. The okHTTP base relies on this library to read and write data. To learn more about Okio, see this article dismantling the Wheel: Dismantling Okio.
RealConnectionPool
Connection pool, used to manage connection objects RealConnection, as follows:
public final class RealConnectionPool {
/ / thread pool
private static final Executor executor = new ThreadPoolExecutor(
0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */.60L /* keepAliveTime */,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
Util.threadFactory("OkHttp ConnectionPool".true));
boolean cleanupRunning;
// Clean up connection tasks, executed in executor
private final Runnable cleanupRunnable = () -> {
while (true) {
// Call the cleanup method to perform the cleanup logic
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (RealConnectionPool.this) {
try {
// Call the wait method to wait
RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
};
// Double end queue, save connection
private final Deque<RealConnection> connections = new ArrayDeque<>();
void put(RealConnection connection) {
if(! cleanupRunning) { cleanupRunning =true;
// Use thread pools to perform cleanup tasks
executor.execute(cleanupRunnable);
}
// Insert the new connection into the queue
connections.add(connection);
}
long cleanup(long now) {
/ /...
}
/ /...
}
Copy the code
RealConnectionPool internally maintains a thread pool to perform the cleanupRunnable connection task, as well as a double-ended queue connections to cache connections that have been created. To create a connection, you need to go through a TCP handshake, or a TLS handshake if you’re using HTTPS. Both handshake processes are time-consuming, so connections is needed to cache the connection for reuse. Okhttp supports 5 concurrent connections. By default, each connection has a keepAlive duration of 5 minutes. The amount of time that you stay alive.
When we first call RealConnectionPool’s PUT method to cache a new connection, if cleanupRunnable hasn’t been executed yet, it will first execute cleanupRunnable using the thread pool and then put the new connection into a two-ended queue. The cleanup method is called in cleanupRunnable, which returns the interval between now and the next cleanup, then calls the WIAT method to wait, then calls the cleanup method again, and so on. Let’s look at the cleanup logic for the cleanup method:
//RealConnectionPool.java
long cleanup(long now) {
int inUseConnectionCount = 0;// The number of connections being used
int idleConnectionCount = 0;// Number of idle connections
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
synchronized (this) {
// Iterate over all connections, recording the number of idle connections and the number of connections in use
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
/ / if the connection is still in use, pruneAndGetAllocationCount through reference counting ways to judge whether a connection idle
if (pruneAndGetAllocationCount(connection, now) > 0) {
// Add 1 to the number of connections
inUseConnectionCount++;
continue;
}
// The connection is not in use
// The number of free connections is increased by 1
idleConnectionCount++;
// Record the connection with the longest keepalive duration
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
// This connection will probably be removed because the idle time is too longlongestIdleConnection = connection; }}// Out of the loop
// The default keepalive duration keepAliveDurationNs is 5 minutes. The maximum number of idle connections idleConnectionCount is 5
if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) {// If the keepalive time of longestIdleConnection is greater than 5 minutes or the number of idle connections is greater than 5
// Clear the longestIdleConnection connection from the queue
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {// If the number of idle connections is less than 5 and the longestIdleConnection connection has not expired, clean up
// Return the expiration time of the connection and clean it up next time
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {// If there are no free connections and all connections are still in use
// Go back to keepAliveDurationNs and clean up after 5 minutes
return keepAliveDurationNs;
} else {
// Reset cleanupRunning without any connection
cleanupRunning = false;
return -1; }}// After clearing the longestIdleConnection connection from the queue, close the socket for the connection, return 0, immediately clean up again
closeQuietly(longestIdleConnection.socket());
return 0;
}
Copy the code
From the cleanup method, okHTTP cleans up connections using the following logic:
1. Firstly, all connections are traversed, and idleConnectionCount and inUseConnectionCount are recorded. When recording the number of idle connections, longestIdleConnection with the longest idle time is also found. The connection is likely to be cleared;
2. After traversal, decide whether to clean longestIdleConnection according to the maximum idle time and the maximum number of idle connections.
2.1. If the idle time of longestIdleConnection is greater than the maximum idle time or the number of idle connections is greater than the maximum number of idle connections, the connection will be removed from the queue, and then close the socket of the connection, return 0, and immediately clean up again.
2.2. If the number of idle connections is less than 5 and the idle time of longestIdleConnection is less than the maximum idle time, that is, it has not expired for cleaning, then return the expiration time of the connection and clean it next time;
2.3. If there are no idle connections and all connections are still in use, the default keepAlive time is returned and cleaned up after 5 minutes.
2.4, There is no connection, idleConnectionCount and inUseConnectionCount are both 0, reset cleanupRunning and wait for the next PUT connection to execute cleanupRunnable again using the thread pool.
With RealConnectionPool and RealConnection in mind, let’s go back to the find method of ExchangeFinder, where the connection is created.
Connection mechanism
The fing method for ExchangeFinder is as follows:
//ExchangeFinder.java
public ExchangeCodec find(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
try {
//1. Call findHealthyConnection internally to return the RealConnection object
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
// create a new connection
return resultConnection.newCodec(client, chain);
} catch (RouteException e) {
...// omit exception handling}}Copy the code
As we know from note 1 to create a RealConnection, let’s look at the findHealthyConnection function
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
// Find a connection
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled);
synchronized (connectionPool) {
if (candidate.successCount == 0) {
returncandidate; }}// Determine the availability of the connection
if(! candidate.isHealthy(doExtensiveHealthChecks)) { candidate.noNewExchanges();continue;
}
returncandidate; }}Copy the code
Then see findConnection
//ExchangeFinder.java
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;// Return the result of the available connection
Route selectedRoute = null;
RealConnection releasedConnection;
Socket toClose;
synchronized (connectionPool) {
if (transmitter.isCanceled()) throw new IOException("Canceled");
hasStreamFailure = false; .
//1. Try to use a connection that has already been created. The connection that has already been created may be restricted to create new streams
releasedConnection = transmitter.connection;
/ / 1.1, if the connection has been created has restricted to create a new flow, the release of the connection (releaseConnectionNoEvents will connect the empty), and returns the connection Socket to shut downtoClose = transmitter.connection ! =null && transmitter.connection.noNewExchanges
? transmitter.releaseConnectionNoEvents()
: null;
// if the connection is still available, use it as a result.
if(transmitter.connection ! =null) {
result = transmitter.connection;
releasedConnection = null;
}
// The connection that has been created cannot be used
if (result == null) {
2.1. Try to find the connection available in the connection pool. If the connection is found, the value will be stored in the Transmitter
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null.false)) {
2.2. Find an available connection from the connection pool
foundPooledConnection = true;
result = transmitter.connection;
} else if(nextRouteToTry ! =null) {
selectedRoute = nextRouteToTry;
nextRouteToTry = null;
} else if (retryCurrentRoute()) {
selectedRoute = transmitter.connection.route();
}
}
}
closeQuietly(toClose);
/ /...
if(result ! =null) {
// if a connection is already available, return the result
return result;
}
// No connection available
// See if routing is required, multi-IP operation
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null| |! routeSelection.hasNext())) { newRouteSelection =true;
routeSelection = routeSelector.next();
}
List<Route> routes = null;
synchronized (connectionPool) {
if (transmitter.isCanceled()) throw new IOException("Canceled");
// If there is a next route
if (newRouteSelection) {
routes = routeSelection.getAll();
// This is the second attempt to find available connections from the connection pool
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
4.1. Find an available connection from the connection pool
foundPooledConnection = true; result = transmitter.connection; }}// No available connection was found in the connection pool
if(! foundPooledConnection) {if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
// create a Socket connection
result = newRealConnection(connectionPool, selectedRoute); connectingConnection = result; }}If a connection is available in the connection pool, return the connection directly
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
Call RealConnection's connect method to connect the Socket. This is described in RealConnection
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener);
connectionPool.routeDatabase.connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
connectingConnection = null;
// If we just created a multiplexed connection with the same address, release this connection and get that one
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
result.noNewExchanges = true;
socket = result.socket();
result = transmitter.connection;
} else {
5.2. Add the newly created connection to the connection pool
connectionPool.put(result);
Save the newly created connection to the Transmitter connection field
transmitter.acquireConnectionNoEvents(result);
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
//5.4
return result;
}
Copy the code
This findConnection method is the core of the ConnectInterceptor. We ignore multiple IP operations and multiplexing (HTTP2). Assume that this connection is not in the connection pool or Transmitter for the first time, so skip 1, 2, 3 and go directly to 5. Create a new connection and put it in the connection pool and Transmitter; Then we use the same Call to make the second request. At this time, there is the connection in the connection pool and the Transmitter, so we will go 1, 2 and 3. If the connection in the Transmitter is still available, we will return, otherwise we will get an available connection from the connection pool, so the general process of the whole connection mechanism is as follows:
What is the difference between a connection in Transmitter and a connection in a connection pool? As we know, every time a Call is created, a corresponding Transmitter will be created. A Call can send multiple calls (synchronous and asynchronous), and different calls have different transmitters. The connection pool is created when OkhttpClient is created. Therefore, the connection pool is shared by all calls, that is, all calls in the connection pool can be reused, while the connection in the Transmitter only corresponds to its corresponding Call, which can only be reused by all calls of this Call.
Now that we know about okHttp3’s connection mechanism, we move on to the next interceptor, networkInterceptors.
networkInterceptors
NetworkInterceptors is the sixth interceptor in the OKHttp interceptor. It belongs to the network interceptor.
Finally, OKHttp’s last interceptor, CallServerInterceptor, is executed
CallServerInterceptor
It is the last interceptor in the chain, according to the source code. It performs network request and response operations with the server.
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
// Get the Exchange to interact with the network
Exchange exchange = realChain.exchange();
// Get the requested data
Request request = realChain.request();
// Get the current request time
long sentRequestMillis = System.currentTimeMillis();
// Write the request header
exchange.writeRequestHeaders(request);
boolean responseHeadersStarted = false;
Response.Builder responseBuilder = null;
// If the request body can be written
if(HttpMethod.permitsRequestBody(request.method()) && request.body() ! =null) {
// If the request header adds 100-continue
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
exchange.flushRequest(); // Close the IO stream resource
responseHeadersStarted = true;
exchange.responseHeadersStart();
responseBuilder = exchange.readResponseHeaders(true);
}
if (responseBuilder == null) { // If it is null
if (request.body().isDuplex()) {
exchange.flushRequest();
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, true));
request.body().writeTo(bufferedRequestBody);
} else { //一般走 else
// Write to the request body
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, false)); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); }}else {
exchange.noRequestBody();
if(! exchange.connection().isMultiplexed()) {// exchange.noNewExchangesOnConnection(); }}}else { // noRequestBody is executed if there is noRequestBody
exchange.noRequestBody();
}
// If the request body is empty and isDuplex = false is not supported I/O streams
if (request.body() == null| |! request.body().isDuplex()) { exchange.finishRequest(); }if(! responseHeadersStarted) { exchange.responseHeadersStart(); }// Read the head of the response
if (responseBuilder == null) {
responseBuilder = exchange.readResponseHeaders(false);
}
// Build response data
Response response = responseBuilder
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
// Get the response code
int code = response.code();
if (code == 100) {
// Build the response
response = exchange.readResponseHeaders(false)
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
exchange.responseHeadersEnd(response);
if (forWebSocket && code == 101) {
// Build an empty response body
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
Construct the response body with the body of the responseresponse = response.newBuilder() .body(exchange.openResponseBody(response)) .build(); }...// Omit some code
return response;
}
Copy the code
In the current interceptor, we write the request head /body to the server through OKIO, and then build some response data such as the response header and the response body based on the response data of the server.
Now that we’re done with the interceptor, let’s move on to the actual interceptor.
Interceptor combat
OKHttp interceptor usage guide
Custom Log print interceptor
/** * Prints the log interceptor */
class LoggingInterceptor implements Interceptor {
private String TAG = "LoggingInterceptor";
public static String requestBodyToString(RequestBody requestBody) throws IOException {
if (requestBody == null)return "";
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
return buffer.readUtf8();
}
@Override
public Response intercept(Chain chain) throws IOException {
// Get the requested data
Request request = chain.request();
// Request headers can be added before requesting the server
request = request.newBuilder()
.addHeader("head-1"."1")
.addHeader("head-2"."2")
.url("https://juejin.cn/user/3368559355637566")
.build();
HttpUrl url = request.url();
String scheme = url.scheme();// http https
String host = url.host();/ / 127.0.0.1
String path = url.encodedPath();// /test/upload/img
String query = url.encodedQuery();// userName=DevYk&userPassword=12345
RequestBody requestBody = request.body();
String bodyToString = requestBodyToString(requestBody);
Log.d(TAG,"Scheme -" "+scheme);
Log.d(TAG,"Host--->"+host);
Log.d(TAG,"path--->"+path);
Log.d(TAG,"query--->"+query);
Log.d(TAG,"requestBody---->"+bodyToString+"");
Log.d(TAG,"head---->"+request.headers().names());
// Invoke the next interceptor
Response response = chain.proceed(request);
// Get the response
ResponseBody responseBody = response.body();
String body = responseBody.string();
String type = responseBody.contentType().type();
String subtype = responseBody.contentType().subtype();
// Prints the response
Log.d(TAG,"contentType--->"+type+""+subtype);
Log.d(TAG,"responseBody--->"+body);
returnchain.proceed(request); }}Copy the code
Add the configuration
OkHttpClient okHttpClient = new OkHttpClient.Builder().
addInterceptor(new LoggingInterceptor())
build();
Copy the code
output:
LoggingInterceptor: scheme-- HTTPS LoggingInterceptor: Host-- >juejin. Im LoggingInterceptor: path-- >/user/578259398ac2470061f3a3fb
LoggingInterceptor: query--->null
LoggingInterceptor: requestBody---->
LoggingInterceptor: head---->[head-1, head-2] LoggingInterceptor: responseHeader--->text html LoggingInterceptor: responseBody---><! DOCTYPE html><html ....Copy the code
Custom global disallow network request interceptor
public class NetworkInterceptor implements Interceptor {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
if (true) {
Response response = new Response.Builder()
.code(404) // Code can be given freely
.protocol(Protocol.HTTP_1_1)
.message("Due to policy, Internet requests cannot be made at this time.")
.body(ResponseBody.create(MediaType.get("text/html; charset=utf-8"), "")) // Return to empty page
.request(chain.request())
.build();
return response;
} else {
returnchain.proceed(chain.request()); }}}Copy the code
configuration
OkHttpClient okHttpClient = new OkHttpClient.Builder().
addInterceptor(new LoggingInterceptor()).
addInterceptor(new NetworkInterceptor()).
build();
Copy the code
Output:
LoggingInterceptor: responseCode--->404LoggingInterceptor: responseMessage-- > According to the regulations, network requests cannot be made at the moment. LoggingInterceptor: responseisSuccessful--->false
Copy the code
Summary: Interceptors are divided into application interceptors and network interceptors.
Apply interceptor
- Don’t worry about intermediate responses, such as redirects and retries.
- Even if an HTTP response is provided from the cache, it is always called once.
- Adhere to the original intent of the application. Don’t care about OkHttp injected headers, for example
If-None-Match
. - Allow short circuit instead of
Chain.proceed()
. - Allow retries and multiple calls
Chain.proceed()
.
Network interceptor
- The ability to operate on intermediate responses such as redirects and retries.
- Will not be called for cache responses that short-circuit the network.
- Observe the data as if it were transmitted over a network.
- access
Connection
With a request.
So how to choose depends on their own needs.
Interceptor summary
OKHttp interceptor: OKHttp interceptor: OKHttp interceptor: OKHttp interceptor: OKHttp interceptor
Each interceptor corresponds to a RealInterceptorChain, and each interceptor generates the next RealInterceptorChain until the List iteration is complete. So this is basically recursion, and I found some pictures to help you understand the picture below
reference
OKHttp source code parsing (4)- Mid-level interceptor and call chain
OKHttp source code analysis