[Android open source framework interview questions] Talk about the principles of OkHttp framework_Part 2

The book is continued from the above article "[Android open source framework interview questions] Talk about the principles of OkHttp framework_Part 1"

Interceptor details
1. Retry and redirect interceptors

The first interceptor: RetryAndFollowUpInterceptor, mainly accomplishes two things: retry and redirect.

Retry

If a RouteException or IOException occurs during the request phase, it will be judged whether to reinitiate the request.

RouteException

catch (RouteException e) {
    
    
	//todo 路由异常,连接未成功,请求还没发出去
    if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
    
    
    	throw e.getLastConnectException();
    }
    releaseConnection = false;
    continue;
} 

IOException

catch (IOException e) {
    
    
	// 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
    // HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true
	boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
	if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
		releaseConnection = false;
		continue;
} 

Both exceptions are based on recoverthe method to determine whether retry can be performed. If returned true, it means that retry is allowed.

private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
    
    
	streamAllocation.streamFailed(e);
	//todo 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
	if (!client.retryOnConnectionFailure()) return false;
	//todo 2、如果是RouteException,不用管这个条件,
    // 如果是IOException,由于requestSendStarted只在http2的io异常中可能为false,所以主要是第二个条件
	if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
		return false;

	//todo 3、判断是不是属于重试的异常
	if (!isRecoverable(e, requestSendStarted)) return false;

	//todo 4、有没有可以用来连接的路由路线
	if (!streamAllocation.hasMoreRoutes()) return false;

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

So first of all, if the user does not prohibit retrying , if some exception occurs and there are more routing lines, the user will try to change the line to retry the request. Some of these exceptions are isRecoverablejudged in:

private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    
    
	// 出现协议异常,不能重试
    if (e instanceof ProtocolException) {
    
    
      return false;
    }

	// 如果不是超时异常,不能重试
    if (e instanceof InterruptedIOException) {
    
    
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }

    // SSL握手异常中,证书出现问题,不能重试
    if (e instanceof SSLHandshakeException) {
    
    
      if (e.getCause() instanceof CertificateException) {
    
    
        return false;
      }
    }
    // SSL握手未授权异常 不能重试
    if (e instanceof SSLPeerUnverifiedException) {
    
    
      return false;
    }
    return true;
}

1. The protocol is abnormal . If so, it is directly judged that it cannot be retried; (there is a problem with your request or the server's response itself. The data is not defined according to the http protocol, and it is useless to try again)

2. Timeout exception . The Socket connection may have timed out due to network fluctuations. You can try again using different routes.

3. SSL certificate exception/SSL verification failure exception . The former means that the certificate verification failed, and the latter may mean that there is no certificate at all, or the certificate data is incorrect. How can I try again?

After the abnormal determination, if retries are still allowed, it will be checked again to see if there are currently available routing routes for connection. To put it simply, for example, DNS may return multiple IPs after resolving domain names. After one IP fails, try another IP and try again.

Redirect

If no exception occurs after the request is completed, it does not mean that the response currently obtained is the one that ultimately needs to be handed over to the user. Further judgment is needed to determine whether redirection is needed. The judgment of redirection is located in followUpRequestthe method

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 客户端使用了HTTP代理服务器,在请求头中添加 “Proxy-Authorization”,让代理服务器授权
      case HTTP_PROXY_AUTH:
        Proxy selectedProxy = route != null
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
    
    
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);
      // 401 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization” 
      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);
      // 308 永久重定向 
      // 307 临时重定向
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
        if (!method.equals("GET") && !method.equals("HEAD")) {
    
    
          return null;
        }
      // 300 301 302 303 
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // 如果用户不允许重定向,那就返回null
        if (!client.followRedirects()) return null;
        // 从响应头取出location 
        String location = userResponse.header("Location");
        if (location == null) return null;
        // 根据location 配置新的请求 url
        HttpUrl url = userResponse.request().url().resolve(location);
        // 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
        if (url == null) return null;
        // 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        Request.Builder requestBuilder = userResponse.request().newBuilder();
		/**
		 *  重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式,
		 *  即只有 PROPFIND 请求才能有请求体
		 */
		//请求不是get与head
        if (HttpMethod.permitsRequestBody(method)) {
    
    
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
           // 除了 PROPFIND 请求之外都改成GET请求
          if (HttpMethod.redirectsToGet(method)) {
    
    
            requestBuilder.method("GET", null);
          } else {
    
    
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          // 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉
          if (!maintainBody) {
    
    
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // 在跨主机重定向时,删除身份验证请求头
        if (!sameConnection(userResponse, url)) {
    
    
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      // 408 客户端请求超时 
      case HTTP_CLIENT_TIMEOUT:
        // 408 算是连接失败了,所以判断用户是不是允许重试
       	if (!client.retryOnConnectionFailure()) {
    
    
			return null;
		}
		// UnrepeatableRequestBody实际并没发现有其他地方用到
		if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
    
    
			return null;
		}
		// 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求了
		if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
    
    
			return null;
		}
		// 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
		if (retryAfter(userResponse, 0) > 0) {
    
    
			return null;
		}
		return userResponse.request();
	   // 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求
 	   case 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:
        return null;
    }
}

