二十六、OkHttp原理分析(二)

前言

前面我們提到了Okhttp的五大攔截器,下面我們將分析每一個攔截器的具體作用。

一、RetryAndFollowUpInterceptor(重試和重定向攔截器)

重試和重定向截器顧名思義是用來負責請求的重試和重定向的。我們來看源碼的實現:

1、重試

在RetryAndFollowUpInterceptor這個攔截器中本身對Request沒有做什麼特殊的處理,在源碼中首先是開啓了一個while(true循環),有兩個關鍵點在兩個catch 異常處執行了continue語句,繼續執行while循環裏的代碼,說明這兩種場景下需要重試。

 while (true) {
      transmitter.prepareToConnect(request);

      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } 
  //需要重試
    catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        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, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

我們來分析下需要重試的條件:當發生了RouteException 和IOException 之後,在catch異常處會根據相應的判斷是否要continue重試。

  • RouteException 路由異常
    catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
    //路由異常,連接未成功,請求還沒發出去,
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      
  • IOException IO異常
catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
//請求發出去了,但是和服務器通信失敗了(Socket流正在讀寫數據的時候斷開)
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      }

以上兩個異常都是根據recover方法判斷是否能夠重試,如果返回true則表示能重試,否則直接拋出異常。

  • recover方法
  private boolean recover(IOException e, Transmitter transmitter,
      boolean requestSendStarted, Request userRequest) {
    // The application layer has forbidden retries.
  //1、應用層禁止了重試,比如在Okhttpclient的時候設置了不允許重試。這種情況發生異常的時候,不會進行重  試
    if (!client.retryOnConnectionFailure()) return false;

    // We can't send the request body again.
  //2、不能再次發送請求體
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // This exception is fatal.
  //3、這個異常是致命的,比如協議的異常
    if (!isRecoverable(e, requestSendStarted)) return false;

    // No more routes to attempt.
//4、沒有可以用來連接的流程
    if (!transmitter.canRetry()) return false;

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

我們分析下recover方法不能重試的條件

  • 第一個判斷表示應用層禁止了重試,比如我們在配置OkHttpClient的時候禁止了重試,設置 retryOnConnectionFailure;屬性爲false
    if (!client.retryOnConnectionFailure()) return false;
  • 第二個判斷註釋的意思是:不能再次發送請求體
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

主要是通過requestIsOneShot方法判斷

  private boolean requestIsOneShot(IOException e, Request userRequest) {
    RequestBody requestBody = userRequest.body();
    return (requestBody != null && requestBody.isOneShot())
        || e instanceof FileNotFoundException;
  }

其中主要又是根據 requestBody.isOneShot()這個條件判斷,大概意思是根據408、401、503、Retry-After: 0
這些條件判斷這個請求是一次性。當滿足這個條件的時候也說明不能重試。這裏我也不是很理解。

   * <p>This method returns false unless it is overridden by a subclass.
   *
   * <p>By default OkHttp will attempt to retransmit request bodies when the original request fails
   * due to a stale connection, a client timeout (HTTP 408), a satisfied authorization challenge
   * (HTTP 401 and 407), or a retryable server failure (HTTP 503 with a {@code Retry-After: 0}
   * header).
   */
  public boolean isOneShot() {
    return false;
  }

  • 第三個異常的意思是這個異常是致命的,比如協議的異常。就不會進重試
   if (!isRecoverable(e, requestSendStarted)) return false;

具體又是通過調用isRecoverable方法來判斷具體是那些協議是致命的。

 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.
//SSL握手異常,比如證書出現問題,不會重試
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
//SSL握手未授權,不能重試
    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;
  }

(1)如果是協議異常,不會進行重試(比如整個通信沒有按照HTTP協議來通信)
(2)超時異常,由於網絡造成的波動造成的超時,允許重試
(3)SSL握手失敗,SSL驗證失敗,不會重試。前者是證書驗證失敗,後者可能壓根沒有證書。

  • 第四個判斷意思是沒有更多的路線,不會重試
    // No more routes to attempt.
    if (!transmitter.canRetry()) return false;

大概的意思是經過上面三個判斷如果都通過了,那麼就再判斷是否有可用的路線,比如經過DNS解析域名可能返回多個IP,一個IP失敗之後,嘗試重試另外一個IP。
以上我們可以瞭解到OKHTTP進行重試的條件是非常苛刻的,一般是由於網絡的波動。分別要經過4個大條件的判斷。

2、重定向

