primers

OkHttp well-known third-party network framework SDK, easy to use, excellent performance, but the kernel is not simple, this series of articles, specifically focused on the core knowledge in detail. Hardcore is something you can’t get away with if you want to get into it.

There are too many types of interceptors to cover in detail, so I’ve picked the most useful ones to summarize.

Go into the heart of OKHttp, the interceptor. However, there are a lot of interceptors, some of which come with the system, and some we can customize.

You can first look at the first article – you must learn to learn OKHttp (easily leave GitHub link, need to obtain the relevant interview or interview treasure book core notes PDF and other content can go to find) github.com/xiangjiana/…





This is the starting point for the core approach to network request execution, which involves many interceptors.

The body of the outline

The system comes with interceptors

1 retry and redirect blocker RetryAndFollowUpInterceptor 2 bridge interceptor 3 cache interceptor CacheInterceptor 4 connect blocker ConnectInterceptor 5 service invocation interceptors CallServerInterceptor

The body of the

Before explanation interceptors, it is necessary to first will RealCall getResponseWithInterceptorChain () method of the last two lines:

  Interceptor.Chain chain = newRealInterceptorChain( interceptors, null, null, null, 0, originalRequest);
  return chain.proceed(originalRequest);
Copy the code

This finally returns a Response, enters the chain.proceed method, and finally indexes to the RealInterceptorChain’s proceed method:






interceptor.intercept(next)
RetryAndFollowUpInterceptor







chain.proceed
RealInterceptorChain.proceed()
intercept
Response

This paragraph is the core of the OKHTTP responsibility chain pattern and should be easy to understand

The system comes with interceptors

1. Retry and redirect RetryAndFollowUpInterceptor interceptor

First, the conclusion:

As the name implies, Retry, FollowUp redirection. This interceptor is the first of its kind to decide whether to retry and redirect the current request, so what we should care about is: when to retry and when to redirect. Also, it determines if the user has canceled the request, because RealCall has a cancel method that allows the user to cancel the request (although there are two cases of canceling before the request is issued and canceling after the request is issued). If the request is cancelled before the request is sent, the subsequent procedure is not executed. If the request is cancelled after the request is sent, the client will discard the response.

Retry RetryAndFollowUpInterceptor the core method of interceptor () :

@Override public Response intercept(Chain chain) throws IOException { ... omitwhile (true) {... Omit the try {response = ((RealInterceptorChain) chain). 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(),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. boolean requestSendStarted = ! (e instanceof ConnectionShutdownException);if(! recover(e, requestSendStarted, request)) throw e; releaseConnection =false;
        continue; }... omitif (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }
      ...省略

    }
  }
Copy the code

In the above code, I only kept the key parts. There are two continue and one return. When the request reaches the interceptor, it enters a while(true) loop,

When a RouteException occurs (because the request has not yet been sent, the route is abnormal, or the connection failed), the return value of the Recover method is evaluated and whether to continue is determined based on the return value. After an IOException occurs (the request was sent, but communication with the server failed), determine the return value of the Recover method and decide whether to continue based on the return value. If neither continues is executed, it is possible to end the request with a final ReturnResponse. The logic to determine whether to retry is inside the recover() method:

private boolean recover(IOException e, StreamAllocation streamAllocation, boolean requestSendStarted, Request userRequest) { streamAllocation.streamFailed(e); //The application layer has forbidden retries if a request fails.if(! client.retryOnConnectionFailure())return false; // Todo 2, because requestSendStarted is only in http2 I/O exceptionfalse, http1 istrue, / / in the case of http1, needs to determine body ever achieve UnrepeatableRequestBody interface, and the body is not implemented by default, so the follow-up instanceOf fails, will not goreturn false.
        //We can't send the request body again. if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false; //This exception is fatal. 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

A quick explanation of this method:

  • ifokhttpClientRetries are not allowed, so return false and do not retry.
  • ifrequestSendStartedOnly in thehttp2.0IO exception is true, howeverHTTP2.0It’s not popular yet, so let’s ignore it. It passes by default.
  • Determine if it is a retry exception, that is, if an exception occurred after the previous retry. So just to read it a little bit, there was an Exception thrown in the retry, this oneisRecoverableThe method determines, based on the exception, whether it is necessary to retry.
  • Protocol exception. If a protocol exception occurs, there is no need to retry. There may be a problem with your request or the server itself.
  • Timeout exception, just timeout, retry directly (here)requestSendStartedishttp2Is true, so the default is false.
  • The SSL is abnormal. The HTTPS certificate is abnormal. There is no need to retry.
  • SSLHandshake unauthorized exception, do not retry
Private Boolean isRecoverable(IOException e, Boolean requestSendStarted) {// A protocol exception occurs and cannot be retriedif (e instanceof ProtocolException) {
      return false; } // requestSendStarted thinks it will always befalse(http2) the exception is a socket timeout exception and can be retriedif (e instanceof InterruptedIOException) {
      returne instanceof SocketTimeoutException && ! requestSendStarted; } // The SSL handshake is abnormal. The certificate is faulty and cannot be retriedif (e instanceof SSLHandshakeException) {
      if (e.getCause() instanceof CertificateException) {
        return false; }} // The SSL handshake is not authorizedif (e instanceof SSLPeerUnverifiedException) {
      return false;    }
    return true;
}
Copy the code

Is there a route that can be used to connect? That is, if multiple IP addresses are returned when DNS resolves a domain name, then it is possible to try again one by one until no more IP addresses are available.

Redirect method still is the core of RetryAndFollowUpInterceptor interceptor () method, I capture during the second half of this time:

  public Response intercept(Chain chain) throws IOException {
     while (true) {... Request followUp = followUpRequest(Response, streamallocation.route ()); // Todo handles status codes for 3 and 4xx, such as 301 302 redirects Request followUp = followUpRequest(Response, streamallocation.route ());if (followUp == null) {
                if (!forWebSocket) {
                    streamAllocation.release();
                }
                returnresponse; } closeQuietly(response.body()); // Todo limits followup to 20 timesif (++followUpCount > MAX_FOLLOW_UPS) {
                streamAllocation.release();
                throw new ProtocolException("Too many follow-up requests: " + followUpCount);
            }

            if (followUp.body() instanceof UnrepeatableRequestBody) {
                streamAllocation.release();
                throw new HttpRetryException("Cannot retry streamed HTTP body", response.code()); } //todo determines whether the same connection can be reusedif(! sameConnection(response, followUp.url())) { streamAllocation.release(); streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(followUp.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; }else if(streamAllocation.codec() ! = null) { throw new IllegalStateException("Closing the body of " + response
                        + " didn't close its backing stream. Bad interceptor?"); }}}Copy the code

