3、深刻理解OkHttp:鏈接處理-ConnectIntercepter

1、前言

終於來到OkHttp的網絡鏈接模塊,這塊內容是OkHttp的核心內容。咱們知道Http的鏈接須要進行3此握手,斷開須要4次揮手。而鏈接的每一次握手,都須要進行Socket鏈接、釋放,這是一個很是麻煩並且耗時耗力的過程。那麼鏈接的服用就顯得尤其重要了,同個地址的鏈接,若是在用完後不斷開,保持鏈接,在下次的請求中便能重複使用這個鏈接,節省了鏈接的時間。這對於大部分時間須要重複頻繁訪問同一個服務器地址的移動端網絡來講更加不可或缺。java

在本篇文章中,咱們將以ConnectIntercepter爲起點,跟隨網絡鏈接獲取的過程,深刻探究其中涉及到的:鏈接查找、鏈接複用,網絡鏈接的創建(三次握手、Http2協議等的處理)。面對這複雜的過程,咱們先整體的走一遍鏈接獲取過程,而後在後續介紹 RealConnection.javaConnectionPool.java 來更深刻的理解鏈接的創建和緩存查找等邏輯。除此以外,咱們還須要先看一下另外一個類:Transmitter.java ,它將在connect的過程當中起到重要的地位。數據庫

2、Transmmiter:應用和Http的橋樑

【2.1】來歷和做用

RealCall.java
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.transmitter = new Transmitter(client, call);
    return call;
  }
 