There is a lot of content to judge whether redirection is needed. It is normal to have trouble remembering it. The key is to understand what they mean. If this method returns empty, it means that there is no need to redirect and the response is returned directly; but if it returns non-empty, then the returned request must be re-requested Request, but it should be noted that the maximum number of times we followupdefine in the interceptor for 20 times.

Summarize

This interceptor is the first in the entire responsibility chain, which means it will be the first contact Requestand the last received Responserole. The main function of this interceptor is to determine whether retry and redirection are needed.

The prerequisite for retrying is that RouteExceptionor IOException. Once these two exceptions occur during the subsequent execution of the interceptor, recovermethods will be used to determine whether to retry the connection.

Redirection occurs after the retry determination. If the retry conditions are not met, the response code based on needs to be further called (of course, if the direct request fails, followUpRequestan exception will be thrown if it does not exist). Occurs up to 20 times.ResponseResponsefollowup

2. Bridge interceptor

BridgeInterceptor, a bridge connecting applications and servers. The requests we make will be processed by it before being sent to the server, such as setting the request content length, encoding, gzip compression, cookies, etc., and saving cookies after obtaining the response. This interceptor is relatively simple.

Completion request header:

Request header illustrate
Content-Type Request body type, such as:application/x-www-form-urlencoded
Content-Length/Transfer-Encoding Request body parsing method
Host Requested host site
Connection: Keep-Alive Keep a long connection
Accept-Encoding: gzip Accept responses support gzip compression
Cookie cookie identification
User-Agent Requested user information, such as: operating system, browser, etc.

After completing the request header, it is handed over to the next interceptor for processing. After getting the response, it mainly does two things:

1. Save the cookie. In the next request, the corresponding data settings will be read and entered into the request header. The default CookieJarimplementation is not provided.

2. If you use data returned by gzip, use GzipSourcepackaging to facilitate parsing.

Summarize

The execution logic of the bridge interceptor is mainly the following points

Add or delete relevant header information built by the user Requestto convert it into one that can actually make network requests. Request
Hand the Request that conforms to the network request specification to the next interceptor for processing, and obtain Response
the response body. If the response body has been GZIP compressed, then it is necessary Unzip it, build it into something user-usable Responseand return it

3. Cache interceptor

CacheInterceptor, before making a request, determine whether the cache is hit. If there is a hit, you can use the cached response directly without requesting it. (Only the cache for Get requests will exist)

The steps are:

1. Obtain the response cache corresponding to the request from the cache

2. Create CacheStrategy. When creating, it will be judged whether the cache can be used. CacheStrategyThere are two members in: networkRequestand cacheResponse. Their combination is as follows:

networkRequest cacheResponse illustrate
Null Not Null Use cache directly
Not Null Null Make a request to the server
Null Null Directly gg, okhttp directly returns 504
Not Null Not Null Initiate a request. If the response is 304 (no modification), update the cached response and return

3. Hand it over to the next chain of responsibility to continue processing.

4. For subsequent work, if 304 is returned, use the cached response; otherwise, use the network response and cache this response (only the response to the Get request is cached)

The work of the cache interceptor is relatively simple, but the specific implementation requires a lot of processing. Determining whether the cache can be used or requesting the server is judged in the cache interceptor CacheStrategy.

caching strategy

CacheStrategy. First, you need to understand several request headers and response headers

Response header illustrate example
Date The time the message was sent Date: Sat, 18 Nov 2028 06:17:41 GMT
Expires Resource expiration time Expires: Sat, 18 Nov 2028 06:17:41 GMT
Last-Modified Resource last modified time Last-Modified: Fri, 22 Jul 2016 02:57:17 GMT
ETag The unique identifier of the resource on the server ETag: “16df0-5383097a03d40”
Age The server responds to the request with the cache. How long has it been since the cache was created (seconds) Age: 3825683
Cache-Control - -
Request header illustrate example
If-Modified-Since The server does not modify the resource corresponding to the request after the specified time and returns 304 (No modification) If-Modified-Since: Fri, 22 Jul 2016 02:57:17 GMT
If-None-Match The server compares it with the value of the corresponding resource requested Etag, and returns 304 if it matches. If-None-Match: “16df0-5383097a03d40”
Cache-Control - -

It Cache-Controlcan exist in the request header or the response header, and the corresponding value can be set in multiple combinations:

  1. max-age=[秒]: The maximum validity time of the resource;
  2. public: Indicates that the resource can be cached by any user, such as clients, proxy servers, etc., which can cache resources;
  3. private: Indicates that the resource can only be cached by a single user. The default is private.
  4. no-store: Resources are not allowed to be cached
  5. no-cache(Request) Do not use caching
  6. immutable(Response) The resource will not change
  7. min-fresh=[秒](Request) Cache minimum freshness (how long the user considers this cache to be valid)
  8. must-revalidate(Response) Expiration cache is not allowed
  9. max-stale=[秒](Request) How long does the cache remain valid after it expires?

