【Androidオープンソースフレームワーク面接の質問】OkHttpフレームワークの原理について語る_後編

本書は上記記事「【Androidオープンソースフレームワーク面接の質問】OkHttpフレームワークの原理を語る_前編」の続きです。

インターセプターの詳細
1. インターセプターの再試行とリダイレクト

最初のインターセプタ: はRetryAndFollowUpInterceptor、主に再試行とリダイレクトという 2 つのことを実行します。

リトライ

リクエストフェーズ中に RouteException または IOException が発生した場合、リクエストを再開始するかどうかが判断されます。

ルート例外

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

IO例外

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;
} 

recoverどちらの例外もリトライの可否を判定するメソッドに基づいており、 が返された場合はtrueリトライが許可されていることを意味します。

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;
}

したがって、まず第一に、ユーザーが再試行を禁止していない場合、何らかの例外が発生し、さらに多くのルーティング行がある場合、ユーザーはリクエストを再試行するために行を変更しようとします。これらの例外の一部は次の方法で判断されますisRecoverable

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.プロトコルが異常である場合は、そのままリトライ不可と判断します(リクエストもしくはサーバーのレスポンス自体に問題がある。データがhttpプロトコルに従って定義されていないため、再試行しても無駄)もう一度やり直してください)

2.タイムアウト例外: ネットワークの変動によりソケット接続がタイムアウトした可能性があります。別のルートを使用して再試行できます。

3. SSL 証明書例外/SSL 検証失敗例外前者は証明書の検証が失敗したことを意味し、後者は証明書が存在しないか、証明書データが間違っていることを意味します。

異常判定後、リトライが許可されている場合は、現在接続可能なルーティング経路があるかどうかを再度確認します。簡単に言うと、たとえば、DNS はドメイン名を解決した後に複数の IP を返すことがありますが、1 つの IP が失敗した後は、別の IP を試して再試行します。

リダイレクト

リクエスト完了後に例外が発生しない場合は、現在取得しているレスポンスが最終的にユーザーに渡すべきレスポンスであるとは限らず、リダイレクトが必要かどうかはさらに判断が必要です。リダイレクトの判断はfollowUpRequestメソッド内にあります

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;
    }
}

リダイレクトが必要かどうかを判断するためのコンテンツはたくさんあり、それを覚えるのは難しいのが普通ですが、重要なのはその意味を理解することです。このメソッドが空を返す場合は、リダイレクトする必要がなく、応答が直接返されることを意味しますが、空でない場合は、返されたリクエストを再リクエストする必要がありますが、最大数はRequest制限されることに注意してください。followupインターセプターで20回を定義します

要約する

このインターセプターは、責任チェーン全体の最初のものであり、つまり、最初のコンタクトでありRequest、最後に受け取ったResponseロールになります。このインターセプターの主な機能は、再試行とリダイレクトが必要かどうかを判断することです。

再試行の前提条件は、RouteExceptionまたはであることですIOExceptionインターセプタの後続の実行中にこれら 2 つの例外が発生すると、recover接続を再試行するかどうかを決定するメソッドが使用されます。

リダイレクションはリトライ判定後に発生するため、リトライ条件を満たさない場合は、さらにそれに基づくレスポンスコードを呼び出す必要があります(もちろんダイレクトリクエストが失敗した場合、存在しない場合はfollowUpRequest例外がスローされます)。最大20回発生します。ResponseResponsefollowup

2. ブリッジインターセプター

BridgeInterceptor、アプリケーションとサーバーを接続するブリッジです。当社が行うリクエストは、リクエスト内容の長さ、エンコード、gzip 圧縮、Cookie などの設定、応答取得後の Cookie の保存など、サーバーに送信される前に処理されます。このインターセプターは比較的単純です。

完了リクエストヘッダー:

リクエストヘッダー 説明する
Content-Type リクエストの本文タイプは次のとおりです。application/x-www-form-urlencoded
Content-Length/Transfer-Encoding リクエストボディの解析方法
Host 要求されたホスト サイト
Connection: Keep-Alive 長い接続を維持する
Accept-Encoding: gzip 応答の受け入れは gzip 圧縮をサポートします
Cookie クッキーの識別
User-Agent オペレーティング システム、ブラウザなどの要求されたユーザー情報。

リクエスト ヘッダーが完了すると、次のインターセプターに渡されて処理され、レスポンスを取得した後、主に次の 2 つのことを行います。