Transmitter.java
  public final class Transmitter {
  
  private final OkHttpClient client;
  //重點:鏈接池
  private final RealConnectionPool connectionPool;
  //這次請求
  private final Call call;
  private Request request;
  //重點:鏈接查找器,它將承當主要的鏈接查找工做。
  private ExchangeFinder exchangeFinder;
  //Connecttion的實現類,表明着和服務器的鏈接。
  public RealConnection connection;
  //重點:負責請求的發送和響應接收
  private @Nullable Exchange exchange;
  //請求是否已取消
  private boolean canceled;
  ...

  public Transmitter(OkHttpClient client, Call call) {
    this.client = client;
    this.connectionPool = Internal.instance.realConnectionPool(client.connectionPool());
    this.call = call;
    this.eventListener = client.eventListenerFactory().create(call);
    this.timeout.timeout(client.callTimeoutMillis(), MILLISECONDS);
  }
  
複製代碼

總結:Transmitter是在建立RealCall的時候被建立的,其中須要了OkHttpClient和當前請求Call做爲參數。因此咱們知道了,一個請求對應着一個Transmitter。並且,它的成員變量裏有ExchangeFinder等類,負責爲這個請求查找到一個合適的請求。緩存

【2.2】 releaseConnectionNoEvents()

這個方法是釋放一個鏈接,該方法在後面的查找鏈接中會涉及到,咱們在這裏先對其進行講述。bash

Transmitter.java
@Nullable Socket releaseConnectionNoEvents() {
    ...
    int index = -1;
    //一個鏈接,能夠有多個transmitter,也就是用於多個請求。因此在這裏須要
    //找到本身的那一個。
    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();

    //將本身從鏈接中剔除掉。
    RealConnection released = this.connection;
    released.transmitters.remove(index);
    this.connection = null;

    //若是這個請求釋放了這個鏈接後,這個鏈接沒有被用於其餘請求
    //調用鏈接池,使這個鏈接變爲一個空閒鏈接。
    if (released.transmitters.isEmpty()) {
      released.idleAtNanos = System.nanoTime();
      //詳見【5.4】
      if (connectionPool.connectionBecameIdle(released)) {
        //沒人在用了,把Socket返回回去。
        return released.socket();
      }
    }
    
    //還有其餘請求在用,就不返回socket回去。
    return null;
  }
複製代碼

總結:這是一個請求關閉一個鏈接的過程。服務器

  1. 先找到鏈接中對直接的索引,斷開。
  2. 判斷鏈接是否還有請求在用,不用使其變爲空閒鏈接。

【2.1】prepareToConnect():鏈接的準備工做

【2.2.1】
RetryAndFollowUpInterceptor.java
@Override public Response intercept(Chain chain) throws IOException {
    ...
    Transmitter transmitter = realChain.transmitter();
    ...
    while (true) {
      transmitter.prepareToConnect(request);

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

      ...
    }
  }
複製代碼

從上面能夠看到,在執行第一個默認攔截器的邏輯的時候,調用transmitter.prepareToConnect()方法。咱們接下去看一下這個方法作了上面準備工做。cookie

【2.2.2】prepareToConnect()
Transmitter.java
 public void prepareToConnect(Request request) {
 
    if (this.request != null) {
    //若是這個Transmitter已經有了一個請求了
    //而且他們的url所指向的地址都是同一個,那麼這個鏈接能夠複用,直接返回。
      if (sameConnection(this.request.url(), request.url())) return; 
      //若是上個請求的信息交換器不爲空,表明此次request尚未結束
      //那麼拋出錯誤,該Transmitter不能給新的request用。
      if (exchange != null) throw new IllegalStateException();
        
      //釋放上次的鏈接。
      if (exchangeFinder != null) {
        maybeReleaseConnection(null, true);
        exchangeFinder = null;
      }
    }
    
    //第一次進來時,直接來到這裏。
    this.request = request;
    //給本身建立一個鏈接查找器,注意這裏的CreateAddress(),它將返回一個Adrees對象,表明着遠方服務器的一個地址。
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }

複製代碼

總結:其實這個方法,重點就是爲鏈接做準備。可是主要目的仍是找到能夠複用的鏈接。它的邏輯以下:網絡

  1. 若是這個Transmiiter以前已經有過請求,並且和新請求所指向的地址是同樣的,那麼這個鏈接能夠複用,直接返回。
  2. 若是不是同個地址,並且上個請求還沒用完(exchange != null),那麼這個Tranmitter不能複用。直接報錯。若是上個請求已經完了, 釋放上個請求的鏈接。
  3. 若是這個Tranmitter是新的,那麼給這個Transmiiter新建立一個ExcahgeFinder,請注意這個類,它很重要,將負責最主要的鏈接查找工做。

【2.2】acquireConnectionNoEvents

Transmitter.java
 void acquireConnectionNoEvents(RealConnection connection) {
    ...
    this.connection = connection;
    connection.transmitters.add(new TransmitterReference(this, callStackTrace));
  }
複製代碼

總結: 這個方法是表明Transmitter得到了一個可用的鏈接了。那麼它作的工做是將這個鏈接保存起來。而後將本身登記到RealConnection。這個方法後面會有用到,這裏先講解一下。數據結構

3、查找鏈接

有了章節二的預備知識後,咱們能夠來看ConnectIntercepter了。不過他只是觸發打開鏈接的按鈕,真正鏈接的查找和鏈接邏輯在exchangeFinder.java和Exchage.java。無論怎麼樣,咱們先來看一下開始的地方。併發

【3.1】ConnectIntercepterapp

ConnectIntercepter.java
@Override public Response intercept(Chain chain) throws IOException {
    ...
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //詳見【3.2】
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    return realChain.proceed(request, transmitter, exchange);
  }
複製代碼

調用transmitter的newExcahge()方法,獲得一個能夠與遠程地址進行通行的Exchage,而後就丟給下一個攔截器了。順帶說一下,在第一篇《》咱們知道,緊跟着ConnectIntercepter的下一個攔截器是ServerIntercepter,那咱們能夠很容易的推理出,它拿到了ConnectIntercepter的excahge後,就進行了數據傳輸和數據接收。

【3.2】newExchange()

Transmitter.java
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
    ...
    //詳見3.3:find()
    //詳見四:ExchangeCodec.java
    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;
    }
  }