Assume that there are max-age=100 and min-fresh=20. This means that the user thinks that the cached response takes 100-20=80s from the time the server creates the response to the time it can be cached. But if max-stale=100. This means that after the cache validity time of 80s has passed, it is still allowed to be used for 100s, which can be regarded as the cache validity period is 180s.

Insert image description here

Detailed process

If the URL corresponding to this request is obtained from the cache Response, the above data will first be obtained from the response for later use.

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

            if (cacheResponse != null) {
    
    
                //对应响应的请求发出的本地时间 和 接收到响应的本地时间
                this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
                this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
                Headers headers = cacheResponse.headers();
                for (int i = 0, size = headers.size(); i < size; i++) {
    
    
                    String fieldName = headers.name(i);
                    String value = headers.value(i);
                    if ("Date".equalsIgnoreCase(fieldName)) {
    
    
                        servedDate = HttpDate.parse(value);
                        servedDateString = value;
                    } else if ("Expires".equalsIgnoreCase(fieldName)) {
    
    
                        expires = HttpDate.parse(value);
                    } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
    
    
                        lastModified = HttpDate.parse(value);
                        lastModifiedString = value;
                    } else if ("ETag".equalsIgnoreCase(fieldName)) {
    
    
                        etag = value;
                    } else if ("Age".equalsIgnoreCase(fieldName)) {
    
    
                        ageSeconds = HttpHeaders.parseSeconds(value, -1);
                    }
                }
            }
        }

get()How to determine cache hits

public CacheStrategy get() {
    
    
	CacheStrategy candidate = getCandidate();
	//todo 如果可以使用缓存,那networkRequest必定为null;指定了只使用缓存但是networkRequest又不为null,冲突。那就gg(拦截器返回504)
	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;
}

The method is called in getCandidate()the method to complete the real cache judgment.

1. Does the cache exist?

The first judgment in the entire method is whether the cache exists:

if (cacheResponse == null) {
    
    
	return new CacheStrategy(request, null);
}

cacheResponseIt is the response found in the cache. If it is null, it means that the corresponding cache is not found and the created CacheStrategyinstance object only exists networkRequest, which means that a network request needs to be initiated.

2. Caching of https requests

Going further down means cacheResponseit must exist, but it may not be usable. A series of subsequent judgments on effectiveness will be made

if (request.isHttps() && cacheResponse.handshake() == null) {
    
    
	return new CacheStrategy(request, null);
}

If this request is HTTPS but there is no corresponding handshake information in the cache, the cache is invalid.

3. Response code and response header
if (!isCacheable(cacheResponse, request)) {
    
    
	return new CacheStrategy(request, null);
}

The entire logic is in isCacheableit, and its content is:

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:
            case HTTP_NOT_AUTHORITATIVE:
            case HTTP_NO_CONTENT:
            case HTTP_MULT_CHOICE:
            case HTTP_MOVED_PERM:
            case HTTP_NOT_FOUND:
            case HTTP_BAD_METHOD:
            case HTTP_GONE:
            case HTTP_REQ_TOO_LONG:
            case HTTP_NOT_IMPLEMENTED:
            case StatusLine.HTTP_PERM_REDIRECT:
                // These codes can be cached unless headers forbid it.
                break;

            case HTTP_MOVED_TEMP:
            case StatusLine.HTTP_TEMP_REDIRECT:
                // 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();
}

When the response code in the cached response is 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308, it is only judged whether the server has given it Cache-Control: no-store(the resource cannot be cached), so if the server gives When it comes to this response header, it is consistent with the previous two determinations (the cache is not available). Otherwise, continue to further determine whether the cache is available.

And if the response code is 302/307 (redirect), you need to further determine whether there are some response headers that allow caching. According to the description in the document http://tools.ietf.org/html/rfc7234#section-3 given in the annotation, if there is Expiresor Cache-Controlthe value is:

  1. max-age=[秒]: The maximum validity time of the resource;

  2. public: Indicates that the resource can be cached by any user, such as clients, proxy servers, etc., which can cache resources;

  3. private: Indicates that the resource can only be cached by a single user. The default is private.

If it does not exist at the same time Cache-Control: no-store, you can continue to further determine whether the cache is available.

So comprehensively, the priority is determined as follows:

1. The response code is not 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308, 302 , 307 cache is not available;

2. When the response code is 302 or 307 and some response headers are not included, the cache is unavailable;

3. When there is Cache-Control: no-storea response header, the cache is not available.

If the response cache is available, further determine the cache validity.

4. User request configuration
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
    
    
	return new CacheStrategy(request, null);
}
private static boolean hasConditions(Request request) {
    
    
	return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
}

At this point, OkHttp needs to first determine the request initiated by the user Request. If the user specifies Cache-Control: no-cachea request header (not using cache) or the request header contains If-Modified-Sinceor If-None-Match(request verification), then caching is not allowed.