1. Cookie を保存します。次のリクエストでは、対応するデータ設定が読み取られ、リクエスト ヘッダーに入力されます。デフォルトのCookieJar実装は提供されていません。

2. gzip によって返されたデータを使用する場合は、GzipSource解析を容易にするためにパッケージ化を使用します。

要約する

ブリッジインターセプタの実行ロジックは主に以下の点です

ユーザが作成した関連ヘッダ情報を追加または削除してRequest、実際にネットワークリクエストを実行できるリクエストに変換し、Request
ネットワークリクエスト仕様に準拠したリクエストを次のインターセプタに渡して処理し、Response
レスポンスボディを取得します。 GZIP 圧縮されている場合は、それを解凍し、ユーザーが使用できるものに構築してResponse返す必要があります

3. キャッシュインターセプタ

CacheInterceptor、リクエストを行う前に、キャッシュがヒットしたかどうかを確認します。ヒットした場合は、キャッシュされた応答をリクエストせずに直接使用できます。(Getリクエストのキャッシュのみが存在します)

手順は次のとおりです。

1. リクエストに対応するレスポンスキャッシュをキャッシュから取得する

2. を作成しますCacheStrategy作成時にキャッシュが使用可能かどうかを判断しますCacheStrategyの2つのメンバーがありますそれらの組み合わせは次のとおりです。networkRequestcacheResponse

ネットワークリクエスト キャッシュ応答 説明する
ヌル Null ではありません キャッシュを直接使用する
Null ではありません ヌル サーバーにリクエストを送信する
ヌル ヌル 直接 gg、okhttp は直接 504 を返します
Null ではありません Null ではありません リクエストを開始します。レスポンスが 304 (変更なし) の場合は、キャッシュされたレスポンスを更新して返します。

3. 次の責任チェーンに引き渡して、処理を続行します。

4. 以降の作業では、304 が返された場合はキャッシュされた応答を使用し、それ以外の場合はネットワーク応答を使用してこの応答をキャッシュします (Get リクエストに対する応答のみがキャッシュされます)。

キャッシュ インターセプターの作業は比較的単純ですが、特定の実装には多くの処理が必要です。キャッシュが使用可能かどうか、またはサーバーへの要求はキャッシュインターセプタで判断されますCacheStrategy

キャッシュ戦略

CacheStrategyまず、いくつかのリクエスト ヘッダーとレスポンス ヘッダーを理解する必要があります。

レスポンスヘッダー 説明する
日付 メッセージが送信された時刻 日付: 2028 年 11 月 18 日土曜日 06:17:41 GMT
有効期限が切れます リソースの有効期限 有効期限: 2028 年 11 月 18 日(土) 06:17:41 GMT
最終更新日 リソースの最終変更時刻 最終更新日: 2016 年 7 月 22 日金曜日 02:57:17 GMT
Eタグ サーバー上のリソースの一意の識別子 Eタグ: 「16df0-5383097a03d40」
サーバーはリクエストにキャッシュを返します。キャッシュが作成されてからの経過時間 (秒) 年齢: 3825683
キャッシュ制御 - -
リクエストヘッダー 説明する
If-Modified-Since サーバーは、指定された時間が経過してもリクエストに対応するリソースを変更せず、304 (変更なし) を返します。 変更日: 金曜日、2016 年 7 月 22 日 02:57:17 GMT
If-None-Match サーバーはそれを、要求された対応するリソースの値と比較しEtag、一致する場合は 304 を返します。 一致しない場合: 「16df0-5383097a03d40」
Cache-Control - -

これはCache-Controlリクエスト ヘッダーまたはレスポンス ヘッダーに存在することができ、対応する値は複数の組み合わせで設定できます。

  1. max-age=[秒]: リソースの最大有効時間。
  2. public: リソースをキャッシュできるクライアント、プロキシ サーバーなど、任意のユーザーがリソースをキャッシュできることを示します。
  3. private: リソースを 1 人のユーザーのみがキャッシュできることを示します。デフォルトはプライベートです。
  4. no-store: リソースのキャッシュは許可されていません
  5. no-cache(お願い)キャッシングは使用しないでください
  6. immutable(応答) リソースは変更されません。
  7. min-fresh=[秒](リクエスト) キャッシュの最小鮮度 (ユーザーがこのキャッシュが有効であるとみなす期間)
  8. must-revalidate(応答) 有効期限キャッシュは許可されていません
  9. max-stale=[秒](要望)キャッシュの有効期限が切れた後、どれくらいの期間有効ですか?