複製代碼

調用exchangeFinder.find()找到一個鏈接,返回ExchangeCodec。ExchangeCodec是一個接口,它表明着Http請求的加密,和響應的解密。它有2個具體實現:Http1ExchangeCodec和Http2ExchangeCodec,它的詳細內容詳見【4】。咱們繼續看鏈接的查找。

【3.4】find()

ExcahgeFinder.java
 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 {
      //詳見【3.5】
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      //詳見【3.7】      
      return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
      trackFailure();
      throw e;
    } catch (IOException e) {
      trackFailure();
      throw new RouteException(e);
    }
  }
複製代碼

【3.5】findHealthyConnection()

ExcahgeFinder.java
  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      //詳見:【3.6】找到鏈接候選人
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // 若是這個鏈接是全新的,那麼能夠直接用
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // 在這裏須要檢查一下這個鏈接是否健康的
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
       //若是不加看,調用RealConnection.noNewExcahge()方法,將此鏈接丟棄並繼續找。
        candidate.noNewExchanges();
        continue;
      }

      return candidate;
    }
  }
複製代碼

總結: 該方法顧名思義,就是經過一個while(true)不斷的找一個鏈接候選人,而後檢查是否健康可用的,若是不能用就進行標記,丟棄。詳細的以下:

  1. 調用findConnection()找到一個鏈接。
  2. 若是全新的直接用。
  3. 若是不健康的調用RealConnection.noNewExcahge(),它內部主要作的是noNewExchanges = true; 這個標誌爲後續將會用到,用來丟棄鏈接。
  4. 不見看的判斷:isHealthy() 就不展開了,就是判斷connection的socket等是否被關閉了,進而判斷鏈接是否健康。

【3.6】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) {
      //1.若是這個請求已經被取消過,那麼再次請求拋出錯誤。
      if (transmitter.isCanceled()) throw new IOException("Canceled");
      ...
      
      //2. 先找到這個鏈接以前的路由結果
      Route previousRoute = retryCurrentRoute()
          ? transmitter.connection.route()
          : null;

      //3. 在這裏嘗試使用一個已經分配好的鏈接,可是如上文【3.5】看到的
      //他會檢查它的noNewExchange標誌爲,若是是true 的話,那麼這個鏈接不但不能用,並且還要複製給toClose,關閉掉。
      //詳見【2.3】:releaseConnectionNoEvents()
      releasedConnection = transmitter.connection;
      toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
          ? transmitter.releaseConnectionNoEvents()
          : null;

      //4.若是transmitter的connection歷經了上面的的邏輯,沒有被置空,說明這個鏈接可用,賦值給result。
      if (transmitter.connection != null) {
        result = transmitter.connection;
        releasedConnection = null;  //這個鏈接能夠用,不能把他釋放掉,從新置爲空。
      }

      //5. 若是此時的result仍是爲空,說明上面嘗試獲取一個已經分配好的鏈接失敗
      //那麼此次嘗試重鏈接池中獲取。
      if (result == null) {
        //詳見【5.3】:嘗試獲取一個鏈接
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
         //獲取成功
          foundPooledConnection = true;
          result = transmitter.connection;
        } else {
          //鏈接池都獲取失敗的話,須要進行路由
          selectedRoute = previousRoute;
        }
      }
    }
    //將剛剛要關閉的鏈接關閉。
    closeQuietly(toClose);

    ...
    //result不空,找到一個可用的鏈接,直接返回。
    if (result != null) {
      return result;
    }

    // 6.進行路由選擇
    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) {
        //7. 因爲有新的路由,用路由選擇的新的IP集合,再次此時到鏈接池中找能夠複用的鏈接。
        routes = routeSelection.getAll();
        if (connectionPool.transmitterAcquirePooledConnection(
            address, transmitter, routes, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        }
      }
       
      //8. 既然路由都沒有找到能夠用的,那麼就建立一個新的RealConnection,
      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }
        result = new RealConnection(connectionPool, selectedRoute);
        connectingConnection = result;
      }
    }

    // 9. 若是剛剛第二次在鏈接池找到了,那麼返回這個鏈接。
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // 10. 詳見【4.2】說明要用新鏈接,那麼進行TCP+TSL鏈接
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      connectingConnection = null;
      //11. 當多個鏈接鏈接到同一個主機時,在這裏會進行鏈接合併。這是最後一次嘗試
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
        //說明鏈接池中已經有一個可用的鏈接了,不須要剛剛建立的鏈接。
        pooled connection.
        result.noNewExchanges = true;
        socket = result.socket();
        result = transmitter.connection;
      } else {
        //12.詳見【5.5】新建立的鏈接正常使用,將它放入池子中
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);
      }
    }
    //若是有須要,丟掉剛剛新建立的鏈接
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    //終於能夠返回
    return result;
  }
