Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

This article also participated in the “Digitalstar Project” to win a creative gift package and creative incentive money

preface

In the previous article, we focused on the OkHttp connection pool reuse mechanism, high concurrency distribution, and interceptor design, but we did not explain the role of each interceptor in the framework, so in this article, we will focus on the execution flow of each interceptor and the corresponding relationship.

In the next post, I will write a castrated version of OkHttp to reinforce my understanding of OkHttp. No more words, just get started.

  Response getResponseWithInterceptorChain(a) throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    // Developer custom interceptor
    interceptors.addAll(client.interceptors());
    / / RetryAndFollowUpInterceptor (redirect interceptor)
    interceptors.add(retryAndFollowUpInterceptor);
    // BridgeInterceptor
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //CacheInterceptor (CacheInterceptor)
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // ConnectInterceptor
    interceptors.add(new ConnectInterceptor(client));
    if(! forWebSocket) {// Developer custom interceptor
      interceptors.addAll(client.networkInterceptors());
    }
    //CallServerInterceptor(read/write interceptor)
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null.null.null.0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
Copy the code

The source code interpretation

In the last article, we looked at the source code directly and ended there, so we’ll start there now. According to this code, interceptors can be divided into (excluding developer customization) :

  1. RetryAndFollowUpInterceptor (redirect/retry interceptor)
  2. BridgeInterceptor
  3. CacheInterceptor (CacheInterceptor)
  4. ConnectInterceptor
  5. CallServerInterceptor(read/write interceptor)

1, RetryAndFollowUpInterceptor (redirect/retry interceptor)

The first interceptor: RetryAndFollowUpInterceptor, main is to complete two things: retry and redirection.

1.1 try again,


public final class RetryAndFollowUpInterceptor implements Interceptor {... slightly@Override public Response intercept(Chain chain) throws IOException {... slightly// StreamAllocation highlights, which will be explained later in connection interceptors
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response;
      boolean releaseConnection = true;
      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.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        booleanrequestSendStarted = ! (einstanceof ConnectionShutdownException);
        if(! recover(e, streamAllocation, requestSendStarted, request))throw e;
        releaseConnection = false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null); streamAllocation.release(); }}... Slightly}.. Slightly}Copy the code

The source code parsing

If a RouteException or IOException occurs during the request phase, the request will be re-initiated.

	  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.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        booleanrequestSendStarted = ! (einstanceof ConnectionShutdownException);
        if(! recover(e, streamAllocation, requestSendStarted, request))throw e;
        releaseConnection = false;
        continue;
      }
Copy the code

The source code parsing

Both exceptions call the same method recover() to determine whether to retry. So get inside and find out.

  private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);

    // The application layer has forbidden retries. Retry is prohibited at the application layer.
    // If a request fails, it will not be retried
    if(! client.retryOnConnectionFailure())return false;

    // We can't send the request body again. We cannot send the request body again.
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    // This exception is fatal.
    // Check whether it is a retry exception
    if(! isRecoverable(e, requestSendStarted))return false;

    // No more routes to attempt.
    if(! streamAllocation.hasMoreRoutes())return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }
Copy the code

The source code parsing

If an exception occurs and more routes exist, the system tries to retry the request on another route without prohibiting the retry. Some of these exceptions are determined in isRecoverable.

  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // If there was a protocol problem, don't recover.
    // A protocol exception occurred and cannot be retried
    if (e instanceof ProtocolException) {
      return false;
    }

    // If there was an interruption don't recover, but if there was a timeout connecting to a route
    // Do not recover if there is an interruption, but if the connection to the route times out
    // we should try the next route (if there is one).
    // We should try the next route (if any).
    if (e instanceof InterruptedIOException) {
      return e instanceofSocketTimeoutException && ! requestSendStarted; }// Look for known client-side or negotiation errors that are unlikely to be fixed by trying
    // again with a different route.
    // The SSL handshake is abnormal. The certificate is faulty and cannot be retried
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e.getCause() instanceof CertificateException) {
        return false; }}// The SSL handshake is not authorized
    if (e instanceof SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      return false;
    }

    return true;
  }