max-age=100 および min-fresh=20 があると仮定します。これは、サーバーが応答を作成してからキャッシュできるようになるまで、キャッシュされた応答に 100-20=80 秒かかるとユーザーが考えることを意味します。ただし、max-stale=100 の場合。これは、キャッシュ有効期間の 80 秒が経過した後も、キャッシュ有効期間が 180 秒であるとみなせる 100 秒間は使用できることを意味します。

ここに画像の説明を挿入します

詳細なプロセス

このリクエストに対応する URL がキャッシュから取得された場合Response、上記のデータは最初にレスポンスから取得され、後で使用されます。

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()方法

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;
}

このメソッドはgetCandidate()メソッド内で呼び出され、実キャッシュの判定を完了します。

1. キャッシュは存在しますか?

メソッド全体における最初の判断は、キャッシュが存在するかどうかです。

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

cacheResponseキャッシュ内で見つかった応答です。null の場合は、対応するキャッシュが見つからず、作成されたCacheStrategyインスタンス オブジェクトのみが存在することを意味しnetworkRequest、ネットワーク リクエストを開始する必要があることを意味します。

2. httpsリクエストのキャッシュ

さらに下に進むと、cacheResponse存在する必要がありますが、使用できない可能性があることを意味します。その後、有効性について一連の判断が行われる

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

このリクエストが HTTPS であるにもかかわらず、キャッシュ内に対応するハンドシェイク情報がない場合、キャッシュは無効です。

3. レスポンスコードとレスポンスヘッダ
if (!isCacheable(cacheResponse, request)) {
    
    
	return new CacheStrategy(request, null);
}

ロジック全体が含まれておりisCacheable、その内容は次のとおりです。

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

キャッシュされたレスポンス内のレスポンスコードが200、203、204、300、301、404、405、410、414、501、308の場合は、サーバーから与えられたCache-Control: no-store(リソースがキャッシュできない)かどうかのみ判断されるため、サーバーがこの応答ヘッダーに関して言えば、前の 2 つの判断と一致しています (キャッシュは使用できません)。それ以外の場合は、キャッシュが利用可能かどうかをさらに判断し続けます。

また、応答コードが 302/307 (リダイレクト) の場合は、キャッシュを許可する応答ヘッダーが存在するかどうかをさらに判断する必要があります。注釈に示されている文書 http://tools.ietf.org/html/rfc7234#section-3 の説明によると、存在するかExpiresCache-Control値が次の場合:

  1. max-age=[秒]: リソースの最大有効時間。

  2. public: リソースをキャッシュできるクライアント、プロキシ サーバーなど、任意のユーザーがリソースをキャッシュできることを示します。

  3. private: リソースを 1 人のユーザーのみがキャッシュできることを示します。デフォルトはプライベートです。

同時に存在しない場合はCache-Control: no-store、キャッシュが利用可能かどうかをさらに確認できます。

したがって、総合的に優先順位は次のように決定されます。

1.応答コードが 200、203、204、300、301、404、405、410、414、501、308、302、307 ではない。キャッシュ利用できない。

2. 応答コードが 302 または 307 で、一部の応答ヘッダーが含まれていない場合、キャッシュは使用できません。

3. 応答ヘッダーがある場合Cache-Control: no-store、キャッシュは使用できません。

応答キャッシュが利用可能な場合は、キャッシュの有効性をさらに判断します。

4. ユーザーリクエストの設定
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;
}

この時点で、OkHttp はまずユーザーによって開始されたリクエストを判断する必要がありますRequest。ユーザーがCache-Control: no-cacheリクエスト ヘッダーを指定した場合 (キャッシュを使用しない)、またはリクエスト ヘッダーにIf-Modified-SinceまたはIf-None-Match(リクエストの検証) が含まれている場合、キャッシュは許可されません。

リクエストヘッダー 説明する
Cache-Control: no-cache キャッシュを無視する
If-Modified-Since: 时间 値は通常Dataまたは でlastModified、指定された時間が経過してもサーバーがリクエストに対応するリソースを変更しない場合は、304 (変更なし) を返します。
If-None-Match:标记 通常、値は です。要求された対応するリソースの値Etagと比較し、一致する場合は 304 を返します。Etag

