OkHttp 차단기(2)

17346 단어
이 글은 주로 OkHttp의 기본 차단기를 소개합니다
  • 차단기 재시험
  • 브리지 차단기
  • 캐시 차단기
  • 연결 차단기
  • 서버 차단기에 액세스합니다

  • 차단기를 통해 OkHttp의 네트워크 접근 과정을 분해하고 모든 차단기는 그 직무를 전담한다.모든 차단기는 Interceptor 인터페이스를 실현했다
    public interface Interceptor {
      Response intercept(Chain chain) throws IOException;
    
      interface Chain {
        Request request();
    
        Response proceed(Request request) throws IOException;
    
        Connection connection();
      }
    }
    

    자, 이 차단기들이 무엇을 했는지 살펴보겠습니다. 위의 인터페이스에서 우리는 Response proceed (Request request)throws IO Exception 방법에만 관심을 갖습니다.

    하나, 차단기 다시 시도하기 Retry AndFollowUpInterceptor

    @Override public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
    
        streamAllocation = new StreamAllocation(
            client.connectionPool(), createAddress(request.url()), callStackTrace);
    
        int followUpCount = 0;
        Response priorResponse = null;
        while (true) {
          if (canceled) {
            streamAllocation.release();
            throw new IOException("Canceled");
          }
    
          Response response = null;
          boolean releaseConnection = true;
          try {
            response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
            releaseConnection = false;
          } catch (RouteException e) {
            // The attempt to connect via a route failed. The request will not have been sent.
            if (!recover(e.getLastConnectException(), false, request)) {
              throw e.getLastConnectException();
            }
            releaseConnection = false;
            Log.i("xxx","retry  "+e.toString() );
            continue;
          } catch (IOException e) {
            // An attempt to communicate with a server failed. The request may have been sent.
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            if (!recover(e, requestSendStarted, request)) throw e;
            releaseConnection = false;
            Log.i("xxx","retry io "+e.toString() );
            continue;
          } finally {
            Log.i("xxx","finally  " );
            // We're throwing an unchecked exception. Release any resources.
            if (releaseConnection) {
              streamAllocation.streamFailed(null);
              streamAllocation.release();
            }
          }
    
          // Attach the prior response if it exists. Such responses never have a body.
          if (priorResponse != null) {
            response = response.newBuilder()
                .priorResponse(priorResponse.newBuilder()
                        .body(null)
                        .build())
                .build();
          }
    
          Request followUp = followUpRequest(response);
    
          if (followUp == null) {
            if (!forWebSocket) {
              streamAllocation.release();
            }
            return response;
          }
    
          closeQuietly(response.body());
    
          if (++followUpCount > MAX_FOLLOW_UPS) {
            streamAllocation.release();
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
          }
    
          if (followUp.body() instanceof UnrepeatableRequestBody) {
            streamAllocation.release();
            throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
          }
    
          if (!sameConnection(response, followUp.url())) {
            streamAllocation.release();
            streamAllocation = new StreamAllocation(
                client.connectionPool(), createAddress(followUp.url()), callStackTrace);
          } else if (streamAllocation.codec() != null) {
            throw new IllegalStateException("Closing the body of " + response
                + " didn't close its backing stream. Bad interceptor?");
          }
    
          request = followUp;
          priorResponse = response;
        }
      }
    

    1, Volley의 재시도 메커니즘과 유사합니다. 우선 응답을 얻지 못하고 이상이 발생하면 다시 시도할 수 있습니다.
    2, 다음에 취소 여부를 판단할 것입니다. 취소하면 이상을 던지고 AsyncCall의execute 방법에서 실패한 방법responseCallback을 직접 리셋합니다.onFailure(RealCall.this, e);
    3, response = (RealInterceptorChain) chain을 사용합니다.proceed(request, streamAllocation, null, null); 다음 차단기를 차례로 호출하여 응답을 가져옵니다. 이상이 발생하면 복구 함수를 통해 다시 시도할 수 있는지 여부를 판단합니다. 그렇지 않으면 계속 아래로 실행합니다.다음은 Recover 함수를 보십시오. 안에 네 가지 상황을 열거하였는데 다시 시도할 수 없습니다
    private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
        streamAllocation.streamFailed(e);
    
        // The application layer has forbidden retries.
        if (!client.retryOnConnectionFailure()) return false;
    
        // We can't send the request body again.
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
    
        // This exception is fatal.
        if (!isRecoverable(e, requestSendStarted)) return false;
    
        // No more routes to attempt.
        if (!streamAllocation.hasMoreRoutes()) return false;
    
        // For failure recovery, use the same route selector with a new connection.
        return true;
      }
    

    A, 재시도 금지 B, 재송신 요청체 C를 설정했는데 심각한 오류가 발생했습니다. 이런 심각한 오류를 다시 시도할 수 없습니다. isRecoverable 방법의 if 판단에서 제시되었습니다. 예를 들어 프로토콜 오류, 중단, 연결 시간 초과, SSL 악수 오류 등입니다.
    private boolean isRecoverable(IOException e, boolean requestSendStarted) {
        // If there was a protocol problem, don't recover.
        if (e instanceof ProtocolException) {
          return false;
        }
    
        // If there was an interruption don't recover, but if there was a timeout connecting to a route
        // we should try the next route (if there is one).
        if (e instanceof InterruptedIOException) {
          return e instanceof SocketTimeoutException && !requestSendStarted;
        }
    
        // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
        // again with a different route.
        if (e instanceof SSLHandshakeException) {
          // If the problem was a CertificateException from the X509TrustManager,
          // do not retry.
          if (e.getCause() instanceof CertificateException) {
            return false;
          }
        }
        if (e instanceof SSLPeerUnverifiedException) {
          // e.g. a certificate pinning error.
          return false;
        }
    
        // An example of one we might want to retry with a different route is a problem connecting to a
        // proxy and would manifest as a standard IOException. Unless it is one we know we should not
        // retry, we return true and try a new route.
        return true;
      }
    

    D, 더 이상 시도하는 루트가 없습니다
    4, 순조롭게 응답을 받으면 Request follow Up = follow Up Request(response)를 통과합니다.재지정 여부를 판단하다
  • 존재하면 새로운follow Up을 통해 다시 접근합니다. 이번에 받은 응답은priorResponse에 저장된 다음 이 순환 While (true) 에서 다시 요청합니다.이 리디렉션 처리에 대해 Volley에는 기본적으로 없습니다
  • 존재하지 않으면followUp이 비어 있으면 Response 대상을 이전 차단기로 되돌려줍니다

  • 둘째, 브리지 차단기 Bridge Interceptor


    브리지 인터셉터 브리지 인터셉터는 비교적 간단하고 주로 두 가지 일을 한다
  • 다음 차단기를 차례로 호출하여 응답을 얻은 후 Request 요청 헤더를 구성합니다
  • 응답을 받은 후 응답 헤더를 구성합니다
  •  public Response intercept(Chain chain) throws IOException {
        Request userRequest = chain.request();
        Request.Builder requestBuilder = userRequest.newBuilder();
    
        RequestBody body = userRequest.body();
        if (body != null) {
          MediaType contentType = body.contentType();
          if (contentType != null) {
            requestBuilder.header("Content-Type", contentType.toString());
          }
    
          long contentLength = body.contentLength();
          if (contentLength != -1) {
            requestBuilder.header("Content-Length", Long.toString(contentLength));
            requestBuilder.removeHeader("Transfer-Encoding");
          } else {
            requestBuilder.header("Transfer-Encoding", "chunked");
            requestBuilder.removeHeader("Content-Length");
          }
        }
    
        if (userRequest.header("Host") == null) {
          requestBuilder.header("Host", hostHeader(userRequest.url(), false));
        }
    
        if (userRequest.header("Connection") == null) {
          requestBuilder.header("Connection", "Keep-Alive");
        }
    
        // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
        // the transfer stream.
        boolean transparentGzip = false;
        if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
          transparentGzip = true;
          requestBuilder.header("Accept-Encoding", "gzip");
        }
    
        List cookies = cookieJar.loadForRequest(userRequest.url());
        if (!cookies.isEmpty()) {
          requestBuilder.header("Cookie", cookieHeader(cookies));
        }
    
        if (userRequest.header("User-Agent") == null) {
          requestBuilder.header("User-Agent", "sss");
    //      requestBuilder.header("User-Agent", Version.userAgent());
        }
    
        Response networkResponse = chain.proceed(requestBuilder.build());
    
        HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
    
        Response.Builder responseBuilder = networkResponse.newBuilder()
            .request(userRequest);
    
        if (transparentGzip
            && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
            && HttpHeaders.hasBody(networkResponse)) {
          GzipSource responseBody = new GzipSource(networkResponse.body().source());
          Headers strippedHeaders = networkResponse.headers().newBuilder()
              .removeAll("Content-Encoding")
              .removeAll("Content-Length")
              .build();
          responseBuilder.headers(strippedHeaders);
          responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
        }
    
        return responseBuilder.build();
      }
    

    위에서 요청 헤더를 구성할 때 주의해야 할 것은 Content-Encoding과 Content-Length가 동시에 나타날 수 없다는 것이다.
  • Content-Length가 존재하고 유효하면 메시지 내용의 전송 길이와 완전히 일치해야 한다.너무 짧으면 자르고, 너무 길면 시간 초과를 초래합니다.
  • HTTP1.1은 chunk 모드를 지원해야 합니다.메시지 길이가 확실하지 않을 때,chunk 메커니즘을 통해 이런 상황을 처리할 수 있기 때문에, 즉 블록 전송..

  • 3, 캐시 차단기 CacheInterceptor


    캐시 차단기를 잘 이해하려면 Http 캐시 메커니즘에 대해 알아야 한다. 특히 요청과 응답에 관련된 요청 헤더에는 캐시 시간을 제어하는 Cache-Control, Expires 및 캐시 서버 재검증을 위한 If-Modify-Since, If-None-Match와 메모리 신선도를 늦추는, no-cache, no-store, max-age, if-only-cache 등이 참고할 수 있다.
    다음은 핵심 코드를 붙여주세요.
    @Override public Response intercept(Chain chain) throws IOException {
        Response cacheCandidate = cache != null
            ? cache.get(chain.request())
            : null;
    
        long now = System.currentTimeMillis();
    
        CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        Request networkRequest = strategy.networkRequest;
        Response cacheResponse = strategy.cacheResponse;
    
        if (cache != null) {
          cache.trackResponse(strategy);
        }
    
        if (cacheCandidate != null && cacheResponse == null) {
          closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
        }
    
        // If we're forbidden from using the network and the cache is insufficient, fail.
        if (networkRequest == null && cacheResponse == null) {
          return new Response.Builder()
              .request(chain.request())
              .protocol(Protocol.HTTP_1_1)
              .code(504)
              .message("Unsatisfiable Request (only-if-cached)")
              .body(Util.EMPTY_RESPONSE)
              .sentRequestAtMillis(-1L)
              .receivedResponseAtMillis(System.currentTimeMillis())
              .build();
        }
    
        // If we don't need the network, we're done.
        if (networkRequest == null) {
          return cacheResponse.newBuilder()
              .cacheResponse(stripBody(cacheResponse))
              .build();
        }
    
        Response networkResponse = null;
        try {
          networkResponse = chain.proceed(networkRequest);
        } finally {
          // If we're crashing on I/O or otherwise, don't leak the cache body.
          if (networkResponse == null && cacheCandidate != null) {
            closeQuietly(cacheCandidate.body());
          }
        }
    
        // If we have a cache response too, then we're doing a conditional get.
        if (cacheResponse != null) {
          if (networkResponse.code() == HTTP_NOT_MODIFIED) {
            Response response = cacheResponse.newBuilder()
                .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
                .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();
            networkResponse.body().close();
    
            // Update the cache after combining headers but before stripping the
            // Content-Encoding header (as performed by initContentStream()).
            cache.trackConditionalCacheHit();
            cache.update(cacheResponse, response);
            return response;
          } else {
            closeQuietly(cacheResponse.body());
          }
        }
    
        Response response = networkResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
    
        if (HttpHeaders.hasBody(response)) {
          CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
          response = cacheWritingResponse(cacheRequest, response);
        }
    
        return response;
      }
    
    

    1, 우선 Request의 URL에 따라 캐시에서 캐시를 가져옵니다cacheCandidate 2, 요청 Request와 캐시에서 가져온cacheCandidate(공일 수 있음)에 따라 캐시 정책 캐시 Strategy를 구성합니다. 다음은 어떻게 A를 구성하는지 보겠습니다. 먼저 Factory 대상을 구성합니다.
     public Factory(long nowMillis, Request request, Response cacheResponse) {
        ... ...
            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);
              }
            }
          }
        }
    

    나중에 캐시 응답이 비어 있지 않을 때 이 응답의 헤더 정보를 주로 얻습니다. Date, Expires Last-Modified ETag 등 정보는 캐시 만료 여부와 캐시 검증을 판단하는 데 사용됩니다.
    B, 캐시 정책 가져오기
    public CacheStrategy get() {
          CacheStrategy candidate = getCandidate();
    
          if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
            // We're forbidden from using the network and the cache is insufficient.
            return new CacheStrategy(null, null);
          }
    
          return candidate;
        }
    
    
    private CacheStrategy getCandidate() {
          if (cacheResponse == null) {
            return new CacheStrategy(request, null);
          }
    
          if (request.isHttps() && cacheResponse.handshake() == null) {
            return new CacheStrategy(request, null);
          }
    ... ....
      
          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);
        }
    

    getCandidate는 캐시 정책의 핵심 두 부분입니다. 1) 캐시를 사용하지 않고 return new Cache Strategy(request,null)를 사용합니다.2) 캐시 사용
  • 캐시가 만료되지 않고 Response로 돌아갑니다
  • 캐시가 만료되었습니다. If-None-Match와 ETag 또는 If-Modified-Since와 Last-Modify를 사용하여 캐시를 검증합니다

  • 3, 우리는 다시 CacheInterceptor의 intercept 방법으로 캐시 정책을 얻는다. CacheStrategy를 통해 일련의 판단을 한다. 1) CacheStrategy의 네트워크 Response와 cacheResponse가 비어 있으면 Cache-Conrol이 only-if-cache임을 설명하고 캐시를 사용한다. 2) 네트워크 Response가 비어 있으면 캐시가 유효하다는 것을 설명하고 cacheResponse 3)캐시가 유효하지 않거나 검증이 필요하다면,다음 차단기를 호출하여 네트워크에서 직접 가져옵니다.네트워크에서 응답을 받은 후 처리해야 할 두 가지 상황이 있을 것이다
  • 200ok는 네트워크에서 새로운 응답을 얻는 것을 대표한다(캐시가 만료되어 새로운 내용을 얻거나 캐시가 네트워크에서 가져오지 않는 것)
  • 304 Not Modify는 캐시가 만료된 후 네트워크에 검증을 요청하지만 내용이 바뀌면 새로운 응답 헤더(Date Expires Cache-Control 등)로 오래된 캐시 응답 헤더를 업데이트한 후 응답을 되돌려줍니다

  • 이로써 캐시 차단기는 캐시 차단기 안에 많은if에 대한 판단을 분석했는데 이것은 모두 Http 캐시 메커니즘에 따라 이루어진 것이다.캐시 차단기를 명확하게 이해하려면 반드시 Http 캐시 메커니즘이 추천하는 읽을거리: 영어가 좋다면: RFC2616 <> 제7장 빠른 이해는 tp 캐시 소개를 참고하십시오.

    넷째, 연결 차단기 ConnectInterceptor

    좋은 웹페이지 즐겨찾기