Copy the code

The source code parsing

As you can see from this code:

  • The protocol is abnormal and cannot be retried
  • If the socket times out and there is no new connection channel, retry cannot be performed
  • The SSL handshake is abnormal
  • Cannot retry without SSL authorization
  • The request can be retried in any other case

1.2. Redirection

So before we talk about redirection, let’s first understand what redirection is, right?

As is shown in

Existing a network interface, when the browser to access, called the error code 302 for the first time, but in the address to return to the head, has a Location field, this field value is our normal access to the interface, so a browser automatically redirect the inside of the access to the corresponding field values, and feedback the latest interface data.

As you can see, the browser automatically redirects us when accessing the interface. So let’s try this address on Android in the most primitive way and see what happens without redirection.

 private void testHttp(a){
        String path = "http://jz.yxg12.cn/old.php";
        try {
            HttpUrl httpUrl = new HttpUrl(path);
            // Concatenate the request header
            StringBuffer request = new StringBuffer();
            request.append("GET ");
            request.append(httpUrl.getFile());
            request.append("HTTP / 1.1 \ r \ n");
            request.append("Host: "+httpUrl.getHost());
            request.append("\r\n");
            request.append("\r\n");
            // If there is a request body, the request body needs to be concatenated
            / / encapsulates the socket
            Socket socket = new Socket();
            socket.connect(new InetSocketAddress(httpUrl.getHost(),httpUrl.getPort()));
            OutputStream os = socket.getOutputStream();
            InputStream is = socket.getInputStream();
            new Thread(){
                @Override
                public void run(a) {
                    HttpCodec httpCodec = new HttpCodec();
                    try {
                        // Read a response line
                        String responseLine = httpCodec.readLine(is);
                        System.out.println("Response line:" + responseLine);
                        // Read the response header
                        Map<String, String> headers = httpCodec.readHeaders(is);
                        for (Map.Entry<String, String> entry : headers.entrySet()) {
                            System.out.println(entry.getKey() + ":" + entry.getValue());
                        }
                        // Read the response body? You need to distinguish between content-Length and Chunked encoding
                        if (headers.containsKey("Content-Length")) {
                            int length = Integer.valueOf(headers.get("Content-Length"));
                            byte[] bytes = httpCodec.readBytes(is, length);
                            content = new String(bytes);
                            System.out.println("Response."+content);
                        } else {
                            // Block encoding
                            String response = httpCodec.readChunked(is);
                            content = response;
                            System.out.println("Response."+content); }}catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
            // Send the request
            os.write(request.toString().getBytes());
            os.flush();
            Thread.sleep(3000);
        } catch(Exception e) { e.printStackTrace(); }}Copy the code

Running effect

I/ system. out: response line: HTTP/1.1 302 Found
 I/System.out: Date: Sun, 03 Oct 2021 10:16:10 GMT
 I/System.out: Location: http://jz.yxg12.cn/newInfo.php?page=1&size=100
 I/System.out: Server: nginx
 I/System.out: Transfer-Encoding: chunked
 I/System.out: Content-Type: text/html; charset=UTF-8I/ system. out: Connection: keep-alive I/ system. out: response:Copy the code

As can be seen, there is no redirection effect, the first request is returned intact, there is no effect to do the request again. Since this is the result of not redirecting (returning the wrong data and containing the Location field in the return header), should redirecting be considered a rerequest for the Location property in the return header? We read the OkHttp source code with this question in mind.

public final class RetryAndFollowUpInterceptor implements Interceptor {... slightly@Override public Response intercept(Chain chain) throws IOException {... slightly// StreamAllocation is explained in connection interceptors
	StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
    createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }
      Response response;
      boolean releaseConnection = true; 
      response = realChain.proceed(request, streamAllocation, null.null); . Request followUp = followUpRequest(Response, streamAllocation. Route ());if (followUp == null) {
        if(! forWebSocket) { streamAllocation.release(); }returnresponse; }... Just request = followUp; priorResponse = response; }}... Slightly}Copy the code