これは、ユーザー リクエスト ヘッダーにこれらの内容が含まれている場合、サーバーに対してリクエストを行う必要があることを意味します。ただし、OkHttp は 304 応答をキャッシュしないことに注意してください。この場合、つまり、ユーザーがサーバーに対して積極的にリクエストを開始し、サーバーが 304 (応答本文なし) を返した場合、304 応答は「あなたがリクエストしたので、このリクエストの結果のみをお知らせします。 」これらのリクエスト ヘッダーが含まれていない場合は、引き続きキャッシュの有効性を判断します。

5. リソースが変更されないかどうか
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
    
    
	return new CacheStrategy(null, cacheResponse);
}

キャッシュされた応答に含まれている場合Cache-Control: immutable、対応するリクエストの応答コンテンツは決して変更されないことを意味します。この時点で、キャッシュを直接使用できます。それ以外の場合は、キャッシュが使用可能かどうかの判断を続けます。

6. レスポンスキャッシュの有効期間

このステップでは、キャッシュ応答内の何らかの情報に基づいて、キャッシュが有効期間内であるかどうかをさらに判断します。満足した場合:

キャッシュの生存時間 < キャッシュの鮮度 - 最小のキャッシュの鮮度 + 有効期限切れ後の継続使用期間

デリゲートはキャッシュを使用できます。鮮度は有効時間として理解でき、ここでの「キャッシュ鮮度 - キャッシュ最小鮮度」がキャッシュの実際の有効時間を表します。

// 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. 現在までキャッシュが存続していた時間: ageMillis

まず、cacheResponseAge()メソッドは応答を取得し、応答が存在するおおよその時間を取得します。

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.apparentReceivedAgeクライアントが応答を受信して​​からサーバーが応答を送信するまでの時間差を表します。

seredDataキャッシュから取得したDateレスポンスヘッダに対応する時刻(サーバがこのレスポンスを発行した時刻)であり、
receivedResponseMillisクライアントがこのレスポンスに対応するリクエストを発行した時刻です。

2.receivedAgeクライアントのキャッシュと、それが受信されたときに存在していた期間を表します。

ageSecondsキャッシュから取得した応答ヘッダーに対応する秒数ですAge(ローカルにキャッシュされた応答はサーバーのキャッシュによって返され、キャッシュはサーバー上に存在します)。

ageSeconds前のステップで計算された最大値はapparentReceivedAge、応答を受信したときに応答データが存在していた期間です。

リクエストを行うときに、サーバーにキャッシュがあると仮定しますData: 0点
このとき、クライアントは 1 時間以内にリクエストを開始します。このとき、サーバーはそれをキャッシュに挿入してクライアントに返します。このとき、クライアントは、クライアントのキャッシュが存在していた時間を表す 1 時間をAge: 1小时計算します。receivedAge受け取ったときです。(このリクエストの時点で存在していた期間を表すものではありません)

3.responseDurationキャッシュ対応リクエスト、リクエスト送信とリクエスト受信までの時間差です。

4.residentDurationこのキャッシュを受信した時刻と現在との時間差です。

receivedAge + responseDuration + residentDurationそれが意味するのは次のとおりです。

クライアントがキャッシュを受信したときにキャッシュがすでに存在していた時間 + リクエストの処理に費やした時間 + このリクエストとキャッシュの取得の間の時間を足したものが、実際にキャッシュが存在する時間となります。

6.2. キャッシュの鮮度 (有効時間): 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;
}

キャッシュの鮮度 (有効期間) が決定される状況はいくつかありますが、それらを優先順位の順に並べると次のようになります。

1. キャッシュされた応答には、Cache-Control: max-age=[秒] リソースの最大有効時間が含まれます。

2. キャッシュされた応答に が含まれている場合Expires: 时间Dateリソース有効時間は応答時間によって計算されるか、応答時間を受信して​​計算されます。

3. キャッシュされた応答に が含まれている場合Last-Modified: 时间Dateリソース有効時間は、要求に対応する応答が送信された時刻によって計算され、推奨事項と Firefox ブラウザーの実装に従って、結果の 10% が要求として使用されます。リソースの有効期限。

6.3. キャッシュの最小鮮度: minFreshMillis

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

