你想要的系列:網絡請求框架OkHttp3全解系列 - (四)攔截器詳解2:連接、請求服務(重點)

Okhttp系列文章:
你想要的系列:網絡請求框架OkHttp3全解系列 - (一)OkHttp的基本使用
你想要的系列:網絡請求框架OkHttp3全解系列 - (二)OkHttp的工作流程分析
你想要的系列:網絡請求框架OkHttp3全解系列 - (三)攔截器詳解1:重試重定向、橋、緩存(重點)
你想要的系列:網絡請求框架OkHttp3全解系列 - (四)攔截器詳解2:連接、請求服務(重點)

在本系列的上一篇文章你想要的系列:網絡請求框架OkHttp3全解系列 - (三)攔截器詳解1:重試重定向、橋、緩存(重點)中,我們分析了OkHttp攔截器鏈中的前三個攔截器:RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor,它們在請求建立連接之前做了一些預處理。

如果請求經過這三個攔截器後,要繼續往下傳遞,說明是需要進行網絡請求的(緩存不能直接滿足),也就是今天要分析的內容——剩下的兩個攔截器:ConnectInterceptor、CallServerInterceptor,分別負責 連接建立請求服務讀寫

背景 - HTTP協議發展

在講解攔截器之前,我們有必要了解http協議相關背景知識,因爲okhttp的網絡連接正是基於此實現的。HTTP協議經歷了以下三個版本階段。

HTTP1.0

HTTP1.0中,一次請求 會建立一個TCP連接,請求完成後主動斷開連接。這種方法的好處是簡單,各個請求互不干擾。
但每次請求都會經歷 3次握手、2次或4次揮手的連接建立和斷開過程——極大影響網絡效率和系統開銷。
http1.0

HTTP1.1

HTTP1.1中,解決了HTTP1.0中連接不能複用的問題,支持持久連接——使用keep-alive機制:一次HTTP請求結束後不會立即斷開TCP連接,如果此時有新的HTTP請求,且其請求的Host同上次請求相同,那麼會直接複用TCP連接。這樣就減少了建立和關閉連接的消耗和延遲。keep-alive機制在HTTP1.1中是默認打開的——即在請求頭添加:connection:keep-alive。(keep-alive不會永久保持連接,它有一個保持時間,可在不同的服務器軟件(如Apache)中設定這個時間)
http1.1

HTTP2.0

HTTP1.1中,連接的複用是串行的:一個請求建立了TCP連接,請求完成後,下一個相同host的請求繼續使用這個連接。 但客戶端想 同時 發起多個並行請求,那麼必須建立多個TCP連接。將會產生網絡延遲、增大網路開銷。

並且HTTP1.1不會壓縮請求和響應報頭,導致了不必要的網絡流量;HTTP1.1不支持資源優先級導致底層TCP連接利用率低下。在HTTP2.0中,這些問題都會得到解決,HTTP2.0主要有以下特性

  • 新的二進制格式(Binary Format):http/1.x使用的是明文協議,其協議格式由三部分組成:request line,header,body,其協議解析是基於文本,但是這種方式存在天然缺陷,文本的表現形式有多樣性,要做到健壯性考慮的場景必然很多,二進制則不同,只認0和1的組合;基於這種考慮,http/2.0的協議解析決定採用二進制格式,實現方便且健壯
  • 多路複用(MultiPlexing):即連接共享,使用streamId用來區分請求,一個request對應一個stream並分配一個id,這樣一個TCP連接上可以有多個stream,每個stream的frame可以隨機的混雜在一起,接收方可以根據stream id將frame再歸屬到各自不同的request裏面
  • 優先級和依賴(Priority、Dependency):每個stream都可以設置優先級和依賴,優先級高的stream會被server優先處理和返回給客戶端,stream還可以依賴其它的sub streams;優先級和依賴都是可以動態調整的,比如在APP上瀏覽商品列表,用戶快速滑動到底部,但是前面的請求已經發出,如果不把後面的優先級設高,那麼當前瀏覽的圖片將會在最後展示出來,顯然不利於用戶體驗
  • header壓縮:http2.0使用encoder來減少需要傳輸的header大小,通訊雙方各自cache一份header fields表,既避免了重複header的傳輸,又減小了需要傳輸的大小
  • 重置連接:很多APP裏都有停止下載圖片的需求,對於http1.x來說,是直接斷開連接,導致下次再發請求必須重新建立連接;http2.0引入RST_STREAM類型的frame,可以在不斷開連接的前提下取消某個request的stream