Request header illustrate
Cache-Control: no-cache Ignore cache
If-Modified-Since: 时间 The value is generally Dataor lastModified. If the server does not modify the resource corresponding to the request after the specified time, it returns 304 (no modification).
If-None-Match:标记 The value is generally Etag, compare it with Etagthe value of the corresponding resource requested; if it matches, return 304

This means that if the user request header contains these contents, a request must be made to the server. However, it should be noted that OkHttp does not cache 304 responses. If this is the case, that is, the user actively requests to initiate a request with the server, and the server returns 304 (no response body), the 304 response will be directly returned to the user: "Since you requested it, I will only inform you of the results of this request . " If these request headers are not included, continue to determine the cache validity.

5. Whether the resources remain unchanged
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
    
    
	return new CacheStrategy(null, cacheResponse);
}

If included in a cached response Cache-Control: immutable, this means that the response content for the corresponding request will never change. At this point you can use the cache directly. Otherwise, continue to determine whether the cache is available.

6. Response cache validity period

This step is to further determine whether the cache is within the validity period based on some information in the cache response. If satisfied:

Cache survival time < cache freshness - minimum cache freshness + duration of continued use after expiration

Delegates can use caching. The freshness can be understood as the effective time, and the "cache freshness - cache minimum freshness" here represents the real effective time of the cache.

// 6.1、获得缓存的响应从创建到现在的时间
long ageMillis = cacheResponseAge();
//todo
// 6.2、获取这个响应有效缓存的时长
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
    
    
//todo 如果请求中指定了 max-age 表示指定了能拿的缓存有效时长,就需要综合响应有效缓存时长与请求能拿缓存的时长,获得最小的能够使用响应缓存的时长
		freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
// 6.3 请求包含  Cache-Control:min-fresh=[秒]  能够使用还未过指定时间的缓存 (请求认为的缓存有效时间)
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
    
    
	minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
// 6.4
//  6.4.1、Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认
//  6.4.2、Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长  如果未指定多少秒,则表示无论过期多长时间都可以;如果指定了,则只要是指定时间内就能使用缓存
	// 前者会忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    
    
	maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

// 6.5 不需要与服务器验证有效性 && 响应存在的时间+请求认为的缓存有效时间 小于 缓存有效时长+过期后还可以使用的时间
// 允许使用缓存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    
    
	Response.Builder builder = cacheResponse.newBuilder();
	//todo 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
	if (ageMillis + minFreshMillis >= freshMillis) {
    
    
		builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
	}
	//todo 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
	long oneDayMillis = 24 * 60 * 60 * 1000L;
	if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
    
    
		builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
	}
	return new CacheStrategy(null, builder.build());
}

6.1. The time the cache has survived until now: ageMillis

First, cacheResponseAge()the method obtains the response and approximately how long it exists:

long ageMillis = cacheResponseAge();

private long cacheResponseAge() {
    
    
	long apparentReceivedAge = servedDate != null
                    ? Math.max(0, receivedResponseMillis - servedDate.getTime())
                    : 0;
	long receivedAge = ageSeconds != -1
                    ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
                    : apparentReceivedAge;
	long responseDuration = receivedResponseMillis - sentRequestMillis;
	long residentDuration = nowMillis - receivedResponseMillis;
	return receivedAge + responseDuration + residentDuration;
}

1. apparentReceivedAgeRepresents the time difference between the client receiving the response and the server sending the response.

seredDataDateIt is the time corresponding to the response header obtained from the cache (the time when the server issued this response);
receivedResponseMillisit is the time when the client issued the request corresponding to this response.

2. receivedAgeIt represents the client’s cache and how long it has existed when received.

ageSecondsIt is the number of seconds corresponding to the response header obtained from the cache Age(the locally cached response is returned by the server's cache, and the cache exists on the server)

ageSecondsThe maximum value calculated from the previous step is apparentReceivedAgehow long the response data has existed when the response was received.

Assume that when we make a request, the server has a cache, which Data: 0点.
At this time, the client initiates a request in 1 hour. At this time, the server inserts it into the cache Age: 1小时and returns it to the client. At this time, the client calculates receivedAge1 hour, which represents how long the client's cache has existed when it was received. . (It does not represent how long it has existed at the time of this request)

3. responseDurationIt is the cache corresponding request, the time difference between sending the request and receiving the request.

4. residentDurationIt is the time difference between the time this cache was received and now.

receivedAge + responseDuration + residentDurationWhat it means is:

The time the cache already exists when the client receives it + the time spent in the request process + the time between this request and the cache acquisition is how long the cache really exists.

6.2. Cache freshness (valid time): freshMillis

long freshMillis = computeFreshnessLifetime();

private long computeFreshnessLifetime() {
    
    
	CacheControl responseCaching = cacheResponse.cacheControl();
            
	if (responseCaching.maxAgeSeconds() != -1) {
    
    
		return SECONDS.toMillis(responseCaching.maxAgeSeconds());
	} else if (expires != null) {
    
    
		long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
		long delta = expires.getTime() - servedMillis;
		return delta > 0 ? delta : 0;
	} else if (lastModified != null && cacheResponse.request().url().query() == null) {
    
    
		// As recommended by the HTTP RFC and implemented in Firefox, the
		// max age of a document should be defaulted to 10% of the
		// document's age at the time it was served. Default expiration
		// dates aren't used for URIs containing a query.
		long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
		long delta = servedMillis - lastModified.getTime();
		return delta > 0 ? (delta / 10) : 0;
	}
	return 0;
}

There are several situations in which the cache freshness (validity period) is determined, which are arranged in order of priority as follows:

1. The cached response contains Cache-Control: max-age=[秒] the maximum validity time of the resource.

2. If the cached response contains Expires: 时间, then Datethe resource validity time is calculated by or receiving the response time.

3. If the cached response contains Last-Modified: 时间, Datethe resource validity time is calculated by or the time when the response is sent corresponding to the request; and according to the recommendations and the implementation in the Firefox browser, 10% of the result is used as the resource validity time.

6.3. Minimum cache freshness: minFreshMillis

long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
    
    
	minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}