ユーザーのリクエスト ヘッダーに含まれている場合Cache-Control: min-fresh=[秒]、ユーザーがキャッシュが有効であると考える期間を表します。キャッシュの鮮度自体が 100 ミリ秒、最小キャッシュの鮮度が 10 ミリ秒であると仮定すると、実際のキャッシュの有効時間は 90 ミリ秒になります。

6.4. キャッシュの有効期限が切れた後の有効期間: maxStaleMillis

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

この判定の最初の条件は、キャッシュされた応答に期限切れのリソースが含まれていないことCache-Control: must-revalidate(期限切れのリソースが利用できないこと)、およびユーザーのリクエスト ヘッダーにCache-Control: max-stale=[秒]キャッシュの期限切れ後も有効な時間が含まれていることです。

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

最後に、次の場合には、キャッシュされた応答が指定されていない限り、キャッシュを無視して、前の 4 つの手順で生成された値を使用しますno-cache

キャッシュ生存時間 + 最小キャッシュ鮮度 < キャッシュ鮮度 + 有効期限後の継続使用期間は、キャッシュが使用できることを意味します。

キャッシュが現在まで存続していると仮定します: 100 ミリ秒、
ユーザーがキャッシュの有効期間 (最小キャッシュ鮮度) を信じている: 10 ミリ秒、
キャッシュの鮮度: 100 ミリ秒、
キャッシュは期限切れ後も使用できる: 0 ミリ秒、キャッシュの有効期限: 0 ミリ秒。
この条件では、キャッシュが最初に実行されます。 実際の有効時間は 90 ミリ秒で、今回はキャッシュが経過しているため、キャッシュは使用できません。

この不等式は次のように変換できます: キャッシュ生存時間 < キャッシュ鮮度 - 最小キャッシュ鮮度 + 期限切れ後の継続使用時間、つまり、
キャッシュ生存時間 < キャッシュ有効時間 + 期限切れ後の継続使用時間

一般に、キャッシュが無視されず、キャッシュの有効期限が切れていない限り、キャッシュを使用します。

7. キャッシュ有効期限処理
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);

実行が継続する場合は、キャッシュの有効期限が切れており、使用できないことを意味します。このとき、キャッシュされた応答が存在する場合は検証のためにサーバーに渡され、存在する場合、または が存在する場合Etag検証のためにサーバーに渡されると判断します。サーバーに何も変更がない場合、304 が返されます。このとき、次の点に注意してください。If-None-MatchLast-ModifiedDataIf-Modified-Since

これはキャッシュの有効期限によって開始されたリクエストであるため (ユーザーを決定するための 4 番目のアクティブな設定とは異なります)、サーバーが 304 を返した場合、フレームワークは自動的にキャッシュを更新します。そのため、今回は両方がCacheStrategy含まれます。networkRequestcacheResponse

7. 終わりに

この時点でキャッシュの判定は終了しており、インターセプタは とCacheStrategyの組み合わせを判定しnetworkRequestcacheResponseキャッシュの使用を許可するかどうかを判断するだけです。

ただし、ユーザーがリクエストの作成時にリクエストを構成した場合、ユーザーは今回はこのリクエストをキャッシュからのみ取得することを希望し、リクエストを開始する必要がないことを意味することに注意してくださいonlyIfCached次に、生成された がCacheStrategy存在する場合networkRequest、リクエストが確実に開始されることを意味し、この時点で競合が発生します。networkRequestこれにより、インターセプタに hasも has も持たないオブジェクトが直接与えられますcacheResponseインターセプタはユーザーに直接戻ります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. まとめ

1. キャッシュから null が取得された場合はResponse、ネットワーク リクエストを使用して応答を取得する必要があります。2
. HTTPS リクエストであってもハンドシェイク情報が失われた場合、キャッシュは使用できず、ネットワーク リクエストが必要です。
3. 応答コードが決定されている場合、キャッシュできず、応答ヘッダーにno-store識別子がある場合は、ネットワーク リクエストが必要です。
4. リクエスト ヘッダーにno-cache識別子がある場合、または存在するIf-Modified-Since/If-None-Match場合、ネットワーク リクエストが必要です。
5.応答ヘッダーにno-cache識別子がなく、キャッシュ時間が制限時間を超えない場合は、キャッシュを使用できます。ネットワーク要求は必要ありません。6. キャッシュの有効期限が切れた場合は
、応答ヘッダーが設定されているかどうかを確認しEtag/Last-Modified/Date、設定されていない場合は、使用します。ネットワークリクエストを直接送信するか、そうでない場合はサーバーが 304 を返すことを考慮する必要があります。