其中涉及了兩個新的概念:

  • 數據流-stream:基於TCP連接之上的邏輯雙向字節流,用於承載雙向消息,對應一個請求及其響應。客戶端每發起一個請求就建立一個數據流,後續該請求及其響應的所有數據都通過該數據流傳輸。每個數據流都有一個唯一的標識符和可選的優先級信息。
  • 幀-frame:HTTP/2的最小數據切片單位,承載着特定類型的數據,例如 HTTP 標頭、消息負載,等等。 來自不同數據流的幀可以交錯發送,然後再根據每個幀頭的數據流標識符重新組裝,從而在宏觀上實現了多個請求或響應並行傳輸的效果。
    http2.0

這裏的 多路複用機制 就實現了 在同一個TCP連接上 多個請求 並行執行。

無論是HTTP1.1的Keep-Alive機制還是HTTP2.0的多路複用機制,在實現上都需要引入連接池來維護網絡連接。下面就開始分析 OkHttp中的連接池實現——連接攔截器ConnectInterceptor

ConnectInterceptor

連接攔截器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.newExchange獲取Exchange實例,並作爲參數調用攔截器鏈的proceed的方法。注意到前面分析過的攔截器調用的proceed方法是一個參數的,而這裏是三個參數的。這是因爲下一個攔截器(如果沒有配置網絡攔截器的話,就是CallServerInterceptor,也是最後一個)需要進行真正的網絡IO操作,而 Exchange(意爲交換)主要作用就是真正的IO操作:寫入請求、讀取響應(會在下一個攔截器做介紹)。

實際上獲取Exchange實例的邏輯處理都封裝在Transmitter中了。前面的文章提到過Transmitter,它是“發射器”,是把 請求 從應用端 發射到 網絡層,它持有請求的 連接、請求、響應 和 流,一個請求對應一個Transmitter實例,一個數據流。下面就看下它的newExchange方法:

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

若是第一次請求,前面兩個if是沒走進去的。接着看到使用exchangeFinder的find方法獲取到了ExchangeCodec實例,然後作爲參數構建了Exchange實例,並返回。嗯,看起來也很簡單的樣子。 注意到這個方法裏涉及了連接池RealConnectionPool、交換尋找器ExchangeFinder、交換編解碼ExchangeCodec、交換管理Exchange這幾個類(翻譯成這樣盡力了😊,意會吧)。

  • RealConnectionPool,連接池,負責管理請求的連接,包括新建、複用、關閉,理解上類似線程池。
  • ExchangeCodec,接口類,負責真正的IO操作—寫請求、讀響應,實現類有Http1ExchangeCodec、Http2ExchangeCodec,分別對應HTTP1.1協議、HTTP2.0協議。
  • Exchange,管理IO操作,可以理解爲 數據流,是ExchangeCodec的包裝,增加了事件回調;一個請求對應一個Exchange實例。傳給下個攔截器CallServerInterceptor使用。
  • ExchangeFinder,(從連接池中)尋找可用TCP連接,然後通過連接得到ExchangeCodec。

ExchangeFinder

ExchangeFinder的作用從名字就可以看出——Exchange尋找者,本質是爲請求尋找一個TCP連接。如果已有可用連接就直接使用,沒有則打開一個新的連接。 一個網絡請求的執行,需要先有一個指向目標服務的TCP連接,然後再進行寫請求、讀響應的IO操作。ExchangeFinder是怎麼尋找的呢?繼續往下看~

我們先看exchangeFinder初始化的地方:

  public void prepareToConnect(Request request) {
    ...
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }

看到這裏應該會想起上一篇文章中分析RetryAndFollowUpInterceptor時提到過,prepareToConnect這個方法作用是連接準備,就是創建了ExchangeFinder實例。主要到傳入的參數有connectionPool、createAddress方法返回的Address、call、eventListener。connectionPool是連接池,稍後分析,先看下createAddress方法:

  private Address createAddress(HttpUrl url) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    if (url.isHttps()) {
      sslSocketFactory = client.sslSocketFactory();
      hostnameVerifier = client.hostnameVerifier();
      certificatePinner = client.certificatePinner();
    }

    return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
        client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
  }