The source code parsing

As you can see from this code, the redirection calls the method followUpRequest and requests the Request again through the while loop. See how followUpRequest handles it.

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) { ... slightly// 300 301 302 303 
      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; . slightly// Most redirects don't include a request body.Request.Builder requestBuilder = userResponse.request().newBuilder(); . slightlyreturnrequestBuilder.url(url).build(); . slightlydefault:
        return null; }}Copy the code

The source code parsing

As we guessed, when we encounter a code that needs to be redirected, we need to read the Location property in the return header, and then wrap the request header with the corresponding property as the new Url.

2. BridgeInterceptor

Note: can skip this kind of source code, directly look at the source code below the analysis, of course, you want to see, I do not block, after all, the source code is also posted.

public final class BridgeInterceptor implements Interceptor {
  private final CookieJar cookieJar;

  public BridgeInterceptor(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }

  @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();
      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");
        requestBuilder.removeHeader("Content-Length"); }}if (userRequest.header("Host") = =null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") = =null) {
      requestBuilder.header("Connection"."Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    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 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();
  }

  /** Returns a 'Cookie' HTTP request header with all cookies, like {@code a=b; c=d}. */
  private String cookieHeader(List<Cookie> cookies) {
    StringBuilder cookieHeader = new StringBuilder();
    for (int i = 0, size = cookies.size(); i < size; i++) {
      if (i > 0) {
        cookieHeader.append("; ");
      }
      Cookie cookie = cookies.get(i);
      cookieHeader.append(cookie.name()).append('=').append(cookie.value());
    }
    returncookieHeader.toString(); }}Copy the code

The source code interpretation

This interceptor, you can think of as a network request/response header splicing wrapper. The string concatenation shown in the picture below is done by the interceptor.

This interceptor is the simplest, nothing to say.

3. CacheInterceptor

Brace yourself, big, hard bones are coming.

public final class CacheInterceptor implements Interceptor {... slightly@Override public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache ! =null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    
	// analysis point 1 cacheStrategy.factory ().get();
    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()); // The cache candidate wasn't applicable. Close it.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    If (networkRequest == null && cacheResponse == null)
    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 we don't need the network, we're done.
    If (networkRequest == NULL) cacheResponse! =null
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      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()); }}// If we have a cache response too, then we're doing a conditional get.
    If (cacheResponse! = null) networkRequest ! =null
    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();

        // 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()); }}// networkRequest! =null cacheResponse =null
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

	// Analysis point 6
    if(cache ! =null) {
      // If there is no cache, write logic before cache
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        // Write cache
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.}}}returnresponse; }}Copy the code

The source code parsing

  • Analysis point 1: Think of it as getting the cache from a cache factory (more on how to get the cache later)
  • NetworkRequest: This call is null if no network is used, and not null if network is used
  • CacheResponse: This call is null if no cache is used and not null if caching is used
  • Analysis point 2: If neither network nor cache is used, the request returns an exception with code 504
  • Analysis point 3: If no network is used, then the cache is read and the last cached data is returned directly
  • Analysis point 4: If you are using the network and there is a cache, first make a network request, then update the cache, and finally return the latest data
  • Analysis point 5: If you are using a network and there is no local cache, ask for network data.
  • Analysis point 6: When the network data request comes in, determine whether to cache it, and if so, which states can be cached (more on that later).

3.1. How do I write to cache?

Let’s move on to analysis 6. The method that writes to the cache is, of course, the cacheWritingResponse method, but before we look at that, let’s look at what conditions are required to enter an if judgment.

If (cache! = null), indicating that the user indicates that the request needs caching; The second floor if judgment if (HttpHeaders hasBody (response) && CacheStrategy. IsCacheable (response, networkRequest)), calls the two methods respectively, HasBody and isCacheable, both of which are true, are used to write to the cache