さらに、ネットワークリクエストが必要である限り、リクエストヘッダーを含めることはできませんonly-if-cached。そうでない場合、フレームワークは直接 504! を返します。

キャッシュ インターセプタ自体のメイン ロジックは、実際にはキャッシュ ストラテジ内にあります。インターセプタ自体のロジックは非常に単純です。ネットワーク リクエストを開始する必要があると判断された場合、次のインターセプタはConnectInterceptor

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

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

コードの量は非常に少ないですが、ほとんどの関数は実際には他のクラスにカプセル化されており、ここで呼び出されるだけです。

まず第一に、私たちが見ているオブジェクトはStreamAllocation最初のインターセプター、つまりリダイレクト インターセプターで作成されていますが、実際にはここで使用されています。

「リクエストが発行されると、接続を確立する必要があります。接続が確立された後、データの読み書きにはストリームを使用する必要があります。」この StreamAllocation は、リクエスト、接続、データ フローの関係を調整するものです。リクエストの接続を見つけて、ネットワーク通信を実装するために使用されるストリームを取得する役割を果たします。

ここで使用されるメソッドはnewStream、実際には、要求元のホストとの有効な接続を検索または確立することです。戻り値にはHttpCodec入力ストリームと出力ストリームが含まれ、HTTP 要求メッセージのエンコードとデコードがカプセル化されています。これを直接使用して、ホストとの接続を完了することができます。要求元ホストのHTTP通信。

StreamAllocation簡単に言うと、接続を維持することです。RealConnection- ソケットとソケット接続プールをカプセル化します。再利用性のRealConnectionニーズ:

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;

接続が最大同時ストリームに達したか、接続で新しいストリームの確立が許可されません。たとえば、http1.x で使用されている接続を他のユーザーが使用できない (最大同時ストリームは 1)、または接続が閉じられています。 ; その場合、再利用は許可されません。

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、プロキシ、SSL 証明書、サーバーのドメイン名、およびポートは、同一であれば再利用できます。

上記の条件がいずれも満たされない場合でも、HTTP/2 の一部のシナリオでは再利用できる可能性があります (現時点では http2 を無視してください)。

要約すると、一貫した接続パラメータを持ち、閉じられたり占有されたりしていない接続が接続プール内で見つかった場合、その接続は再利用できます。

要約する

このインターセプターのすべての実装は、ターゲット サーバーへの接続を取得し、この接続上で HTTP データを送受信します。

5. リクエストサーバーインターセプター

CallServerInterceptorHttpCodecサーバーにリクエストを送信し、それを解析して生成しますResponse

最初の呼び出しでは、httpCodec.writeRequestHeaders(request);リクエスト ヘッダーがキャッシュに書き込まれます (flushRequest()呼び出しが行われるまで、リクエスト ヘッダーは実際にはサーバーに送信されません)。そしてすぐに最初の論理的判断を下します

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();

if 全体はリクエスト ヘッダーに関連しています: Expect: 100-continueこのリクエスト ヘッダーは、リクエスト本文を送信する前に、クライアントによって送信されたリクエスト本文を受け入れるかどうかをサーバーで決定する必要があることを表します。したがって、permitsRequestBodyリクエストボディを送信するかどうかを判断し(POST)、if にヒットすると、サーバーに対してリクエストボディを受信するかどうかのクエリが開始されます。喜んで、それは 100 で応答します (応答本文はなく、responseBuilder は null です)。その後のみ、残りのリクエスト データを送信できます。

ただし、サーバーがリクエスト本文の受け入れに同意しない場合は、接続をマークし、それを呼び出してnoNewStreams()関連するソケットを閉じる必要があります。

後続のコードは次のとおりです。

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();

この時の状況はresponseBuilder次のとおりです。

1. POST リクエスト。リクエスト ヘッダーには が含まれておりExpect、サーバーはリクエスト本文の受け入れを許可されており、リクエスト本文は送信されました。responseBuilderこれは null です。

2. POST リクエスト。リクエスト ヘッダーには が含まれますExpect。サーバーはリクエスト本文を受け入れることができません。nullresponseBuilderではありません。

3. POST メソッド リクエストは含まれておらず、 null であるExpectリクエスト本文を直接送信します。responseBuilder

4. リクエスト本文のない POST リクエストresponseBuilderは null です。