使用url和client配置 創建一個Address實例。Address意思是指向服務的連接的地址,可以理解爲請求地址及其配置。Address有一個重要作用:相同Address的HTTP請求 共享 相同的連接。這可以作爲前面提到的 HTTP1.1和HTTP2.0 複用連接 的請求的判斷。

回頭看exchangeFinder的find方法

  public ExchangeCodec find(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      //找到一個健康的連接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      //利用連接實例化ExchangeCodec對象,如果是HTTP/2返回Http2ExchangeCodec,否則返回Http1ExchangeCodec
      return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
      trackFailure();
      throw e;
    } catch (IOException e) {
      trackFailure();
      throw new RouteException(e);
    }
  }

主要就是通過findHealthyConnection方法獲取連接RealConnection實例,然後用RealConnection的newCodec方法獲取了ExchangeCodec實例,如果是HTTP/2返回Http2ExchangeCodec,否則返回Http1ExchangeCodec,然後返回。

findHealthyConnection方法名透露着 就是去尋找可用TCP連接的,而我們猜測這個方法內部肯定和連接池ConnectionPool有緊密的關係。接着跟進findHealthyConnection方法:

  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      //找連接
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // 是新連接 且不是HTTP2.0 就不用體檢
      synchronized (connectionPool) {
        if (candidate.successCount == 0 && !candidate.isMultiplexed()) {
          return candidate;
        }
      }
      // 體檢不健康,繼續找
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
      	//標記不可用
        candidate.noNewExchanges();
        continue;
      }

      return candidate;
    }
  }

循環尋找連接:如果是不健康的連接,標記不可用(標記後會移除,後面講連接池會講到),然後繼續找。健康是指連接可以承載新的數據流,socket是連接狀態。我們跟進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) {
      //請求已被取消(Call的cancel方法->transmitter的cancel方法),拋異常
      if (transmitter.isCanceled()) throw new IOException("Canceled");
      hasStreamFailure = false; 

      // 嘗試使用 已給數據流分配的連接.(例如重定向請求時,可以複用上次請求的連接)
      releasedConnection = transmitter.connection;
      //有已分配的連接,但已經被限制承載新的數據流,就嘗試釋放掉(如果連接上已沒有數據流),並返回待關閉的socket。
      toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
          ? transmitter.releaseConnectionNoEvents()
          : null;

      if (transmitter.connection != null) {
        // 不爲空,說明上面沒有釋放掉,那麼此連接可用
        result = transmitter.connection;
        releasedConnection = null;
      }

      if (result == null) {
        // 沒有已分配的可用連接,就嘗試從連接池獲取。(連接池稍後詳細講解)
        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();
        }
      }
    }
    closeQuietly(toClose);//(如果有)關閉待關閉的socket

    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);//(如果有)回調連接釋放事件
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);//(如果有)回調(從連接池)獲取連接事件
    }
    if (result != null) {
      // 如果有已分配可用連接 或 從連接池獲取到連接,結束!  沒有 就走下面的新建連接過程。
      return result;
    }

    // 如果需要路由信息,就獲取。是阻塞操作
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    List<Route> routes = null;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) throw new IOException("Canceled");

      if (newRouteSelection) {
        //現在有了IP地址,再次嘗試從連接池獲取。可能會因爲連接合並而匹配。(這裏傳入了routes,上面的傳的null)
        routes = routeSelection.getAll();
        if (connectionPool.transmitterAcquirePooledConnection(
            address, transmitter, routes, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        }
      }
	  //第二次連接池也沒找到,就新建連接
      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        result = new RealConnection(connectionPool, selectedRoute);
        connectingConnection = result;
      }
    }

    // 如果第二次從連接池的嘗試成功了,結束,因爲連接池中的連接是已經和服務器建立連接的
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // 第二次沒成功,就把新建的連接,進行TCP + TLS 握手,與服務端建立連接. 是阻塞操作
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());//從失敗名單中移除

    Socket socket = null;
    synchronized (connectionPool) {
      connectingConnection = null;
      // 最後一次嘗試從連接池獲取,注意最後一個參數爲true,即要求 多路複用(http2.0)
      //意思是,如果本次是http2.0,那麼爲了保證 多路複用性,(因爲上面的握手操作不是線程安全)會再次確認連接池中此時是否已有同樣連接
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
        // 如果獲取到,就關閉我們創建裏的連接,返回獲取的連接
        result.noNewExchanges = true;
        socket = result.socket();
        result = transmitter.connection;

        // 那麼這個剛剛連接成功的路由 就可以 用作下次 嘗試的路由
        nextRouteToTry = selectedRoute;
      } else {
        //最後一次嘗試也沒有的話,就把剛剛新建的連接存入連接池
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);//把連接賦給transmitter
      }
    }
    closeQuietly(socket);//如果剛剛建立的連接沒用到,就關閉

    eventListener.connectionAcquired(call, result);
    return result;
  }