複製代碼

總結:這是一個查找鏈接的過程,在查找的時候,綜合考慮了自身的鏈接,路由的結果,鏈接池的複用,和新建幾種方案。具體的以下:

  1. 嘗試獲取這個請求被已經被分配的鏈接,若是存在,且可用,那麼直接使用這個,返回。不然,這個鏈接的socket將會被關閉。
  2. 嘗試從鏈接池中獲取,若是可用,返回結果。
  3. 嘗試使用路由器進行路由選擇出路由結果集合,再次到鏈接池中進行查找可用鏈接。
  4. 若是路由都找不到,新建一個鏈接,進行TCP+TSL 鏈接。
  5. 再次到鏈接池中,看有沒有可用的鏈接,避免屢次鏈接形成重複建立。若是找到,關閉新建的鏈接。結果替換爲新找到的鏈接。若是沒有,將新鏈接放入鏈接池中。
  6. 返回結果。

【3.7】 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);
    }
  }
複製代碼

總結: 根據鏈接性質不同,生成不一樣的數據加解密器。

章節小結:本節從ConnectIntercepter開始,追尋了一個鏈接如何被得到的過程,它涉及到了新建鏈接、路由選擇,鏈接池複用等邏輯,最終的產物是Exchange,由它去到下一個攔截器:ServerIntercepter進行網絡傳輸工做。其中Exchange、RealConnectionPool起到了很重要角色,咱們將在下一小節中解析


4、RealConnection

RealConnection,描述的是一次與遠程服務器的鏈接,因此它須要具有與遠程地址進行創建鏈接,通行的能力。這些能裏咱們能夠在後續它的成員變量和方法中看出來。照例,咱們來看一下的構造函數和成員變量。

【4.1】成員變量和構造

public final class RealConnection extends Http2Connection.Listener implements Connection {
  ...
  private static final int MAX_TUNNEL_ATTEMPTS = 21;

  //鏈接池
  public final RealConnectionPool connectionPool;
  //路由器
  private final Route route;

  //這個socket將在connect()方法中被賦值,而且不會再從新賦值。它用於底層的Socket通訊。
  private Socket rawSocket;

  //表明着應用層的Socket
  private Socket socket;
  //描述一次完整握手過程的對象。
  private Handshake handshake;
  //協議枚舉類,包括「http/1.0」、「http/3.1」等。
  private Protocol protocol;
  //表明了一個Http2的Socket鏈接
  private Http2Connection http2Connection;
  //與服務器進行數據交互的流操做對象。
  private BufferedSource source;
  private BufferedSink sink;

  //表示connection的一個標誌位,被connectionPool管理着,而且一旦爲true,將一直爲true。表明着這個鏈接不須要新的Exchage了。
  boolean noNewExchanges;

  ...

  /** 這個鏈接所負載的請求 */
  final List<Reference<Transmitter>> transmitters = new ArrayList<>();
  ...