5. GET リクエストresponseBuilderが null です。

上記 5 つの状況に応じて、応答ヘッダーを読み取り、応答を作成しますResponse(注:Response応答本文はありません)。同時に、サーバーがExpect: 100-continueそれを受け入れた場合、それを 2 回開始したことになることに注意してくださいRequestこのときの応答ヘッダーは、実際のリクエストに対応する結果応答ではなく、サーバーがリクエスト本文の受け入れをサポートしているかどうかを初めて問い合わせます。それで:

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

レスポンスが 100 の場合、リクエストがExpect: 100-continue成功したことを意味し、リクエスト結果に対応する実際のレスポンス ヘッダーをすぐに再度読み取る必要があります。

それから終了

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;

forWebSocketWebSocket リクエストを表すと、直接 else に移動し、そこで応答本文のデータを読み取ります。次に、リクエストとサーバーの両方が長い接続を必要としているかどうかを判断し、一方がそれを指示したらclose、接続を閉じる必要がありますsocketサーバーが 204/205 を返した場合、一般的にこれらのリターン コードは存在しませんが、これらのリターン コードが表示されると、応答本文は存在しないが、解析された応答ヘッダーにはContent-Lenght0 ではなく、データ バイトを表す 0 が含まれていることを意味します。レスポンスボディの長さ。このとき、競合が発生し、プロトコル例外が直接スローされます。

要約する

このインターセプターでは、HTTP プロトコル メッセージのカプセル化と解析が完了します。

OKHttp概要

OkHttp 機能全体はこれら 5 つのデフォルト インターセプターに実装されているため、インターセプター モードの動作メカニズムを理解することが前提条件です。これら 5 つのインターセプターは、再試行インターセプター、ブリッジ インターセプター、キャッシュ インターセプター、接続インターセプター、および要求サービス インターセプターです。工場の組立ラインのように各インターセプターが担当し、5つの工程を経て最終製品が完成します。

ただし、パイプラインとは異なり、OkHttp のインターセプターはリクエストを開始するたびに、次のインターセプターに渡す前に何かを実行し、結果を取得した後に再度何かを実行します。プロセス全体は、要求方向では順次に行われ、応答方向では逆の順序で行われます。

ユーザーがリクエストを開始すると、タスク ディスパッチャはDispatcherリクエストをパッケージ化し、処理のために再試行インターセプタに渡します。

1. 再試行インターセプターが (次のインターセプターに) 引き渡される前に、ユーザーがリクエストをキャンセルしたかどうかを判断する責任があり、結果を取得した後、応答コードに基づいてリダイレクトが必要かどうかを判断します。すべてのインターセプターを実行します。

2. ハンドオーバーの前に、ブリッジ インターセプタは、HTTP プロトコルの必要なリクエスト ヘッダー (ホストなど) を追加し、いくつかのデフォルト動作 (GZIP 圧縮など) を追加する責任を負い、結果を取得した後、保存 Cookie を呼び出します。インターフェイスと GZIP データの解析。

3. 名前が示すように、キャッシュ インターセプターは、キャッシュを渡す前に読み取りを行って使用するかどうかを決定し、結果を取得してからキャッシュするかどうかを決定します。

4. ハンドオーバーの前に、接続インターセプターは新しい接続を検索または作成し、対応するソケット ストリームを取得する責任を負い、結果の取得後に追加の処理は実行されません。

5. サーバー インターセプタに、実際にサーバーと通信し、データをサーバーに送信し、読み取り応答データを解析するように要求します。

この一連の処理を経て、HTTPリクエストが完成します!

補足:代理店

OkHttp を使用する場合、ユーザーがOkHttpClientを使用して構成されている場合、proxyまたは の作成時にproxySelector、構成されたプロキシが使用され、proxy優先度が高くなりますproxySelector構成されていない場合は、マシン上に構成されているエージェントが取得されて使用されます。

//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();
}

したがって、アプリ内のリクエストがプロキシを経由する必要がない場合は、proxy(Proxy.NO_PROXY)パケット キャプチャを回避するようにプロキシを設定できます。NO_PROXYは次のように定義されます。

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

Java のエージェントに対応する抽象クラスには、次の 3 種類があります。

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

DIRECT:プロキシなし、HTTP:http プロキシ、SOCKS:socks プロキシ。1つ目は言うまでもありませんが、HTTPプロキシとSocksプロキシの違いは何でしょうか?