代碼看着很長,已經加了註釋,方法目的就是 爲 承載新的數據流 尋找 連接。尋找順序是 已分配的連接、連接池、新建連接。梳理如下:

  1. 首先會嘗試使用 已給數據流分配的連接。(已分配連接的情況例如重定向時的再次請求,說明上次已經有了連接)
  2. 若沒有 已分配的可用連接,就嘗試從連接池中 匹配獲取。因爲此時沒有路由信息,所以匹配條件:address一致——host、port、代理等一致,且 匹配的連接可以接受新的數據流。
  3. 若從連接池沒有獲取到,則取下一個代理的路由信息(多個Route,即多個IP地址),再次嘗試從連接池獲取,此時可能因爲連接合並而匹配到。
  4. 若第二次也沒有獲取到,就創建RealConnection實例,進行TCP + TLS 握手,與服務端建立連接。
  5. 此時爲了確保Http2.0連接的多路複用性,會第三次從連接池匹配。因爲新建立的連接的握手過程是非線程安全的,所以此時可能連接池新存入了相同的連接。
  6. 第三次若匹配到,就使用已有連接,釋放剛剛新建的連接;若未匹配到,則把新連接存入連接池並返回。

流程圖如下:
findConnection方法流程

看到這裏,小盆友,你是否有很多問號?

  • 開始若有了已分配的連接,但已經被限制承載新的數據流,是如何釋放的呢?
  • 代理路由信息是如何獲取的呢?
  • 如何從連接池獲取連接的?三次有什麼不同?

沒關係,慢慢來,我們先看第二個問號,代理路由信息的獲取。

RouteSelector

先來看下Route類:

public final class Route {
  final Address address;
  final Proxy proxy;//代理
  final InetSocketAddress inetSocketAddress;//連接目標地址

  public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress) {
    ...
    this.address = address;
    this.proxy = proxy;
    this.inetSocketAddress = inetSocketAddress;
  }

Route,通過代理服務器信息 proxy、連接目標地址 InetSocketAddress 來描述一條 連接服務器的具體路由

  • proxy代理:可以爲客戶端顯式配置代理服務器。否則,將使用ProxySelector代理選擇器。可能會返回多個代理。
  • IP地址:無論是直連還是代理,打開socket連接都需要IP地址。 DNS服務可能返回多個IP地址嘗試。

上面分析的findConnection方法中是使用routeSelection.getAll()獲取Route集合routes,而routeSelection是通過routeSelector.next()獲取,routeSelector是在ExchangeFinder的構造方法內創建的,也就是說routeSelector在RetryAndFollowUpInterceptor中就創建了,那麼我們看下RouteSelector:

  RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
      EventListener eventListener) {
    this.address = address;
    this.routeDatabase = routeDatabase;//連接池中的路由黑名單(連接失敗的路由)
    this.call = call;
    this.eventListener = eventListener;

    resetNextProxy(address.url(), address.proxy());
  }
  //收集代理服務器
  private void resetNextProxy(HttpUrl url, Proxy proxy) {
    if (proxy != null) {
      // 若指定了代理,那麼就這一個。(就是初始化OkhttpClient時配置的)
      proxies = Collections.singletonList(proxy);
    } else {
      //沒配置就使用ProxySelector獲取代理(若初始化OkhttpClient時沒有配置ProxySelector,會使用系統默認的) 
      List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
      proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
          ? Util.immutableList(proxiesOrNull)
          : Util.immutableList(Proxy.NO_PROXY);
    }
    nextProxyIndex = 0;
  }