  //構造函數須要鏈接池和路由器。
  public RealConnection(RealConnectionPool connectionPool, Route route) {
    this.connectionPool = connectionPool;
    this.route = route;
  }
複製代碼

總結: 一些主要的成員變量已經如上列出註釋。接下來從它最重要的方法connect()入手來理解它的做用。

【4.2】 connect():與遠程服務器創建鏈接

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    //當portacol不等空時,表明鏈接已經創建,拋出錯誤。
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    //注意這裏的ConnectSpec對象,它表明了Http的Socket通訊的配置,好比它會指定TLS協議版本。
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    //1.對協議配置的一些檢查,若是配置不合法將會拋出錯誤
    //HTTP的話,判斷是否配置了不容許明文傳輸或者Android平臺規定了不容許明文傳輸。不知足的拋出錯誤。
    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    } else {
    //若是是Https鏈接,判斷是否配置h2_prior_knowledge。
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
      }
    }

    //從這裏開始鏈接。
    while (true) {
      try {
        //2.檢查是否須要隧道模式,若是須要就創建隧道鏈接。
        //若是目標地址是Https協議,可是又經過Http協議代理的話,將會知足斷定。
        if (route.requiresTunnel()) {
          //詳見【4.3】
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          //rawSocket爲空,表明不能創建隧道鏈接,退出。
          if (rawSocket == null) {
            break;
          }
        } else {
        //3.詳見【4.4】創建普通的Socket鏈接
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        //4. 詳見【4.5】創建協議。
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        ...
      }
    }

    //5. 對隧道鏈接創建失敗的處理
    if (route.requiresTunnel() && rawSocket == null) {
      ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
          + MAX_TUNNEL_ATTEMPTS);
      throw new RouteException(exception);
    }
    
    //若是是Http2協議,獲取最大併發流限制
    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }
複製代碼

總結:該方法是Connection處理鏈接邏輯的地方,主要包括一下幾點:

  1. 經過protocol來判斷這個鏈接是否已經創建好了,若是不爲空,就表明已經創建好,此時會拋出錯誤。
  2. 根據不一樣的Http鏈接協議,進行配置的檢查。
  3. 判斷是否創建隧道鏈接,是的話進入隧道鏈接流程。它的斷定條件是,HTTP代理的Http2或者Https。創建隧道鏈接的Http代理將再也不解析數據,而是直接轉發數據。
  4. 創建普通的Socket鏈接。
  5. 創建協議:TCL握手,HTTP/2的協商等。
  6. 對鏈接創建後或者失敗的一些處理

【4.3】connectTunnel():創建隧道鏈接

private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
      EventListener eventListener) throws IOException {
    //1. 建立用於隧道鏈接用的請求。
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
      //2. 詳見【4.4】和普通鏈接同樣,也須要進行Socket鏈接
      connectSocket(connectTimeout, readTimeout, call, eventListener);
      //3. 建立隧道
      tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

      if (tunnelRequest == null) break; // Tunnel successfully created.

      // The proxy decided to close the connection after an auth challenge. We need to create a new
      // connection, but this time with the auth credentials.
      closeQuietly(rawSocket);
      rawSocket = null;
      sink = null;
      source = null;
      eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
    }
  }
複製代碼

總結: 建立隧道鏈接,就是在Http代理的代理上創建Https鏈接。主要的作了以下事情:

  1. 建立一個tunnelRequest:一個經過代理建裏TLS隧道的請求。因爲是未加密的,它的頭部信息是隻包涵了最小的頭集,這也是爲了不傳遞一些敏感的數據給到Http,好比cookie等。
  2. 進行Socket鏈接。
  3. 進行隧道鏈接:傳入剛剛建立的tunnelRequset,構建出一個Http1ExchangeCodec對象,用於Http1協議的流操做實現類,對Http代理進行鏈接請求。

【4.4】connectSocket()