The followUpRequest() method specifies which response codes can be redirected:

  private Request followUpRequest(Response userResponse) throws IOException {
    if(userResponse == null) throw new IllegalStateException(); Connection connection = streamAllocation.connection(); Route route = connection ! = null ? connection.route() : null; int responseCode = userResponse.code(); final String method = userResponse.request().method(); Switch (responseCode) {// 407 If the client uses an HTTP Proxy server, add proxy-authorization in the request header for Proxy AuthorizationcaseHTTP_PROXY_AUTH: Proxy selectedProxy = route ! = null ? route.proxy() : client.proxy();if(selectedProxy.type() ! = Proxy.Type.HTTP) { throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        returnclient.proxyAuthenticator().authenticate(route, userResponse); // Add Authorization to request headerscase HTTP_UNAUTHORIZED:
        returnclient.authenticator().authenticate(route, userResponse); // 308 permanent redirect // 307 temporary redirectcase HTTP_PERM_REDIRECT:
      caseHTTP_TEMP_REDIRECT: // The framework does not automatically redirect requests if they are not GET or HEADif(! method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      // 300 301 302 303
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      caseHTTP_SEE_OTHER: // Return null if the user does not allow redirectionif(! client.followRedirects())returnnull; String location = userResponse.header("Location");
        if (location == null) returnnull; HttpUrl url = userResponse.request().url().resolve(location); // If HttpUrl is null, the protocol is faulty and the HttpUrl cannot be retrievedif (url == null) returnnull; // If the redirect switches between HTTP and HTTPS, Boolean sameScheme =url.scheme().equals(userResponse.request().url().scheme());if(! sameScheme && ! client.followSslRedirects())returnnull; Request.Builder requestBuilder = userResponse.request().newBuilder(); /** * Redirects requests that are not PROPFIND requests, either POST or other methods should be changed to GET requests, * i.e. only PROPFIND requests can have request bodies */ / requests are not GET and headif(HttpMethod.permitsRequestBody(method)) { final boolean maintainBody = HttpMethod.redirectsWithBody(method); // All requests except PROPFIND are changed to GET requestsif (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else{ RequestBody requestBody = maintainBody ? userResponse.request().body() : null; requestBuilder.method(method, requestBody); } // If the request is not PROPFIND, delete the data in the request header about the request bodyif(! maintainBody) { requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type"); }} // Delete the authentication request header when redirecting across hostsif(! sameConnection(userResponse, url)) { requestBuilder.removeHeader("Authorization");
        }

        returnrequestBuilder.url(url).build(); // 408 The client request timed outcaseHTTP_CLIENT_TIMEOUT: // 408 The connection failed, so check whether the user is allowed to retryif(! client.retryOnConnectionFailure()) {returnnull; } // UnrepeatableRequestBody is not actually used elsewhereif (userResponse.request().body() instanceof UnrepeatableRequestBody) {
            returnnull; } // If this response is the result of a new request and the last request was made again because 408, then we will not re-request this timeif(userResponse.priorResponse() ! = null &&userResponse.priorResponse().code()==HTTP_CLIENT_TIMEOUT) {returnnull; } // If the server tells us how long to Retry After, the framework doesn't care.if (retryAfter(userResponse, 0) > 0) {
            return null;
        }
        returnuserResponse.request(); // 503 service unavailable is similar to 408, but only requests again if the server tells you to Retry After: 0case HTTP_UNAVAILABLE:
        if(userResponse.priorResponse() ! = null && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {return null;
         }

         if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
             return userResponse.request();
         }

         return null;
      default:
        returnnull; }}Copy the code

So if you look at this method, it looks at the response code and decides whether or not to return a new request, and if it returns a new request, The peripheral (see RetryAndFollowUpInterceptor intercept method) while (true) infinite loop will use the new request again request, complete the redirection. For details please see the above code comments, from a master, written in detail. Here’s the general conclusion:

  • The response code 3XX usually returns a new Request, and the other return NULL does not allow redirection.
  • Followup can happen up to 20 times

But again, we are not specialized in network architecture or optimization. If we understand the basic function of this interceptor, we can only use the important nodes. If we really need to cut down the details, no one can remember so clearly.

2. BridgeInterceptor

This is probably the most simple among the five a blocker, after it get the request from a layer RetryAndFollowUpInterceptor, do only one thing: We use OkHttp to send network requests, and usually just addHeader with some parameters related to our business, but the real request header is not that simple. The server not only needs to identify the service parameters, but also needs to identify the request type and the parsing method of the request body, as listed below:









1, save the cookie, the next request of the same domain name will bring the cookie in the request header, but this request ourselvesokHttpClienttheCookieJarTo achieve a specific process.







gzip
GzipSource

3. CacheInterceptor

This article only describes its role, because the internal logic is so complex that it must be explained separately.

(conveniently leave GitHub link, need to obtain the relevant interview or interview treasure book core notes PDF and other content can go to find) github.com/xiangjiana/…