注意到RouteSelector的構造方法中傳入了routeDatabase,是連接失敗的路由黑名單(後面連接池也會講到),並使用resetNextProxy方法獲取代理服務器列表:若沒有指定proxy就是用ProxySelector獲取proxy列表(若沒有配置ProxySelector會使用系統默認)。接着看next方法:

  //收集代理的路由信息
  public Selection next() throws IOException {
    if (!hasNext()) {//還有下一個代理
      throw new NoSuchElementException();
    }

    List<Route> routes = new ArrayList<>();
    while (hasNextProxy()) {
      Proxy proxy = nextProxy();
      //遍歷proxy經DNS後的所有IP地址,組裝成Route
      for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
        Route route = new Route(address, proxy, inetSocketAddresses.get(i));
        if (routeDatabase.shouldPostpone(route)) {//此路由在黑名單中,存起來最後嘗試
          postponedRoutes.add(route);
        } else {
          routes.add(route);
        }
      }

      if (!routes.isEmpty()) {
        break;
      }
    }

    if (routes.isEmpty()) {
      // 若沒有拿到路由,就嘗試上面存的黑名單的路由
      routes.addAll(postponedRoutes);
      postponedRoutes.clear();
    }
    //routes包裝成Selection返回
    return new Selection(routes);
  }

next方法主要就是獲取下一個代理Proxy的代理信息,即多個路由。具體是在resetNextInetSocketAddress方法中實現,主要是對代理服務地址進行DNS解析獲取多個IP地址,這裏就不展開了,具體可以參考OkHttp中的代理和路由

好了,到這裏 就解決了第二個問號。其他兩個問號涉及 連接池RealConnectionPool、連接RealConnection,下面就來瞅瞅。

ConnectionPool

ConnectionPool,即連接池,用於管理http1.1/http2.0連接重用,以減少網絡延遲。相同Address的http請求可以共享一個連接,ConnectionPool就是實現了連接的複用。

public final class ConnectionPool {
  final RealConnectionPool delegate;
  //最大空閒連接數5,最大空閒時間5分鐘
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }
  
  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.delegate = new RealConnectionPool(maxIdleConnections, keepAliveDuration, timeUnit);
  }
  //返回空閒連接數
  public int idleConnectionCount() {
    return delegate.idleConnectionCount();
  }
  //返回池子中的連接數
  public int connectionCount() {
    return delegate.connectionCount();
  }
  //關閉並移除所以空閒連接
  public void evictAll() {
    delegate.evictAll();
  }
}

ConnectionPool看起來比較好理解,默認配置是最大空閒連接數5,最大空閒時間5分鐘(即一個連接空閒時間超過5分鐘就移除),我們也可以在初始化okhttpClient時進行不同的配置。需要注意的是ConnectionPool是用於應用層,實際管理者是RealConnectionPool。RealConnectionPool是okhttp內部真實管理連接的地方。

連接池對連接的管理無非是 存、取、刪,上面的兩個問號分別對應 刪、取,跟進RealConnectionPool我們一個個看:

  private final Deque<RealConnection> connections = new ArrayDeque<>();
  
  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) {
          }
        }
      }
    }
  };
  //存
  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

connections是用於存連接的隊列Deque。看到在add之前 使用線程池executor執行了cleanupRunnable,意思是清理連接,爲啥要清理呢?上面提到過 連接池有 最大空閒連接數、最大空閒時間的限制,所以不滿足時是要進行清理的。並且注意到清理是一個循環,並且下一次清理前要等待waitNanos時間,啥意思呢?我們看下cleanup方法:

  long cleanup(long now) {
    int inUseConnectionCount = 0;//正在使用的連接數
    int idleConnectionCount = 0;//空閒連接數
    RealConnection longestIdleConnection = null;//空閒時間最長的連接
    long longestIdleDurationNs = Long.MIN_VALUE;//最長的空閒時間

    //遍歷連接:找到待清理的連接, 找到下一次要清理的時間(還未到最大空閒時間)
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //若連接正在使用,continue,正在使用連接數+1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }
		//空閒連接數+1
        idleConnectionCount++;

        // 賦值最長的空閒時間和對應連接
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
	  //若最長的空閒時間大於5分鐘 或 空閒數 大於5,就移除並關閉這個連接
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // else,就返回 還剩多久到達5分鐘,然後wait這個時間再來清理
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        //連接沒有空閒的,就5分鐘後再嘗試清理.
        return keepAliveDurationNs;
      } else {
        // 沒有連接,不清理
        cleanupRunning = false;
        return -1;
      }
    }
	//關閉移除的連接
    closeQuietly(longestIdleConnection.socket());

    //關閉移除後 立刻 進行下一次的 嘗試清理
    return 0;
  }