我們 繼續來看RetryAndFollowUpInterceptor下面的源碼

 Route route = exchange != null ? exchange.connection().route() : null;
      Request followUp = followUpRequest(response, route);

      if (followUp == null) {
        if (exchange != null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }

      RequestBody followUpBody = followUp.body();
      if (followUpBody != null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;

當走過重試條件的代碼之後,往下走,有兩行比較關鍵的代碼

Route route = exchange != null ? exchange.connection().route() : null;
      Request followUp = followUpRequest(response, route);

表示重定向,主要調用了followUpRequest方法

/**
   * Figures out the HTTP request to make in response to receiving {@code userResponse}. This will
   * either add authentication headers, follow redirects or handle a client request timeout. If a
   * follow-up is either unnecessary or not applicable, this returns null.
   */
  private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
       case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

    .......
      default:
        return null;
    }
  }

判斷重定向的代碼非常多,最終目的就是要通過拿到一個Request,如果Request爲空,就不需要重定向,如果Request不爲空,則可以重定向。而裏面的核心判斷主要是通過服務器返回的響應碼比如是3開頭的,當服務器返回了一個location的響應頭的時候,代表這是一個重定向的新地址。那麼客戶端就可以創建新的Request重定向到該地址。
而重定向不是無限制的重定向的,在接下來的源碼中提現

     if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

當重定向次數大於MAX_FOLLOW_UPS = 20的時候,就不能再重定向了,拋出ProtocolException異常。

3、RetryAndFollowUpInterceptor攔截器小結

RetryAndFollowUpInterceptor攔截器主要是負責重試和重定向,本身對Request沒有做其他的處理。

  • 重試的話會根據兩個異常來判斷RouteException和IOException,而他們最終都是通過調用recover方法來判斷是否要重試。而主要的判斷是用戶禁止了重試、協議異常、RequestBody一次發送就不能重試。如果滿足的話,在判斷是否有可用的路線。如果有才能重試,如果沒有也不能重試。整個重試的條件比較苛刻,一般是由於網絡波動造成的超時,並且有多餘的可用路線的情況下才能重試。
  • 重定向最終都是通過followUpRequest方法來獲取一個重定向的Request,判斷條件就比較多,裏面的判斷主要能重定向的就是服務器返回了一個3開頭的狀態碼,並且返回了一個重定向地址Location。並且最大重定向次數爲20

二、BridgeInterceptor(橋接攔截器)

在HTT協議中需要設置一些必須的請求頭,因此在橋接攔截器中就是負責給我們補全這些請求頭。比如設置請求內容長度、編碼、gzip壓縮、cookie等


橋接攔截器主要是將用戶構建的Request轉換成符合Http協議網絡請求的Request,將符合網絡規範的Request交接給下一個攔截器。
部分源碼

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

三、CacheInterceptor(緩存攔截器)

緩存攔截器主要負責是否要寫入緩存,請求的時候是否要使用緩存裏的數據
核心源碼

  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

  
    }

主要是根據networkRequest 和cacheResponse兩個對象判斷,分別意思是:是否需要網絡請求和是否有緩存
具體判斷根據以下表格


而這兩個對象的誕生主要是根據CacheStrategy策略類來的

 CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

最終都是通過下面這個方法來策略的

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

      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }
...............

緩存的判斷是非常複雜的,我自己也還不是很搞懂。。。

四、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();
    Transmitter transmitter = realChain.transmitter();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    return realChain.proceed(request, transmitter, exchange);
  }
}

這個連接的創建或者查找主要是通過Transmitter類以下核心代碼,

  Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
      if (noMoreExchanges) {
        throw new IllegalStateException("released");
      }
      if (exchange != null) {
        throw new IllegalStateException("cannot make a new request because the previous response "
            + "is still open: please call response.close()");
      }
    }

    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      return result;
    }
  }

我們看到Transmitter封裝了一個連接池connectionPool。然後調用exchangeFinder.find方法去查找
最終調用到了ExchangeFinder的findConnection方法

 private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
....

最終就是通過操作這個連接池看是否是從連接池中拿連接還是創建新的連接
接下來我們來簡單分析連接池的實現。RealConnectionPool

 public RealConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
  }

構造方法中規定了連接池閒置連接數量的大小,以及存活時間

  • 將連接加入連接池
  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