Socks プロキシの場合、HTTP シナリオでは、プロキシ サーバーは TCP データ パケットの転送を完了します。
一方、HTTP プロキシ サーバーは、データの転送に加えて、HTTP 要求と応答も解析し、パケットの内容に基づいていくつかの処理を実行します。リクエストとレスポンス。

RealConnectionメソッド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);

SOCKS プロキシが設定されている場合、ソケットの作成時に、そのソケットのプロキシを渡します。コードを記述するときは、接続時のターゲット アドレスとして HTTP サーバーが引き続き使用されます (実際には、ソケットは SOCKS プロキシ サーバーに接続する必要があります)。 ; ただし、HTTP プロキシが設定されている場合、作成されるソケットは HTTP プロキシ サーバーとの接続を確立します。

メソッド時間にconnect渡されるメソッドはaddress、次のinetSocketAddresses
RouteSelectorコレクションからのものです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));
      }
    }
}

プロキシを設定すると、HTTP サーバーのドメイン名解決がプロキシ サーバーに引き継がれます。ただし、HTTP プロキシが設定されている場合は、OkhttpClient設定された DNS 解決プロキシ サーバーが HTTP プロキシ サーバーのドメイン名に使用され、HTTP サーバーのドメイン名解決は解決のためにプロキシ サーバーに引き継がれます。

上記のコードは OkHttp でのプロキシと DNS の使用を示していますが、もう 1 つ注意すべき点があり、HTTP プロキシも通常のプロキシとトンネル プロキシの 2 種類に分類されます。

その中で、通常のエージェントは追加の操作を必要とせず、両端間でメッセージをやり取りする「仲介者」の役割を果たします。この「仲介者」は、クライアントから送信されたリクエストメッセージを受信すると、リクエストと接続ステータスを正しく処理すると同時に、新しいリクエストをサーバーに送信する必要があり、レスポンスを受信した後、レスポンス結果をパッケージ化します。応答本文を作成してクライアントに返します。通常のエージェント プロセスでは、エージェントの両端が「仲介者」の存在を認識していない可能性があります。

ただし、トンネル プロキシは仲介者として機能しなくなり、クライアントのリクエストを書き換えることはできなくなり、接続の確立後に確立されたトンネルを通じてクライアントのリクエストを無知にターミナル サーバーに転送するだけになります。トンネル プロキシは Http CONNECTリクエストを開始する必要があります。このリクエスト メソッドにはリクエスト本文がなく、プロキシ サーバーによってのみ使用され、ターミナル サーバーには渡されません。リクエストヘッダー部分が終了すると、それ以降のデータはすべてターミナルサーバーに転送すべきデータとみなされ、プロキシはクライアントからの TCP リードチャネルが閉じるまで何も考えずに直接転送する必要があります。プロキシ サーバーとターミナル サーバーが接続を確立した後、CONNECT200 Connect established応答メッセージは、ターミナル サーバーとの接続が正常に確立されたことを示すステータス コードをクライアントに返すことができます。

RealConnection の connect メソッド

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);
}

requiresTunnelメソッドの判断は次のとおりです: 現在のリクエストは https であり、http プロキシが存在します。connectTunnelこの時点でメソッドが開始されます。

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

リクエストの場合、接続が成功するとプロキシ サーバーは 200 を返します。407 が返された場合は、プロキシ サーバーが認証を必要とすることを意味します (有料プロキシなど)。この場合、リクエストに次の内容を追加する必要があります。ヘッダー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);

やっと

Android の面接の質問をまとめました。上記の面接の質問に加え、[ Java の基本、コレクション、マルチスレッド、仮想マシン、リフレクション、ジェネリックス、同時プログラミング、Android の 4 つの主要コンポーネント、非同期タスク、メッセージ] も含まれていますメカニズム、UI描画、パフォーマンスチューニング、SDN、サードパーティフレームワーク、デザインパターン、Kotlin、コンピュータネットワーク、システム起動プロセス、Dart、Flutter、アルゴリズムとデータ構造、NDK、H.264、H.265、オーディオコーデック、FFmpeg 、OpenMax、OpenCV、OpenGL ES ]
ここに画像の説明を挿入します

困っている友達は、以下の QR コードをスキャンして、すべてのインタビューの質問と回答分析を無料で受け取ることができます。

おすすめ

転載: blog.csdn.net/datian1234/article/details/135256999