思路還是很清晰的:

  • 有空閒連接的話,如果最長的空閒時間大於5分鐘 或 空閒數 大於5,就移除關閉這個最長空閒連接;如果 空閒數 不大於5 且 最長的空閒時間不大於5分鐘,就返回到5分鐘的剩餘時間,然後等待這個時間再來清理。
  • 沒有空閒連接就等5分鐘後再嘗試清理。
  • 沒有連接不清理。

其中判斷連接正在使用的方法pruneAndGetAllocationCount我們來看下:

  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    //連接上的數據流,弱引用列表
    List<Reference<Transmitter>> references = connection.transmitters;
    for (int i = 0; i < references.size(); ) {
      Reference<Transmitter> reference = references.get(i);
      if (reference.get() != null) {
        i++;
        continue;
      }

      // 到這裏,transmitter是泄漏的,要移除,且此連接不能再承載新的數據流(泄漏的原因就是下面的message)
      TransmitterReference transmitterRef = (TransmitterReference) reference;
      String message = "A connection to " + connection.route().address().url()
          + " was leaked. Did you forget to close a response body?";
      Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace);
      references.remove(i);
      connection.noNewExchanges = true;

      //連接因爲泄漏沒有數據流了,那麼可以立即移除了。所以設置 開始空閒時間 是5分鐘前(厲害厲害!)
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }
    //返回連接上的數據流數量,大於0說明正在使用。
    return references.size();
  }

邏輯註釋已經標明瞭,很好理解。其中connection.transmitters,表示在此連接上的數據流,transmitters size大於1即表示多個請求複用此連接。

另外,在findConnection中,使用connectionPool.put(result)存連接後,又調用transmitter.acquireConnectionNoEvents方法,瞅下:

  void acquireConnectionNoEvents(RealConnection connection) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();
    this.connection = connection;
    connection.transmitters.add(new TransmitterReference(this, callStackTrace));
  }

先把連接賦給transmitter,表示數據流transmitter依附在這個connection上;然後connection.transmitters add 這個transmitter的弱引用,connection.transmitters表示這個連接承載的所有數據流,即承載的所有請求。

好了,存 講完了,主要就是把連接存入隊列,同時開始循環嘗試清理過期連接。

  //爲transmitter 從連接池 獲取 對應address的連接。若果routes不爲空,可能會因爲 連接合並(複用) 而獲取到HTTP/2連接。
  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;
  }

存的方法名是put,但你發現 取 的方法名卻不是get,transmitterAcquirePooledConnection意思是 爲transmitter 從連接池 獲取連接,實際上transmitter就代表一個數據流,也就是一個http請求。注意到,在遍歷中 經過判斷後也是transmitter的acquireConnectionNoEvents方法,即把匹配到的connection賦給transmitter。所以方法名還是很生動的。

繼續看是如何匹配的:如果requireMultiplexed爲false,即不是多路複用(不是http/2),那麼就要看Connection的isEligible方法了,isEligible方法返回true,就代表匹配成功:

  //用於判斷 連接 是否 可以承載指向address的數據流
  boolean isEligible(Address address, @Nullable List<Route> routes) {
    //連接不再接受新的數據流,false
    if (transmitters.size() >= allocationLimit || noNewExchanges) return false;

    //匹配address中非host的部分
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    //匹配address的host,到這裏也匹配的話,就return true
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }

    //到這裏hostname是沒匹配的,但是還是有機會返回true:連接合並
    // 1. 連接須是 HTTP/2.
    if (http2Connection == null) return false;

    // 2. IP 地址匹配
    if (routes == null || !routeMatchesAny(routes)) return false;

    // 3. 證書匹配
    if (address.hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;

    // 4. 證書 pinning 匹配.
    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.
  }

  private boolean routeMatchesAny(List<Route> candidates) {
    for (int i = 0, size = candidates.size(); i < size; i++) {
      Route candidate = candidates.get(i);
      if (candidate.proxy().type() == Proxy.Type.DIRECT
          && route.proxy().type() == Proxy.Type.DIRECT
          && route.socketAddress().equals(candidate.socketAddress())) {
        return true;
      }
    }
    return false;
  }