private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    //1. 根據不一樣的代理類型來選擇不一樣的Socket生成策略。
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    eventListener.connectStart(call, route.socketAddress(), proxy);
    //設置超時
    rawSocket.setSoTimeout(readTimeout);
    try {
    //2. 採用平臺上的鏈接socket方式
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ...
    }

   //獲得Socket的輸出輸入流
    try {
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
      if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
        throw new IOException(npe);
      }
    }
  }
複製代碼

總結: 該方法是與遠程服務器地址創建起Socket鏈接,並得到輸入輸出流。具體的以下:

  1. 若是是直連或者Http類型時,直接經過SocketFactory新建一個Socket。不然是代理Socket,將代理傳入Socket新建一個代理Socket。
  2. 創建Socket鏈接。Platform.get()在這裏獲得的是Android平臺,而它的內部作的是也就是
socket.connect(address, connectTimeout);
複製代碼

在這一步connect事後,socket完成了3次握手創建TCP鏈接。 3. 得到Socket的輸出輸入流。

####【4.5】establishProtocol()

private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
      //1. 判斷是否爲http請求
    if (route.address().sslSocketFactory() == null) {
      //2.若是http請求裏包涵了「h2_prior_knowledge」協議,表明是一個支持明文的http2請求,因此仍然開啓的是http2的鏈接
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        socket = rawSocket;
        protocol = Protocol.H2_PRIOR_KNOWLEDGE;
        //3. 創建http2鏈接
        startHttp2(pingIntervalMillis);
        return;
      }
      //4. 不屬於以上狀況,正常創建http鏈接
      socket = rawSocket;
      protocol = Protocol.HTTP_1_1;
      return;
    }

    eventListener.secureConnectStart(call);
    //5. 詳見【4.6】創建Tls協議
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);
    
    //創建http2鏈接
    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }
複製代碼

總結: 該方法根據請求協議,來肯定創建的鏈接是否須要進一步協議處理。具體的以下:

  1. 若是是http請求可是包涵「h2_prior_knowledge」或者是http2協議,都進一步構建htpp2鏈接。
  2. 其餘的爲正常的http請求,直接將表明底層的rawSocket賦值給應用層的socket。

【4.6】connectTls()

private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // 1. 將剛剛獲得的socket經過sslSocketFactory進行包裝
      //獲得SSLSocket對象。
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      // 2. 詳見【4.7】對sslSocket進行配置協議。
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      //3. 看狀況是否進行Tls擴展配置
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      //4. 開始進行三次握手
      sslSocket.startHandshake();
      SSLSession sslSocketSession = sslSocket.getSession();
      Handshake unverifiedHandshake = Handshake.get(sslSocketSession);

      //5. 對sslSocket的地址與主機地址進行校驗,確保一致可用。
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
        List<Certificate> peerCertificates = unverifiedHandshake.peerCertificates();
        if (!peerCertificates.isEmpty()) {
          X509Certificate cert = (X509Certificate) peerCertificates.get(0);
          throw new SSLPeerUnverifiedException(
              "Hostname " + address.url().host() + " not verified:"
                  + "\n certificate: " + CertificatePinner.pin(cert)
                  + "\n DN: " + cert.getSubjectDN().getName()
                  + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
        } else {
          throw new SSLPeerUnverifiedException(
              "Hostname " + address.url().host() + " not verified (no certificates)");
        }
      }

      //6. 證書校驗
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      //7. 在3中若是配置了進行擴展,那麼在這裏將會取到協議協商的結果。
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      
      //8. 將剛纔完成握手和協議校驗的sslSocket保存起來
      //而且得到用於IO傳輸的source、sink
      socket = sslSocket;
      source = Okio.buffer(Okio.source(socket));
      sink = Okio.buffer(Okio.sink(socket));
      handshake = unverifiedHandshake;
      protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
      success = true;
    } catch (AssertionError e) {
      ...
    } finally {
      ...
    }
  }
複製代碼