HttpHeaders.hasBody

  /** Returns true if the response must have a (possibly 0-length) body. See RFC 7231. */
  public static boolean hasBody(Response response) {
    // HEAD requests never yield a body regardless of the response headers.
    if (response.request().method().equals("HEAD")) {
      return false;
    }

    int responseCode = response.code();
    // HTTP_CONTINUE:100
    if ((responseCode < HTTP_CONTINUE || responseCode >= 200) && responseCode ! = HTTP_NO_CONTENT// HTTP_NO_CONTENT 204
        									//HTTP_NOT_MODIFIED 304&& responseCode ! = HTTP_NOT_MODIFIED ) {return true;
    }

    // If the Content-Length or Transfer-Encoding headers disagree with the response code, the
    // response is malformed. For best compatibility, we honor the headers.
    if(contentLength(response) ! = -1
        || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      return true;
    }

    return false;
  }
Copy the code

The source code parsing

You can see from this method

  • The HEAD request cannot be cached
  • Network requests with a response body code less than 100 or greater than 200, but not 204 or 304, can be cached
  • Cache only if the Content-Length or Transfer-Encoding header matches the response code

Now comes the second method

CacheStrategy.isCacheable


  /** Returns true if {@code response} can be stored to later serve another request. */
  public static boolean isCacheable(Response response, Request request) {
    Always go to network for uncacheable response codes (RFC 7231 Section 6.1),
    // This implementation doesn't support caching partial content.
    switch (response.code()) {
      case HTTP_OK: / / 200
      case HTTP_NOT_AUTHORITATIVE: / / 203
      case HTTP_NO_CONTENT: / / 204
      case HTTP_MULT_CHOICE: / / 300
      case HTTP_MOVED_PERM: / / 301
      case HTTP_NOT_FOUND: / / 404
      case HTTP_BAD_METHOD: / / 405
      case HTTP_GONE: / / 410
      case HTTP_REQ_TOO_LONG: / / 414
      case HTTP_NOT_IMPLEMENTED: / / 501
      case StatusLine.HTTP_PERM_REDIRECT: / / 308
        // These codes can be cached unless headers forbid it.
        break;

      case HTTP_MOVED_TEMP: / / 302
      case StatusLine.HTTP_TEMP_REDIRECT: / / 307
        // These codes can only be cached with the right response headers.
        // http://tools.ietf.org/html/rfc7234#section-3
        // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
        if (response.header("Expires") != null|| response.cacheControl().maxAgeSeconds() ! = -1
            || response.cacheControl().isPublic()
            || response.cacheControl().isPrivate()) {
          break;
        }
        // Fall-through.

      default:
        // All other codes cannot be cached.
        return false;
    }

    // A 'no-store' directive on request or response prevents the response from being cached.
    return! response.cacheControl().noStore() && ! request.cacheControl().noStore(); }Copy the code

The source code parsing

  1. The last line of code does not Cache any cache-control property in the request header or response header with a no-store value
  2. In the case of 1, code needs to determine if there are any cache-allowed response headers in the case of 302 and 307 redirects
  3. In case 1, but not in case 2, the response of the corresponding code can be cached

Now that you’ve parsed all the judgments before writing to the cache, you can start analyzing the write to the cache.

Write cache (cacheWritingResponse)

  /**
   * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
   * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
   * may never exhaust the source stream and therefore not complete the cached response.
   */
  private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
      throws IOException {
    // Some apps return a null body; for compatibility we treat that like a null cache request.
    if (cacheRequest == null) return response;
    Sink cacheBodyUnbuffered = cacheRequest.body();
    if (cacheBodyUnbuffered == null) return response;
	// Get the cached data
    final BufferedSource source = response.body().source();
    // Another framework developed by Square is Okio read-write streams
    final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
	
	// Analysis point 7
    Source cacheWritingSource = new Source() {
      boolean cacheRequestClosed;

      @Override public long read(Buffer sink, long byteCount) throws IOException {
        long bytesRead;
        try {
          // Get the length of the data to be cached in bytes
          bytesRead = source.read(sink, byteCount);
        } catch (IOException e) {
          if(! cacheRequestClosed) { cacheRequestClosed =true;
            cacheRequest.abort(); // Failed to write a complete cache response.
          }
          throw e;
        }
	    // The cache object is closed if the length is abnormal
        if (bytesRead == -1) {
          if(! cacheRequestClosed) { cacheRequestClosed =true;
            cacheBody.close(); // The cache response is complete!
          }
          return -1;
        }

        sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
        cacheBody.emitCompleteSegments();
        return bytesRead;
      }

      @Override public Timeout timeout(a) {
        return source.timeout();
      }

      @Override public void close(a) throws IOException {
        if(! cacheRequestClosed && ! discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
          cacheRequestClosed = true; cacheRequest.abort(); } source.close(); }}; String contentType = response.header("Content-Type");
    long contentLength = response.body().contentLength();
    // Analysis point 8
    return response.newBuilder()
        .body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
        .build();
  }