取的過程就是 遍歷連接池,進行地址等一系列匹配,到這裏第三個問號也解決了。

  //移除關閉空閒連接
  public void evictAll() {
    List<RealConnection> evictedConnections = new ArrayList<>();
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();
        if (connection.transmitters.isEmpty()) {
          connection.noNewExchanges = true;
          evictedConnections.add(connection);
          i.remove();
        }
      }
    }

    for (RealConnection connection : evictedConnections) {
      closeQuietly(connection.socket());
    }
  }

遍歷連接池,如果連接上的數據流是空,那麼就從連接池移除並且關閉。

我們回過頭看下Transmitter的releaseConnectionNoEvents方法,也就第一個問號,如果連接不再接受新的數據流,就會調用這個方法:

  //從連接上移除transmitter.
  @Nullable Socket releaseConnectionNoEvents() {
    assert (Thread.holdsLock(connectionPool));

    int index = -1;
    //遍歷 此數據流依附的連接 上的所有數據流,找到index
    for (int i = 0, size = this.connection.transmitters.size(); i < size; i++) {
      Reference<Transmitter> reference = this.connection.transmitters.get(i);
      if (reference.get() == this) {
        index = i;
        break;
      }
    }

    if (index == -1) throw new IllegalStateException();
	//transmitters移除此數據流
    RealConnection released = this.connection;
    released.transmitters.remove(index);
    this.connection = null;
	//如果連接上沒有有數據流了,就置爲空閒(等待清理),並返回待關閉的socket
    if (released.transmitters.isEmpty()) {
      released.idleAtNanos = System.nanoTime();
      if (connectionPool.connectionBecameIdle(released)) {
        return released.socket();
      }
    }

    return null;
  }

主要就是嘗試釋放連接,連接上沒有數據流就關閉socket等待被清理。

好了,到這裏連接池的管理就分析完了。

從連接的查找 到 連接池的管理,就是ConnectInterceptor的內容了。

CallServerInterceptor

哎呀,終於到最後一個攔截器了!

請求服務攔截器,也就是真正地去進行網絡IO讀寫了——寫入http請求的header和body數據、讀取響應的header和body。

上面ConnectInterceptor主要介紹瞭如何 尋找連接 以及 連接池如何管理連接。在獲取到連接後,調用了RealConnection的newCodec方法ExchangeCodec實例,然後使用ExchangeCodec實例創建了Exchange實例傳入CallServerInterceptor了。上面提到過ExchangeCodec負責請求和響應的IO讀寫,我們先來看看ExchangeCodec創建過程——RealConnection的newCodec方法:

  ExchangeCodec newCodec(OkHttpClient client, Interceptor.Chain chain) throws SocketException {
    if (http2Connection != null) {
      return new Http2ExchangeCodec(client, this, chain, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1ExchangeCodec(client, this, source, sink);
    }
  }

http2Connection不爲空就創建Http2ExchangeCodec,否則是Http1ExchangeCodec。而http2Connection的創建是連接進行TCP、TLS握手的時候,即在RealConnection的connect方法中,具體就是connect方法中調用的establishProtocol方法:

  private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
    //針對http請求,如果配置的協議包含Protocol.H2_PRIOR_KNOWLEDGE,則開啓Http2連接
    if (route.address().sslSocketFactory() == null) {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        socket = rawSocket;
        protocol = Protocol.H2_PRIOR_KNOWLEDGE;
        startHttp2(pingIntervalMillis);
        return;
      }

      socket = rawSocket;
      protocol = Protocol.HTTP_1_1;
      return;
    }
	//針對https請求,會在TLS握手後,根據平臺獲取協議(),如果協議是Protocol.HTTP_2,則開啓Http2連接
    eventListener.secureConnectStart(call);
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }

  private void startHttp2(int pingIntervalMillis) throws IOException {
    socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
    http2Connection = new Http2Connection.Builder(true)
        .socket(socket, route.address().url().host(), source, sink)
        .listener(this)
        .pingIntervalMillis(pingIntervalMillis)
        .build();
    http2Connection.start();
  }

好了,到這裏不再深入了,繼續瞭解可以參考HTTP 2.0與OkHttp。那麼到這裏,ExchangeCodec已經創建了,然後又包裝成Exchange,最後傳入了CallServerInterceptor。