總結: 在這個方法裏,鏈接將進行SSL配置,三次握手,證書校驗等工做。具體的以下:

  1. 將socket包裝成SLLSocket。
  2. 對SLLSocket進行協議配置。
  3. 若是有須要,對SLL協議進行擴展配置。
  4. 開始三次握手。
  5. 對主機地址一致性進行校驗,防止握手過程當中丟失。
  6. 對服務器回傳回來的證書進行合法行校驗。
  7. 若是須要,取得握手過程當中,協議協商選擇出的協議。
  8. 將完成握手和協議校驗的SSLSocket保存起來,並得到用於IO傳輸的source、sink。

【4.7】configureSecureSocket() 對SSLScoket進行協議配置

ConnectionSpecSelector.java
 ConnectionSpec configureSecureSocket(SSLSocket sslSocket) throws IOException {
    ConnectionSpec tlsConfiguration = null;
    for (int i = nextModeIndex, size = connectionSpecs.size(); i < size; i++) {
      ConnectionSpec connectionSpec = connectionSpecs.get(i);
      if (connectionSpec.isCompatible(sslSocket)) {
        tlsConfiguration = connectionSpec;
        nextModeIndex = i + 1;
        break;
      }
    }
    ...
    Internal.instance.apply(tlsConfiguration, sslSocket, isFallback);
    return tlsConfiguration;
  }

複製代碼

總結: 能夠看到,對SSLScoket配置,就是遍歷connectionSpecs集合,而後挑出適合於這個sslScoket的配置,而後進行要用。具體的以下:

  1. connectionSpecs集合:在OkHttpClient建立的時候有默認值:
OkHttpClient.java
 static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
      ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
複製代碼
  1. 協議的應用最終會調用到ConnectionSpec.apply() 方法,對SSLScoket進行tsl版本,設置密碼套件。

【4.8】ConnectionSpec.apply(): 協議應用

ConnectionSpec.java
 void apply(SSLSocket sslSocket, boolean isFallback) {
    ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);

    if (specToApply.tlsVersions != null) {
      sslSocket.setEnabledProtocols(specToApply.tlsVersions);
    }
    if (specToApply.cipherSuites != null) {
      sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
    }
  }
複製代碼

總結: 對這個socket設置tls版本和密碼套件

【4.9】isEligible():判斷鏈接是否可複用的邏輯

RealConnection.java
boolean isEligible(Address address, @Nullable List<Route> routes) {
    // 若是這個鏈接所承載的請求達到最大,則不能重用
    if (transmitters.size() >= allocationLimit || noNewExchanges) return false;

    // 若是不是Host域,看他們地址是否徹底同樣。
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // Host域相同,返回能夠複用的結果。
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; 
    }

    //下面是Http2鏈接複用相關。
    ....
    
    return true; 
  }
複製代碼

總結: 這個方法在後續的解析中會涉及到,因此先放在這裏講了。主要是用來判斷這個鏈接可不能夠複用的。判斷條件如註釋。


5、ConnectiongPool:鏈接池

在3.6的findConnetion過程當中,咱們看到了不少次鏈接池的身影,它對鏈接的複用也起着絕對重要的位置,若是不仔細的理解它的話,查找鏈接這塊的邏輯就會少一大快。照例,從它的出生、成員變量和構造函數來初步認識它。

【5.1】RealConnection 的出生

OkHttpClient.Builder.java
public Builder() {
      ...
      connectionPool = new ConnectionPool();
    }
複製代碼

在Builder()裏建立默認的鏈接池。

public final class ConnectionPool {
  final RealConnectionPool delegate;
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.delegate = new RealConnectionPool(maxIdleConnections, keepAliveDuration, timeUnit);
  }
複製代碼

總結: 能夠看出到,ConectionPool才用代理模式,實際邏輯交給RealConnection()。5個最大空閒鏈接,每一個鏈接可保活5分鐘。

【5.2】成員變量和構造函數