Copy the code

Source code analysis

It starts with a standard non-null statement, and then points 7 and 8 use Square’s Okio read-write framework to write data from the cache to the SD card.

3.2. How to read the cache?

To avoid scrolling back and forth, here is a brief post of the source code inside the cache interceptor.

  @Override public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache ! =null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    
	// analysis point 1 cacheStrategy.factory ().get();
    CacheStrategy strategy = newCacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse; . Slightly}Copy the code

The source code parsing

Continuing with analysis 1, let’s look at the cache factory


    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if(cacheResponse ! =null) {
        // The local time when the corresponding request is issued and the local time when the response is received
        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

Source code analysis

There’s nothing more than a constructor, a for loop, and then a bunch of if conditional statements inside of it. Explain what each judgment means separately:

  • Date Indicates the time when the message is sent
  • Expires Indicates the expiration time of the resource
  • Last-modified Time when the resource was Last Modified
  • ETag The unique identification of the resource on the server
  • The Age server responds to requests with a cache, and how long has the cache elapsed since it was created (in seconds)

And then let’s look at the get method

    /**
     * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
     */
    public CacheStrategy get(a) {
      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 source code interpretation

With very little code, the method getCandidate() is used to get the corresponding cache. Go further.


    /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate(a) {
      // No cached response.
      // Check whether the cache exists:
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      // If the request is HTTPS and there is no handshake in the cache, the cache is invalid
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

	  // Write caching is described above, but not described here
      if(! isCacheable(cacheResponse, request)) {return new CacheStrategy(request, null);
      }

	  // If the user specified cache-control: no-cache(no Cache) request header or request header containing
          // if-modified-since or if-none-match, then caching is not allowed
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();
      // If the cached response contains cache-control: immutable, it means that the content of the response to the request will never change.
      // You can use the cache directly. Otherwise, continue to determine whether the cache is available
      if (responseCaching.immutable()) { // Analysis point 9
        return new CacheStrategy(null, cacheResponse);
      }


      // Get the cached response time from creation to present
      long ageMillis = cacheResponseAge();
      // Get the duration of the valid cache for this response
      long freshMillis = computeFreshnessLifetime();
      
      // If max-age is specified in the request, the cache validity period is specified.
      // We need to synthesize the effective response cache length and the request cache length to obtain the minimum available response cache length
      if(requestCaching.maxAgeSeconds() ! = -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }
     // Request contains cache-control :min-fresh=[seconds] can use the Cache before the specified time (request thought Cache valid time)
      long minFreshMillis = 0;
      if(requestCaching.minFreshSeconds() ! = -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }
          // cache-control :must-revalidate Can be cached but must be confirmed to the source server
	  // cache-control :max-stale=[SEC] Specifies how long a Cache can be used after it expires.
          // It does not matter how long the expiration time is. If specified, the cache can be used for as long as specified
	  // The former ignores the latter, so it is not necessary to check with the server to get max-stale in the request header
      long maxStaleMillis = 0;
      if(! responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() ! = -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }

	  // There is no need to verify validity with the server && the time the response exists + the time the request thinks the cache is valid is less than
          // Cache validity period + time to be used after expiration
	  // Cache is allowed
      if(! responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { Response.Builder builder = cacheResponse.newBuilder();// If it has expired, but has not exceeded the expiration period, it can continue to be used, just add the corresponding header field
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning"."110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        // If the cache is more than a day old and no expiration time is set in the response, you also need to add a warning
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning"."113 HttpURLConnection \"Heuristic expiration\"");
        }
        // Analysis point 10
        return 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();
          // Analysis point 11
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

Copy the code

The source code parsing

I’m so tired. I’m about to throw up, but I still have one last step to go. This is going to be in conjunction with the analysis above, and I’m going to paste it right here.

  • Analysis point 1: Think of it as getting the cache from a cache factory (more on how to get the cache later)
  • NetworkRequest: This call is null if no network is used, and not null if network is used
  • CacheResponse: This call is null if no cache is used and not null if caching is used
  • Analysis point 2: If neither network nor cache is used, the request returns an exception with code 504
  • Analysis point 3: If no network is used, then the cache is read and the last cached data is returned directly
  • Analysis point 4: If you are using the network and there is a cache, first make a network request, then update the cache, and finally return the latest data
  • Analysis point 5: If you are using a network and there is no local cache, ask for network data.
  • Analysis point 6: When the network data request comes in, determine whether to cache it, and if so, which states can be cached (more on that later).
	// analysis point 1 cacheStrategy.factory ().get();
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

	public final class CacheStrategy {
	  CacheStrategy(Request networkRequest, Response cacheResponse) {
	    this.networkRequest = networkRequest;
	    this.cacheResponse = cacheResponse; }}Copy the code

The cache attribute is the second variable in the constructor, that is, to use the cache, the second parameter must not be empty. In the getCandidate method, the cache is actually used at the corresponding analysis points 9, 10, and 11.

  • Analysis point 9: Cache data is safe to fetch if cached responses contain cache-control: immutable, meaning that the content of the response to a request never changes
  • Analysis point 10: If analysis 9 is not met, the cached data is taken during the cache validity period
  • Analysis point 11: if analysis points 9 and 10 are not met, the cached data is fetched as long as the cached etag (cached response), lastModified (lastModified time), and servedDate (world of the response server) are not empty

4. ConnectInterceptor

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;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    booleandoExtensiveHealthChecks = ! request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    returnrealChain.proceed(request, streamAllocation, httpCodec, connection); }}Copy the code

The source code parsing

Connect the interceptor code here is relatively small, main connection functions are encapsulated in streamAllocation. NewStream there, And StreamAllocation variables in RetryAndFollowUpInterceptor (redirect/retry interceptor) created inside. Now look at the newStream method.

  public HttpCodec newStream(
      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);
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        returnresultCodec; }}catch (IOException e) {
      throw newRouteException(e); }}Copy the code

