This is the 12th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

Previous article (juejin.cn/post/703004… Having already introduced interceptors, this article is going to clarify what interceptors are about

call

@Override public Response execute() throws IOException { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } transmitter.timeoutEnter(); transmitter.callStart(); try { client.dispatcher().executed(this); / / for the interceptor relevant return getResponseWithInterceptorChain (); } finally { client.dispatcher().finished(this); }}Copy the code

Synchronous or asynchronous after will enter the getResponseWithInterceptorChain this method

Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. List<Interceptor>  interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); interceptors.add(new RetryAndFollowUpInterceptor(client)); interceptors.add(new BridgeInterceptor(client.cookieJar())); interceptors.add(new CacheInterceptor(client.internalCache())); interceptors.add(new ConnectInterceptor(client)); if (! forWebSocket) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor(forWebSocket)); Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); boolean calledNoMoreExchanges = false; try { Response response = chain.proceed(originalRequest); if (transmitter.isCanceled()) { 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

In this method, We, in turn, add the user-defined interceptor, retryAndFollowUpInterceptor, BridgeInterceptor, CacheInterceptor, ConnectInterceptor, NetworkInterceptors, CallServerInterceptor, and pass these interceptors to the RealInterceptorChain.

Enter the proceed method of the RealInterceptorChain class

public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange) throws IOException { if (index >= interceptors.size()) throw new AssertionError(); . RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange, index + 1, request, call, connectTimeout, readTimeout, writeTimeout); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); . return response; }Copy the code

The core of this method is the middle line, which executes the current interceptor’s Intercept method and calls the next (index+1) interceptor. The next call to the (index+1) interceptor depends on the call to the Proceed method of the RealInterceptorChain in the Intercept method of the current interceptor.

The Response of the current interceptor depends on the Response of the Intercept of the next interceptor. Therefore, each interceptor will be called in turn along the chain of interceptors, and when the last interceptor is executed, Response will be returned in the opposite direction, and finally we get the “ultimate version” of Response we need.

RetryAndFollowUpInterceptor interceptor

@Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); RealInterceptorChain realChain = (RealInterceptorChain) chain; Transmitter transmitter = realChain.transmitter(); int followUpCount = 0; Response priorResponse = null; while (true) { transmitter.prepareToConnect(request); if (transmitter.isCanceled()) { throw new IOException("Canceled"); } Response response; boolean success = false; try { response = realChain.proceed(request, transmitter, null); success = true; } catch (RouteException e) { if (! recover(e.getLastConnectException(), transmitter, false, request)) { throw e.getFirstConnectException(); } continue; } catch (IOException e) {Boolean requestSendStarted =! (e instanceof ConnectionShutdownException); if (! recover(e, transmitter, requestSendStarted, request)) throw e; continue; } finally {if (! success) { transmitter.exchangeDoneDueToException(); } } if (priorResponse ! Response.newbuilder ().priorResponse(priorResponse.newBuilder().body(null) .build()) .build(); } Exchange exchange = Internal.instance.exchange(response); Route route = exchange ! = null ? exchange.connection().route() : null; Request followUp = followUpRequest(response, route); // If a request follows up with code 200, then followUp is null. if (followUp == null) { if (exchange ! = null && exchange.isDuplex()) { transmitter.timeoutEarlyExit(); } return response; } RequestBody followUpBody = followUp.body(); if (followUpBody ! = null && followUpBody.isOneShot()) { return response; } closeQuietly(response.body()); if (transmitter.hasExchange()) { exchange.detachWithViolence(); } if (++followUpCount > MAX_FOLLOW_UPS) {//// throw new ProtocolException("Too many follow-up requests: " + followUpCount); } request = followUp; // Get the processed Request to continue with the Request priorResponse = response; }}Copy the code

The main function of this interceptor is retry and followup.

When a Request fails for any reason, if the route or connection is faulty, it attempts to resume. Otherwise, according to the ResponseCode, the followup method reprocesses the Request to get a new one, and then continues the new one along the interceptor chain. And of course, if the responseCode is 200, we’re done.

BridgeInterceptor interceptor

@Override public Response intercept(Chain chain) throws IOException { Request userRequest = chain.request(); Request.Builder requestBuilder = userRequest.newBuilder(); //-------------request------------- RequestBody body = userRequest.body(); if (body ! = null) { MediaType contentType = body.contentType(); if (contentType ! = null) { requestBuilder.header("Content-Type", contentType.toString()); } long contentLength = body.contentLength(); if (contentLength ! = -1) { requestBuilder.header("Content-Length", Long.toString(contentLength)); requestBuilder.removeHeader("Transfer-Encoding"); } else { requestBuilder.header("Transfer-Encoding", "chunked"); / / block transmission requestBuilder removeHeader (" the Content - Length "); } } if (userRequest.header("Host") == null) { requestBuilder.header("Host", hostHeader(userRequest.url(), false)); } if (userRequest.header("Connection") == null) { requestBuilder.header("Connection", "Keep-Alive"); } boolean transparentGzip = false; if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { transparentGzip = true; requestBuilder.header("Accept-Encoding", "gzip"); } List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url()); if (! cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)); } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", Version.userAgent()); } //------------response------------- Response networkResponse = chain.proceed(requestBuilder.build()); HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers()); Response.Builder responseBuilder = networkResponse.newBuilder() .request(userRequest); if (transparentGzip && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) { GzipSource responseBody = new GzipSource(networkResponse.body().source()); Headers strippedHeaders = networkResponse.headers().newBuilder() .removeAll("Content-Encoding") .removeAll("Content-Length") .build(); responseBuilder.headers(strippedHeaders); String contentType = networkResponse.header("Content-Type"); responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody))); } return responseBuilder.build(); }Copy the code

The main purpose of the BridgeInterceptor is to add a request header for the request before and a Response header for the Response before.

CacheInterceptor interceptor

Http caching mechanism

When the server receives the request, it sends back the last-Modified and ETag headers for the resource in 200 OK, and the client stores the resource in the cache and logs these attributes. When the client needs to send the same request, Date + cache-control is used to determine whether the Cache has expired. If the Cache has expired, if-modified-since and if-none-match headers are carried in the request. The two values are the last-Modified and ETag headers in the response. The server uses these two headers to determine that the local resources have not changed, and the client does not need to download again, and returns a 304 response.

Cache-related fields

  • Cache-control: indicates the maximum lifetime of the Cache;
  • Date: The server tells the client the sending time of the resource
  • Expires: Indicates the expiration time
  • Last-modified: The server tells the client when the resource was Last Modified
  • E-tag: indicates the unique identifier of the current resource on the server. It can be used to determine whether the resource content is modified.
@Override public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache ! = null ? cache.get(chain.request()) : null; long now = System.currentTimeMillis(); // Cache policy class, CacheStrategy Strategy = new cacheStrategy.factory (now, chain-.Request (), cacheCandidate).get(); // networkRequest. If the value is null, no networkRequest is required. Response cacheResponse = strategy.cacheresponse; // cacheResponse. // According to the cache policy, update statistics: request count, network request count, cache count if (cache! = null) { cache.trackResponse(strategy); } // If (cacheCandidate! = null && cacheResponse == null) { closeQuietly(cacheCandidate.body()); } // If no network request is available and no cache is available, 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(); } // Cache available, If (networkRequest == NULL) {return Cacheresponse.newBuilder ().Cacheresponse (stripBody) .build(); } Response networkResponse = null; NetworkResponse = chain.proceed(networkRequest); } finally { if (networkResponse == null && cacheCandidate ! = null) { closeQuietly(cacheCandidate.body()); }} //HTTP_NOT_MODIFIED cache valid, merge network request and cache if (cacheResponse! = null) { if (networkResponse.code() == HTTP_NOT_MODIFIED) { Response response = cacheResponse.newBuilder() .headers(combine(cacheResponse.headers(), networkResponse.headers())) .sentRequestAtMillis(networkResponse.sentRequestAtMillis()) .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); networkResponse.body().close(); cache.trackConditionalCacheHit(); // Update cache cache.update(cacheResponse, response); return response; } else { closeQuietly(cacheResponse.body()); } } Response response = networkResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (cache ! = null) { if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) { CacheRequest cacheRequest = cache.put(response); return cacheWritingResponse(cacheRequest, response); / / write caching} / / judge validity if (HttpMethod. InvalidatesCache (networkRequest. Method ())) {try {cache. Remove (networkRequest); } catch (IOException ignored) { } } } return response; }Copy the code
  • If the network is unavailable and no valid cache is available, a 504 error is returned;
  • If no network request is required, use caching directly;
  • Make a network request if the network is needed;
  • If there is a cache and the network request returns HTTP_NOT_MODIFIED, indicating that the cache is still valid, the network response and cached results are merged. Update cache at the same time;
  • If no cache exists, a new cache is written;

CacheStrategy is a caching policy class that tells the CacheInterceptor whether to use caching or network requests

Private CacheStrategy getCandidate() {// No cache, Direct network request if (cacheResponse == null) {return new CacheStrategy(request, null); } // HTTPS, but no handshake, If (Request.ishttps () && Cacheresponse.Handshake () == NULL) {return new CacheStrategy(request, null); } // Not cacheable, direct network request if (! isCacheable(cacheResponse, request)) { return new CacheStrategy(request, null); } CacheControl requestCaching = request.cacheControl(); If (requestCaching noCache () | | hasConditions (request)) {/ / noCache or request header contains the if - Modified Since - or if - None - Match Return new CacheStrategy(request, null); // Request headers containing if-modified-since or if-none-match indicate that the local cache has expired. } CacheControl responseCaching = cacheResponse.cacheControl(); long ageMillis = cacheResponseAge(); 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 (! 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 new CacheStrategy(null, builder.build()); } 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
  • No caching, direct network requests;
  • If HTTPS, but no handshake, direct network request;
  • Non-cacheable, direct network request;
  • Request header nocache or request header containing if-modified-since or if-none-match requires the server to verify whether the local cache is still usable.
  • If it is cacheable, and ageMillis + minFreshMillis < freshMillis + maxStaleMillis (meaning expired, but available, except that a warning is added to the response header), use caching;
  • If the cache has expired, add a request header: if-modified-since or if-none-match

ConnectInterceptor (core, connection pool)

@Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); Transmitter transmitter = realChain.transmitter(); // We need the network to fulfill this request. Possible to verify a conditional GET request (cache validation, etc.) Boolean doExtensiveHealthChecks =! request.method().equals("GET"); Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks); return realChain.proceed(request, transmitter, exchange); }Copy the code
  • Transmitter is the bridge of OkHttp network layer. These concepts we mentioned above are finally integrated through Transmitter, and provide external function realization.
  • Exchange functions like Exchange Dec, but it corresponds to a single request, with some connection management and event distribution on top of Exchange Dec.

Specifically, Exchange corresponds to Request one-to-one. When a new Request is created, an Exchange is created that sends out the Request and reads the response data, while Exchange Dec is used for sending and receiving the data.

Enter the newExchange method

Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
  synchronized (connectionPool) {
    if (noMoreExchanges) {
      throw new IllegalStateException("released");
    }
    if (exchange != null) {
      throw new IllegalStateException("cannot make a new request because the previous response "
          + "is still open: please call response.close()");
    }
  }

  ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
  Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

  synchronized (connectionPool) {
    this.exchange = result;
    this.exchangeRequestDone = false;
    this.exchangeResponseDone = false;
    return result;
  }
}
Copy the code

ExchangeCodec encodes and decodes Request Response, that is, write Request and read Response, through which both our Request and Response data are read and written.

RealConnectionPool A pool used to store realConnections using a two-ended queue. In OkHttp, a RealConnection is not immediately closed and released when it runs out, but is stored in a RealConnectionPool.

Go to find

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(); boolean connectionRetryEnabled = client.retryOnConnectionFailure(); try { RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks); return resultConnection.newCodec(client, chain); } catch (RouteException e) { trackFailure(); throw e; } catch (IOException e) { trackFailure(); throw new RouteException(e); }}Copy the code

RealConnection implements the Connection interface, which uses sockets to establish HTTP/HTTPS connections and obtain I/O streams. The same Connection may carry multiple HTTP requests and responses. In fact, it can be roughly understood as the Socket, I/O stream and some protocol encapsulation.

An Exchange object is created through the Transmitter#newExchange method and the Chain#process method is called.

The newExchange method first tries to find an existing connection in the RealConnectionPool through ExchangeFinder. If it does not find an existing connection, it creates a new RealConnection and starts the connection. It is then deposited into the RealConnectionPool, at which point the RealConnection object is ready, and a different Exchange Dec is created through the request protocol and returned.

CallServerInterceptor

The CallServerInterceptor is responsible for reading and writing data. This is the last interceptor. Everything is ready here. It sends data from the Request to the server and writes it to the Response.