If included in the user's request header Cache-Control: min-fresh=[秒], it represents how long the user thinks the cache is valid. Assume that the cache freshness itself is: 100 milliseconds, and the minimum cache freshness is: 10 milliseconds, then the actual cache validity time is 90 milliseconds.

6.4. How long the cache remains valid after expiration: maxStaleMillis

long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    
    
	maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

The first condition in this judgment is that the cached response does not contain Cache-Control: must-revalidate(expired resources are not available), and the user's request header contains Cache-Control: max-stale=[秒]the length of time that is still valid after the cache expires.

6.5. Determine whether the cache is valid

if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    
    
	Response.Builder builder = cacheResponse.newBuilder();
	//todo 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
	if (ageMillis + minFreshMillis >= freshMillis) {
    
    
		builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
	}
	//todo 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
	long oneDayMillis = 24 * 60 * 60 * 1000L;
	if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
    
    
		builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
	}
	return new CacheStrategy(null, builder.build());
}

Finally, use the value generated in the previous 4 steps, ignoring the cache as long as the cached response is not specified no-cache, if:

Cache survival time + minimum cache freshness < cache freshness + duration of continued use after expiration means that the cache can be used.

Assume that the cache has survived until now: 100 milliseconds;
the user believes that the cache validity time (minimum cache freshness) is: 10 milliseconds;
the cache freshness is: 100 milliseconds; the
cache can still be used after expiration: 0 milliseconds;
under these conditions, cache first The real effective time is: 90 milliseconds, and the cache has passed this time, so the cache cannot be used.

The inequality can be converted into: cache survival time < cache freshness - minimum cache freshness + continued use time after expiration, that is,
cache survival time < cache validity time + continued use time after expiration

In general, use caching as long as it is not ignored and the cache has not expired.

7. Cache expiration processing
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);

If the execution continues, it means that the cache has expired and cannot be used. At this time, we determine that if the cached response exists Etag, it will be If-None-Matchhanded to the server for verification; if it exists Last-Modifiedor Data, then it will be If-Modified-Sincehanded to the server for verification. If the server has no modifications, it will return 304. At this time, please note:

Since it is a request initiated due to cache expiration (different from the fourth active setting to determine the user), if the server returns 304, the framework will automatically update the cache, so this time it CacheStrategyincludes networkRequestbothcacheResponse

7. Closing

At this point, the cache judgment is over. The interceptor only needs to judge different combinations CacheStrategyof networkRequestand cacheResponseto determine whether the cache is allowed to be used.

However, it should be noted that if the user configures the request when creating the request, onlyIfCachedit means that the user wants this request to be obtained only from the cache this time and does not need to initiate a request. Then if the generated CacheStrategyexists networkRequest, it means that the request will definitely be initiated, and a conflict will occur at this time! That will directly give the interceptor an object that neither has networkRequestnor has cacheResponse. The interceptor returns directly to the user 504!

//缓存策略 get 方法
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    
    
	// We're forbidden from using the network and the cache is insufficient.
	return new CacheStrategy(null, 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();
}
8. Summary

1. If null is obtained from the cache Response, you need to use a network request to obtain the response;
2. If it is an HTTPS request, but the handshake information is lost, the cache cannot be used and a network request is required;
3. If the response code is determined If it cannot be cached and the response header has no-storean identifier, then a network request is required;
4. If the request header has no-cachean identifier or is present If-Modified-Since/If-None-Match, then a network request is required;
5. If the response header has no no-cacheidentifier and the cache time does not exceed the limit time, then it can be used Caching, no network request is required;
6. If the cache expires, determine whether the response header is set Etag/Last-Modified/Date, if not, use the network request directly, otherwise you need to consider the server returning 304;

Moreover, as long as a network request is required, the request header cannot be included only-if-cached, otherwise the framework will directly return 504!