The source code parsing

The main logic here is in the findHealthyConnection method, so go inside

  /** * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated * until a healthy connection is found. */
  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          returncandidate; }}// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if(! candidate.isHealthy(doExtensiveHealthChecks)) { noNewStreams();continue;
      }

      returncandidate; }}Copy the code

The source code parsing

There is less code. Here you can see that an infinite loop is started, and then you call findConnection. The main logic is in this method.


  /** * Returns a connection to host a new stream. This prefers the existing connection if it exists, * then the pool, finally building a new connection. */
  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) {
      if (released) throw new IllegalStateException("released");
      if(codec ! =null) throw new IllegalStateException("codec ! = null");
      if (canceled) throw new IOException("Canceled");

      // Attempt to use an already-allocated connection. We need to be careful here because our
      // already-allocated connection may have been restricted from creating new streams.
      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      // Check whether the connection is currently in KeepLive
      if (this.connection ! =null) {
        // We had an already-allocated connection and it's good.
        result = this.connection;
        releasedConnection = null;
      }
      if(! reportedAcquired) {// If the connection was never reported acquired, don't report it as released!
        releasedConnection = null;
      }
	 The connection is not currently in KeepLive state
      if (result == null) {
        // Attempt to get a connection from the pool.
        // Try to get the requested link from the connection pool
        Internal.instance.get(connectionPool, address, this.null);
        if(connection ! =null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    closeQuietly(toClose);

    if(releasedConnection ! =null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
    }
    // When we get the request link from the connection pool for KeepLive, return
    if(result ! =null) {
      // If we found an already-allocated or pooled connection, we're done.
      return result;
    }
	// Failed to fetch the corresponding link from the pool
    // If we need a route selection, make one. This is a blocking operation.
    boolean newRouteSelection = false;
    
    // Determine if there is a thread of the next layer. If there is, look for the corresponding request link in the thread pool of the next layer
    if (selectedRoute == null && (routeSelection == null| |! routeSelection.hasNext())) { newRouteSelection =true;
      routeSelection = routeSelector.next();
    }

    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        List<Route> routes = routeSelection.getAll();
        // Go through all the existing lines to see if there is a corresponding request
        for (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;
            result = connection;
            this.route = route;
            break; }}}// The request link was not found, so prepare to create a new request RealConnection to be linked
      if(! foundPooledConnection) {if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        route = selectedRoute;
        refusedStreamCount = 0;
        result = new RealConnection(connectionPool, selectedRoute);
        acquire(result, false); }}// If an existing link is found, return directly
    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }
	
	// Set TCP + TLS handshake on the newly created connection object.
    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;
	 // Add the current connection to the thread pool
      // Pool the connection.
      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 source code parsing