public final class RealConnectionPool{
 //
 private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** 每一個地址可保持的最大空閒鏈接 */
  private final int maxIdleConnections;
  //鏈接的保活時間
  private final long keepAliveDurationNs;
  //鏈接清理任務
  private final Runnable cleanupRunnable = () -> {
    while (true) {
    //詳見【5.6】
      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) {
          }
        }
      }
    }
  };

  //鏈接集合,採用雙向鏈標數據結構
  private final Deque<RealConnection> connections = new ArrayDeque<>();
  
  //路由數據庫
  final RouteDatabase routeDatabase = new RouteDatabase();
  
  //清除任務執行標誌
  boolean cleanupRunning;

  /**
  *構造函數
  */
  public RealConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
    ...
  }
}
複製代碼

總結: 能夠看出,這個鏈接池是用來管理同個地址的鏈接的。它提供根據地址查找可用鏈接、清除鏈接等功能。接下來介紹一下它的幾個重要方法。

【5.3】transmitterAcquirePooledConnection():獲取鏈接

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;
      【詳見4.9】
      if (!connection.isEligible(address, routes)) continue;
      【詳見2.2】
      transmitter.acquireConnectionNoEvents(connection);
      return true;
    }
    return false;
  }
複製代碼

總結: 遍歷保存的鏈接,調用RealConnection.isEligible() 來判斷這個鏈接是否符合條件。將這個請求的Transmitter登記到RealConnection。

【5.4】connectionBecameIdle()

boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connection.noNewExchanges || maxIdleConnections == 0) {
      connections.remove(connection);
      return true;
    } else {
      //通知清理任務執行。
      notifyAll(); 
      connection limit.
      return false;
    }
  }
複製代碼

總結: 將一個鏈接變爲空閒鏈接。若是此時這個鏈接不可用的話,將鏈接從鏈接集合中移除,並返回true。若是還能夠,通知清理任務執行,並返回false。

【5.5】put()

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
複製代碼

總結: 該方法是將一個鏈接放入鏈接池中,而後執行清理任務,不過它會被堵塞住,直到【5.4】方法觸發。

【5.6】cleanup()

long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    synchronized (this) {
    //1. 遍歷鏈接池
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //2. 若是鏈接還在用,繼續遍歷
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        //3. 找出最長空閒時間和對於的鏈接
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

        //4. 清除空閒最長的鏈接,並且須要知足以下條件:
        //a. 空閒時間大於最大保活時間。
        //b. 空閒鏈接數大於最大空閒鏈接數
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // 清理不了,返回下次清理須要的時間
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 沒有空閒鏈接,返回keepAliveDuration時間,表明keepAliveDuration後再執行。
        return keepAliveDurationNs;
      } else {
        // 沒有空閒或者在用的鏈接,清理結束。
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // 已經清理了一個,會當即再執行清理任務。
    return 0;
  }
複製代碼

總結: 這是一個清理鏈接的方法,它作的使其以下:

  1. 遍歷鏈接,若是鏈接還在用跳過。
  2. 找出最長空閒時間和其鏈接。
  3. 若是知足條件下,將鏈接移除鏈接池。而後觸發再執行一次清理任務。
  4. 若是沒有找到,返回下次須要清理的時間或者-1表明結束清理。

小篇結:本篇是介紹OkHttp的網絡鏈接創建。開篇先介紹了Trasnmitter這一重要的類,隨後從ConnectIntercepter入手,深刻研究了鏈接Connection的獲取邏輯。在獲取的過程當中,咱們將到了鏈接緩存的處理。當獲取不到緩存的時候,便會新建一個全新的網絡鏈接,在這個過程當中會進行Http的3次握手等過程。在最後2小節中,分別介紹了在整個過程當中的中心類,被查找對象:RealConnection。和管理緩存Connection的ConnectionPool。最後以一張圖來總結這一過程

相關文章
相關標籤/搜索