The main logic of the cache interceptor itself is actually in the cache strategy. The logic of the interceptor itself is very simple. If it is determined that a network request needs to be initiated, the next interceptor isConnectInterceptor

4. Connection interceptor

ConnectInterceptor, opens a connection to the target server, and executes the next interceptor. It is so short that you can post it here in its entirety:

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.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
}

Although the amount of code is very small, most of the functions are actually encapsulated into other classes, and they are just called here.

First of all, the object we see StreamAllocationis created in the first interceptor: the redirection interceptor, but it is actually used here.

"When a request is issued, a connection needs to be established. After the connection is established, a stream needs to be used to read and write data." This StreamAllocation is to coordinate the relationship between the request, the connection and the data flow. It is responsible for finding a connection for a request, and then obtaining Streams are used to implement network communication.

The method used here newStreamis actually to find or establish a valid connection with the requesting host. The return HttpCodeccontains the input and output streams, and encapsulates the encoding and decoding of the HTTP request message. It can be used directly to complete the connection with the requesting host. HTTP communication.

StreamAllocationTo put it simply, it is to maintain the connection: RealConnection- Encapsulates Socket and a Socket connection pool. Reusability RealConnectionneeds:

public boolean isEligible(Address address, @Nullable Route route) {
    
    
    // If this connection is not accepting new streams, we're done.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // If the non-host fields of the address don't overlap, we're done.
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // If the host exactly matches, we're done: this connection can carry the address.
    if (address.url().host().equals(this.route().address().url().host())) {
    
    
      return true; // This connection is a perfect match.
    }

    // At this point we don't have a hostname match. But we still be able to carry the request if
    // our connection coalescing requirements are met. See also:
    // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
    // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/

    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;

    // 2. The routes must share an IP address. This requires us to have a DNS address for both
    // hosts, which only happens after route planning. We can't coalesce connections that use a
    // proxy, since proxies don't tell us the origin server's IP address.
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;

    // 3. This connection's server certificate's must cover the new host.
    if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;

    // 4. Certificate pinning must match the host.
    try {
    
    
      address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
    
    
      return false;
    }

    return true; // The caller's address can be carried by this connection.
  }

1、 if (allocations.size() >= allocationLimit || noNewStreams) return false;

​ The connection reaches the maximum concurrent stream or the connection does not allow the establishment of new streams; for example, the connection being used by http1.x cannot be used by others (the maximum concurrent stream is: 1) or the connection is closed; then reuse is not allowed;

2、

if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
if (address.url().host().equals(this.route().address().url().host())) {
    
    
      return true; // This connection is a perfect match.
}

DNS, proxy, SSL certificate, server domain name, and port can be reused if they are identical;

If none of the above conditions are met, it may still be reusable in some scenarios of HTTP/2 (Ignore http2 for now).

So in summary, if you find a connection in the connection pool that has consistent connection parameters and has not been closed or occupied, it can be reused.

Summarize

All implementations in this interceptor are to obtain a connection to the target server and send and receive HTTP data on this connection.

5. Request server interceptor

CallServerInterceptor, making HttpCodeca request to the server and parsing it to generate it Response.

First call httpCodec.writeRequestHeaders(request);writes the request header to the cache (it flushRequest()is not actually sent to the server until the call is made). Then immediately make the first logical judgment

Response.Builder responseBuilder = null;
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.
		realChain.eventListener().requestBodyStart(realChain.call());
		long contentLength = request.body().contentLength();
		CountingSink requestBodyOut =
                        new CountingSink(httpCodec.createRequestBody(request, contentLength));
		BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

		request.body().writeTo(bufferedRequestBody);
		bufferedRequestBody.close();
		realChain.eventListener().requestBodyEnd(realChain.call(),requestBodyOut.successfulCount);
	} else if (!connection.isMultiplexed()) {
    
     
        //HTTP2多路复用,不需要关闭socket,不管!
		// 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();
	}
}
httpCodec.finishRequest();

The entire if is related to a request header: Expect: 100-continue. This request header represents the need to determine with the server whether it is willing to accept the request body sent by the client before sending the request body. Therefore, permitsRequestBodyit is judged whether the request body will be carried (POST). If the if is hit, a query will be initiated to the server to see if it is willing to receive the request body. At this time, if the server is willing, it will respond with 100 (there is no response body, and the responseBuilder is nul). . Only then can the remaining request data be sent.

But if the server does not agree to accept the request body, then we need to mark the connection and call it to noNewStreams()close the related Socket.

The subsequent code is:

if (responseBuilder == null) {
    
    
	realChain.eventListener().responseHeadersStart(realChain.call());
	responseBuilder = httpCodec.readResponseHeaders(false);
}

Response response = responseBuilder
                .request(request)
                .handshake(streamAllocation.connection().handshake())
                .sentRequestAtMillis(sentRequestMillis)
                .receivedResponseAtMillis(System.currentTimeMillis())
                .build();

The situation at this time responseBuilderis:

1. POST request, the request header contains Expect, the server is allowed to accept the request body, and the request body has been sent, responseBuilderwhich is null;

2. POST request, the request header contains Expect, the server is not allowed to accept the request body, responseBuildernot null

3. POST method request, not included Expect, directly send the request body, responseBuilderwhich is null;

4. POST request, no request body, responseBuilderis null;

5. GET request responseBuilderis null;

Corresponding to the above five situations, read the response header and form a response Response. Note: There Responseis no response body. At the same time, it should be noted that if the server accepts Expect: 100-continueit, does it mean that we initiated it twice Request? The response header at this time is the first time to query whether the server supports accepting the request body, rather than the result response corresponding to the actual request. So then:

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
	responseBuilder = httpCodec.readResponseHeaders(false);

	response = responseBuilder
                    .request(request)
                    .handshake(streamAllocation.connection().handshake())
                    .sentRequestAtMillis(sentRequestMillis)
                    .receivedResponseAtMillis(System.currentTimeMillis())
                    .build();

	code = response.code();
}

If the response is 100, it means that the request is Expect: 100-continuesuccessful and a response header needs to be read again immediately. This is the real response header corresponding to the request result.

then finish

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 {
    
    
	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());
}
return response;

forWebSocketRepresenting a websocket request, we go directly to else, where we read the response body data. Then determine whether both the request and the server want a long connection. Once one party indicates it close, it needs to be closed socket. If the server returns 204/205, generally speaking, these return codes will not exist, but once they appear, it means that there is no response body, but the parsed response header contains and is Content-Lenghtnot 0, which represents the data bytes of the response body. length. At this time, a conflict occurs and a protocol exception is thrown directly!

Summarize

In this interceptor, the encapsulation and parsing of HTTP protocol messages is completed.

OkHttpSummary

The entire OkHttp function is implemented in these five default interceptors, so understanding the working mechanism of the interceptor mode is a prerequisite. These five interceptors are: retry interceptor, bridge interceptor, cache interceptor, connection interceptor, and request service interceptor. Each interceptor is responsible for different tasks, just like a factory assembly line. After these five processes, the final product is completed.

But unlike the pipeline, every time the interceptor in OkHttp initiates a request, it will do something before handing it to the next interceptor, and do something again after getting the result. The entire process is sequential in the request direction, and in reverse order in the response direction.

When the user initiates a request, the task dispatcher will Dispatcherpackage the request and hand it over to the retry interceptor for processing.

1. Before the retry interceptor is handed over (to the next interceptor), it is responsible for determining whether the user has canceled the request; after obtaining the result, it will determine whether redirection is needed based on the response code. If the conditions are met, it will restart. Execute all interceptors.

2. Before handing over, the bridge interceptor is responsible for adding the necessary request headers of the HTTP protocol (such as: Host) and adding some default behaviors (such as: GZIP compression); after obtaining the result, it calls the save cookie interface and Parse GZIP data.

3. As the name suggests, the cache interceptor reads and determines whether to use cache before handing it over; it determines whether to cache after obtaining the result.

4. Before handing over, the connection interceptor is responsible for finding or creating a new connection and obtaining the corresponding socket stream; no additional processing is performed after obtaining the result.

5. Request the server interceptor to actually communicate with the server, send data to the server, and parse the read response data.

After going through this series of processes, an HTTP request is completed!

Supplement: Agency

When using OkHttp, if the user is OkHttpClientconfigured with proxyor when creating proxySelector, the configured proxy will be used and proxyhas a higher priority proxySelector. If it is not configured, the agent configured on the machine will be obtained and used.

//JDK : ProxySelector
try {
    
    
	URI uri = new URI("http://restapi.amap.com");
	List<Proxy> proxyList = ProxySelector.getDefault().select(uri);
	System.out.println(proxyList.get(0).address());
	System.out.println(proxyList.get(0).type());
} catch (URISyntaxException e) {
    
    
	e.printStackTrace();
}

Therefore, if we do not need the request in our App to go through a proxy, we can configure one proxy(Proxy.NO_PROXY)to avoid packet capture. NO_PROXYis defined as follows:

public static final Proxy NO_PROXY = new Proxy();
private Proxy() {
    
    
	this.type = Proxy.Type.DIRECT;
	this.sa = null;
}

There are three types of abstract classes corresponding to agents in Java:

public static enum Type {
    
    
        DIRECT,
        HTTP,
        SOCKS;
	private Type() {
    
    
	}
}

DIRECT:No proxy, HTTP:http proxy, SOCKS:socks proxy. Needless to say, the first one goes without saying, but what is the difference between Http proxy and Socks proxy?

For Socks proxy, in the HTTP scenario, the proxy server completes the forwarding of TCP data packets;
while the HTTP proxy server, in addition to forwarding data, will also parse HTTP requests and responses, and do some processing based on the content of the requests and responses. .

RealConnectionMethods connectSocket:

//如果是Socks代理则 new Socket(proxy); 否则无代理或http代理就address.socketFactory().createSocket(),相当于直接:new Socket()
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
                ? address.socketFactory().createSocket()
                : new Socket(proxy);