其實就是將連接加入到connections集合中,並且在第一次加入連接的時候開啓了一個線程池任務呢,來維護連接池裏的數量,及時清理操作。

 
 executor.execute(cleanupRunnable);
  private final Runnable cleanupRunnable = () -> {
    while (true) {
      long waitNanos = cleanup(System.nanoTime());
      if (waitNanos == -1) return;
      if (waitNanos > 0) {
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        synchronized (RealConnectionPool.this) {
          try {
//典型的生產者消費者模式
            RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };
  • 移除連接
   */
  boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connection.noNewExchanges || maxIdleConnections == 0) {
      connections.remove(connection);
      return true;
    } else {
      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
      return false;
    }
  }
  • 獲取連接
    通過封裝的Address嘗試從連接池中獲取連接。
  if (result == null) {
        // Attempt to get a connection from the pool.
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        } else if (nextRouteToTry != null) {
          selectedRoute = nextRouteToTry;
          nextRouteToTry = null;
        } else if (retryCurrentRoute()) {
          selectedRoute = transmitter.connection.route();
        }
  /**
   * Attempts to acquire a recycled connection to {@code address} for {@code transmitter}. Returns
   * true if a connection was acquired.
   *
   * <p>If {@code routes} is non-null these are the resolved routes (ie. IP addresses) for the
   * connection. This is used to coalesce related domains to the same HTTP/2 connection, such as
   * {@code square.com} and {@code square.ca}.
   */
  boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
      @Nullable List<Route> routes, boolean requireMultiplexed) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (requireMultiplexed && !connection.isMultiplexed()) continue;
      if (!connection.isEligible(address, routes)) continue;
      transmitter.acquireConnectionNoEvents(connection);
      return true;
    }
    return false;
  }

連接攔截器小結

連接攔截器主要是負責提供一個有效的Socket連接,在這個連接上進行HTTP數據的收發,而這個連接通過RealConnection類來封裝,而在連接攔截器中通過一個Transmitter類的一個RealConnectionPool連接池來管理連接。RealConnectionPool連接池規定了最大閒置連接大小,以及連接的存活時間。連接池的加入連接和移除連接通過典型的生產者消費者模型。

五、CallServerInterceptor(請求服務器攔截器)

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();

    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    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"))) {
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        if (request.body().isDuplex()) {
          // Prepare a duplex body so that the application can send a request body later.
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else {
          // Write the request body if the "Expect: 100-continue" expectation was met.
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
        exchange.noRequestBody();
        if (!exchange.connection().isMultiplexed()) {
          // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
          // from being reused. Otherwise we're still obligated to transmit the request body to
          // leave the connection in a consistent state.
          exchange.noNewExchangesOnConnection();
        }
      }
    } else {
      exchange.noRequestBody();
    }

    if (request.body() == null || !request.body().isDuplex()) {
      exchange.finishRequest();
    }

    if (!responseHeadersStarted) {
      exchange.responseHeadersStart();
    }

    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }

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

    int code = response.code();
    if (code == 100) {
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual response
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }

    exchange.responseHeadersEnd(response);

    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      exchange.noNewExchangesOnConnection();
    }

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

    return response;
  }

CallServerInterceptor請求服務器攔截器拿到上一個攔截器的連接之後負責和服務器真正的通信,收發數據。然後將響應結果返回到上一個攔截器,依次往上傳遞。最終傳到用戶層。

五、攔截器總結

  • 重試和重定向攔截器
    主要作用是當拋出IO和路線異常的時候,根據判斷是否要重試,一般主要判斷就是用戶是否禁止了重試、是否是協議異常、是否有多餘的路線等等
    而重定向主要是根據服務器的響應碼中是否是3開頭的以及是否返回了重定向的地址來決定是否重定向。並且重定向的最大次數是限制的,20次。
  • 橋接攔截器
    橋接攔截器主要是設置一些HTTP規範的請求頭比如HOST,使得我們的請求是符合HTTP協議的。並添加一些默認的行爲比如GZIP壓縮。
  • 緩存攔截
    緩存攔截器主要是作用是決定是否需要緩存到本地和是否需要從本地讀取緩存
  • 連接攔截器
    連接攔截器主要作用是提供一個有效的Socket連接,連接的管理通過一個連接池來控制。 並獲得Socket的流,獲得之後不做任何操作
  • 請求服務器攔截器
    真正的與服務器通信,根據上面連接攔截器提供的 Socket流向服務器發送數據並解析響應的數據。然後返回到上一個攔截器。最後每個攔截器依次返回最終返回到用戶端。

六、OkHttp的優點與小結

  • 支持Http1、Http2、Quic以及WebSocket
  • 連接池服用,減少延遲,避免每次請求都創建一個新的連接,每一個新的連接需要TCP三次握手。
  • 支撐GZIP壓縮,減少數據量
  • 支撐緩存
  • 請求失敗有重試機制和重定向機制
  • OkHttp是基於Socket套接字實現的一個HTTP應用層協議。直接與TCP傳輸層協議打交道。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章