Connected at the heart of the interceptor is this method, the main logic: first, determine whether allocated connection exists, if not, then from the thread pool inside looking for first, the thread pool is not found inside and then to the corresponding lines to find, if don’t find, finally will open up a new connection, add to the thread pool and the object returns after the connection is successful.

CallServerInterceptor(Read/write/request interceptor)

public final class CallServerInterceptor implements Interceptor {
  private final boolean forWebSocket;

  public CallServerInterceptor(boolean forWebSocket) {
    this.forWebSocket = forWebSocket;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    // httpCodec this is set from the connection interceptor
    HttpCodec httpCodec = realChain.httpStream();  
    StreamAllocation This is set from the redirection interceptor
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    // Request this is set from the bridge interceptor
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();

    realChain.eventListener().requestHeadersStart(realChain.call());
    // Write the contents of the bridge interceptor
    httpCodec.writeRequestHeaders(request);
    realChain.eventListener().requestHeadersEnd(realChain.call(), request);

    Response.Builder responseBuilder = null;
	/ /! (method.equals("GET") || method.equals("HEAD"))
	// If the request is not get, or if it is HEAD and the request parameter cannot be empty
    if(HttpMethod.permitsRequestBody(request.method()) && request.body() ! =null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
		// Start fetching request parameters
        realChain.eventListener().requestBodyStart(realChain.call());
        long contentLength = request.body().contentLength();
        CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
		// Write the request parameters to the object to be requested
        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 in a consistent state.streamAllocation.noNewStreams(); }}// We are ready for the network request
    httpCodec.finishRequest();

    if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(false);
    }
	// Start requesting network
    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual response
      // Request again if the response code is 100
      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);

    if (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 {
    // A normal HTTP web request would simply go else, and then re-wrap the response body data via openResponseBody
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    returnresponse; }... Slightly}Copy the code

The source code parsing

This class contains all the content of the above interceptor, including the httpCode set by the connection interceptor, the request body and response body wrapped by the bridge interceptor, and the redirected interceptor. The main function is the network interface request, play a network request body and response body encapsulation. The written request body is taken to the request network, and the written response body is returned to the upper level once.

Now that the chapter is almost over, let’s conclude with a flow chart.

6. Interceptor flow

As is shown in

You can easily follow this flowchart and get a sense of where the OkHttp interceptor is headed. At this point, the entire OkHttp source code has been completely parsed.

In the next article, I’ll write a castrated version of OkHttp, modeled after the OkHttp structure, to reinforce my understanding of OkHttp source code.