//connect方法
socket.connect(address);

When a SOCKS proxy is set up, when creating a Socket, pass in proxy for it. When writing code, the HTTP server is still used as the target address when connecting (in fact, the Socket must be connected to the SOCKS proxy server); but if an Http proxy is set , the Socket created is to establish a connection with the Http proxy server.

connectThe methods passed in method time addressare from the following inetSocketAddresses
RouteSelectorcollection resetNextInetSocketAddress:

private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    
    
    // ......
    if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
    
    
        //无代理和socks代理,使用http服务器域名与端口
      socketHost = address.url().host();
      socketPort = address.url().port();
    } else {
    
    
      SocketAddress proxyAddress = proxy.address();
      if (!(proxyAddress instanceof InetSocketAddress)) {
    
    
        throw new IllegalArgumentException(
            "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
      }
      InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
      socketHost = getHostString(proxySocketAddress);
      socketPort = proxySocketAddress.getPort();
    }

    // ......

    if (proxy.type() == Proxy.Type.SOCKS) {
    
    
        //socks代理 connect http服务器 (DNS没用,由代理服务器解析域名)
      inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
    
    
        //无代理,dns解析http服务器
        //http代理,dns解析http代理服务器
      List<InetAddress> addresses = address.dns().lookup(socketHost);
      //......
      for (int i = 0, size = addresses.size(); i < size; i++) {
    
    
        InetAddress inetAddress = addresses.get(i);
        inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
      }
    }
}

When setting up a proxy, the domain name resolution of the HTTP server will be handed over to the proxy server. However, if an Http proxy is set up, OkhttpClientthe configured dns resolution proxy server will be used for the domain name of the Http proxy server, and the domain name resolution of the Http server will be handed over to the proxy server for resolution.

The above code is the use of proxy and DNS in OkHttp, but there is one more thing to note. Http proxies are also divided into two types: ordinary proxies and tunnel proxies.

Among them, ordinary agents do not require additional operations and play the role of "middleman" to pass messages back and forth between the two ends. When this "middleman" receives the request message sent by the client, it needs to correctly process the request and connection status, and at the same time send a new request to the server. After receiving the response, it will package the response result into a response body and return it to the client. end. In the ordinary agent process, both ends of the agent may not be aware of the existence of the "middleman".

However, the tunnel proxy no longer acts as a middleman and cannot rewrite the client's request. Instead, it only brainlessly forwards the client's request to the terminal server through the established tunnel after the connection is established. The tunnel proxy needs to initiate an Http CONNECT request. This request method has no request body and is only used by the proxy server and will not be passed to the terminal server. Once the request header part ends, all subsequent data are regarded as data that should be forwarded to the terminal server, and the proxy needs to forward them directly without thinking until the TCP read channel from the client is closed. The CONNECT response message, after the proxy server and the terminal server establish a connection, can return a 200 Connect establishedstatus code to the client to indicate that the connection with the terminal server is successfully established.

RealConnection's connect method

if (route.requiresTunnel()) {
    
             
	connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
	if (rawSocket == null) {
    
    
		// We were unable to connect the tunnel but properly closed down our
		// resources.
		break;
	}
} else {
    
    
	connectSocket(connectTimeout, readTimeout, call, eventListener);
}

requiresTunnelThe judgment of the method is: the current request is https and there is an http proxy. connectTunnelAt this time, it will be initiated:

CONNECT xxxx HTTP/1.1
Host: xxxx
Proxy-Connection: Keep-Alive
User-Agent: okhttp/${version}

For a request, the proxy server will return 200 if the connection is successful; if 407 is returned, it means that the proxy server requires authentication (such as a paid proxy). In this case, you need to add the following to the request header Proxy-Authorization:

 Authenticator authenticator = new Authenticator() {
    
    
        @Nullable
        @Override
        public Request authenticate(Route route, Response response) throws IOException {
    
    
          if(response.code == 407){
    
    
            //代理鉴权
            String credential = Credentials.basic("代理服务用户名", "代理服务密码");
            return response.request().newBuilder()
                    .header("Proxy-Authorization", credential)
                    .build();
          }
          return null;
        }
      };
new OkHttpClient.Builder().proxyAuthenticator(authenticator);

at last

I have compiled a collection of Android interview questions. In addition to the above interview questions, it also includes [ Java basics, collections, multi-threading, virtual machines, reflection, generics, concurrent programming, Android's four major components, asynchronous tasks and message mechanisms, UI drawing , performance tuning, SDN, third-party framework, design pattern, Kotlin, computer network, system startup process, Dart, Flutter, algorithm and data structure, NDK, H.264, H.265. Audio codec, FFmpeg, OpenMax, OpenCV, OpenGL ES ]
Insert image description here

Friends in need can scan the QR code below to receive all interview questions + answer analysis for free! ! !

Guess you like

Origin blog.csdn.net/datian1234/article/details/135256999
Recommended