下面就來看看這最後一個攔截器:

public final class CallServerInterceptor implements Interceptor {
  private final boolean forWebSocket;

  public CallServerInterceptor(boolean forWebSocket) {
    this.forWebSocket = forWebSocket;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Exchange exchange = realChain.exchange();//上個攔截器傳入的exchange
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();
	//寫請求頭
    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    //含body的請求
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // 若請求頭包含 "Expect: 100-continue" , 就會等服務端返回含有 "HTTP/1.1 100 Continue"的響應,然後再發送請求body. 
      //如果沒有收到這個響應(例如收到的響應是4xx),那就不發送body了。
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true);
      }
	  //responseBuilder爲null說明服務端返回了100,也就是可以繼續發送body了
      if (responseBuilder == null) {
        if (request.body().isDuplex()) {//默認是false不會進入
          // 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 {
          // 滿足了 "Expect: 100-continue" ,寫請求body
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
       //沒有滿足 "Expect: 100-continue" ,請求發送結束
        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 {
     //沒有body,請求發送結束
      exchange.noRequestBody();
    }

	//請求發送結束
    if (request.body() == null || !request.body().isDuplex()) {
      exchange.finishRequest();
    }
	//回調 讀響應頭開始事件(如果上面沒有)
    if (!responseHeadersStarted) {
      exchange.responseHeadersStart();
    }
	//讀響應頭(如果上面沒有)
    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }
	//構建response
    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
      //這裏服務端又返回了個100,就再嘗試獲取真正的響應()
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }
	//回調讀響應頭結束
    exchange.responseHeadersEnd(response);
	//這裏就是獲取響應body了
    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();
    }
	//請求頭中Connection是close,表示請求完成後要關閉連接
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      exchange.noNewExchangesOnConnection();
    }
	//204(無內容)、205(充值內容),body應該是空
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
	
    return response;
  }
}

你會發現,整個內容就是前面說的一句話:寫入http請求的header和body、讀取響應的header和body。這裏就不再解釋了。

這裏我們可以看到,無論寫請求還是讀響應,都是使用Exchange對應的方法。上面也提到過Exchange理解上是對ExchangeCodec的包裝,這寫方法內部除了事件回調和一些參數獲取外,核心工作都由 ExchangeCodec 對象完成,而 ExchangeCodec實際上利用的是 Okio,而 Okio 實際上還是用的 Socket。

ExchangeCodec的實現類 有對應Http1.1的Http1ExchangeCodec 和 對應Http2.0的Http2ExchangeCodec。其中Http2ExchangeCodec是使用Http2.0中 數據幀 的概念完成請求響應的讀寫。關於Http1ExchangeCodec、Http2ExchangeCodec具體實現原理涉及okio這不再展開。

最後一點,CallServerInterceptor的intercept方法中沒有調用連接器鏈Chain的proceed方法,因爲這是最後一個攔截器啦!

好了,到這裏最後一個攔截器也分析完啦!

總結

本篇分析了ConnectInterceptor、CallServerInterceptor兩個攔截器的作用和原理。ConnectInterceptor負責連接的獲取,其中涉及到連接池的概念;CallServerInterceptor是真正的網絡IO讀寫。ConnectInterceptor涉及的內容較多,它是Okhttp的核心。
結合上一篇,我們已經分析完了Okhttp內部所有的攔截器,最後給出Okhttp的整體架構圖(圖片來源):
okhttp

到這裏,Okhttp源碼解析部分就真的結束了,可真是一個漫長的過程!
從使用方式到工作流程,再到具體攔截器,掌握了這四篇文章的內容,應該說得上對Okhttp是比較熟悉了。這裏還計劃會出第五篇終章,來介紹一些Okhttp的常見問題和高級使用方式,敬請期待!

最後的最後,歡迎留言討論,如果你喜歡這一系列,或者覺得寫得還不錯,請幫忙 點贊、收藏和轉發,感謝

.
感謝與參考:
okhttp源碼解析
通過ConnectInterceptor源碼掌握OKHttp3網絡連接原理 嘔心瀝血第十彈【十】
OkHttp源碼深度解析
OkHttp源碼解析 (三)——代理和路由
OkHttp 源碼學習筆記(三) 數據交換的流 HTTPCodec

歡迎關注我的 公 衆 號
公衆號:胡飛洋

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章