三、深入理解OkHttp:連接處理-ConnectIntercepter

一、前言

【1.1】OkHttp系列其他篇章:

  1. 同步請求的實現(xiàn)流程
  2. 異步請求的實現(xiàn)流程
  3. 重要攔截器:CacheInterceptor 的解析
  4. 重要攔截器:ConnectInterceptor 的解析
  5. 重要攔截器:CallServerInterceptor 的解析

【1.2】陳述

終于來到OkHttp的網絡連接模塊,這塊內容是OkHttp的核心內容。我們知道Http的連接需要進行3此握手,斷開需要4次揮手。而連接的每一次握手,都需要進行Socket連接、釋放,這是一個非常麻煩而且耗時耗力的過程。那么連接的服用就顯得尤為重要了,同個地址的連接,如果在用完后不斷開,保持連接,在下次的請求中便能重復使用這個連接,節(jié)省了連接的時間。這對于大部分時間需要重復頻繁訪問同一個服務器地址的移動端網絡來說更加不可或缺。

在本篇文章中,我們將以ConnectIntercepter為起點,跟隨網絡連接獲取的過程,深入探究其中涉及到的:連接查找、連接復用,網絡連接的建立(三次握手、Http2協(xié)議等的處理)。面對這復雜的過程,我們先總體的走一遍連接獲取過程,然后在后續(xù)介紹 RealConnection.javaConnectionPool.java 來更深入的理解連接的建立和緩存查找等邏輯。除此之外,我們還需要先看一下另一個類:Transmitter.java ,它將在connect的過程中起到重要的地位。

二、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的實現(xiàn)類,代表著和服務器的連接。
  public RealConnection connection;
  //重點:負責請求的發(fā)送和響應接收
  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是在創(chuàng)建RealCall的時候被創(chuàng)建的,其中需要了OkHttpClient和當前請求Call作為參數。所以我們知道了,一個請求對應著一個Transmitter。而且,它的成員變量里有ExchangeFinder等類,負責為這個請求查找到一個合適的請求。

【2.2】 releaseConnectionNoEvents()

這個方法是釋放一個連接,該方法在后面的查找連接中會涉及到,我們在這里先對其進行講述。

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;

    //如果這個請求釋放了這個連接后,這個連接沒有被用于其他請求
    //調用連接池,使這個連接變?yōu)橐粋€空閑連接。
    if (released.transmitters.isEmpty()) {
      released.idleAtNanos = System.nanoTime();
      //詳見【5.4】
      if (connectionPool.connectionBecameIdle(released)) {
        //沒人在用了,把Socket返回回去。
        return released.socket();
      }
    }
    
    //還有其他請求在用,就不返回socket回去。
    return null;
  }

總結:這是一個請求關閉一個連接的過程。

  1. 先找到連接中對直接的索引,斷開。
  2. 判斷連接是否還有請求在用,不用使其變?yōu)榭臻e連接。

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

      ...
    }
  }

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

【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;
    //給自己創(chuàng)建一個連接查找器,注意這里的CreateAddress(),它將返回一個Adrees對象,代表著遠方服務器的一個地址。
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }

總結:其實這個方法,重點就是為連接作準備。但是主要目的還是找到可以復用的連接。它的邏輯如下:

  1. 如果這個Transmiiter之前已經有過請求,而且和新請求所指向的地址是一樣的,那么這個連接可以復用,直接返回。
  2. 如果不是同個地址,而且上個請求還沒用完(exchange != null),那么這個Tranmitter不能復用。直接報錯。如果上個請求已經完了, 釋放上個請求的連接。
  3. 如果這個Tranmitter是新的,那么給這個Transmiiter新創(chuàng)建一個ExcahgeFinder,請注意這個類,它很重要,將負責最主要的連接查找工作。

【2.2】acquireConnectionNoEvents

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

總結: 這個方法是代表Transmitter獲得了一個可用的連接了。那么它做的工作是將這個連接保存起來。然后將自己登記到RealConnection。這個方法后面會有用到,這里先講解一下。

三、查找連接

有了章節(jié)二的預備知識后,我們可以來看ConnectIntercepter了。不過他只是觸發(fā)打開連接的按鈕,真正連接的查找和連接邏輯在exchangeFinder.java和Exchage.java。不管怎么樣,我們先來看一下開始的地方。

【3.1】ConnectIntercepter

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個具體實現(xiàn):Http1ExchangeCodec和Http2ExchangeCodec,它的詳細內容詳見【4】。我們繼續(xù)看連接的查找。

【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()方法,將此連接丟棄并繼續(xù)找。
        candidate.noNewExchanges();
        continue;
      }

      return candidate;
    }
  }

總結: 該方法顧名思義,就是通過一個while(true)不斷的找一個連接候選人,然后檢查是否健康可用的,如果不能用就進行標記,丟棄。詳細的如下:

  1. 調用findConnection()找到一個連接。
  2. 如果全新的直接用。
  3. 如果不健康的調用RealConnection.noNewExcahge(),它內部主要做的是noNewExchanges = true; 這個標志為后續(xù)將會用到,用來丟棄連接。
  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. 既然路由都沒有找到可以用的,那么就創(chuàng)建一個新的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)) {
        //說明連接池中已經有一個可用的連接了,不需要剛剛創(chuàng)建的連接。
        pooled connection.
        result.noNewExchanges = true;
        socket = result.socket();
        result = transmitter.connection;
      } else {
        //12.詳見【5.5】新創(chuàng)建的連接正常使用,將它放入池子中
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);
      }
    }
    //如果有需要,丟掉剛剛新創(chuàng)建的連接
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    //終于可以返回
    return result;
  }

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

  1. 嘗試獲取這個請求被已經被分配的連接,如果存在,且可用,那么直接使用這個,返回。否則,這個連接的socket將會被關閉。
  2. 嘗試從連接池中獲取,如果可用,返回結果。
  3. 嘗試使用路由器進行路由選擇出路由結果集合,再次到連接池中進行查找可用連接。
  4. 如果路由都找不到,新建一個連接,進行TCP+TSL 連接。
  5. 再次到連接池中,看有沒有可用的連接,避免多次連接造成重復創(chuàng)建。如果找到,關閉新建的連接。結果替換為新找到的連接。如果沒有,將新連接放入連接池中。
  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);
    }
  }

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

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


四、RealConnection

RealConnection,描述的是一次與遠程服務器的連接,所以它需要具備與遠程地址進行建立連接,通行的能力。這些能里我們可以在后續(xù)它的成員變量和方法中看出來。照例,我們來看一下的構造函數和成員變量。

【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;
  //協(xié)議枚舉類,包括“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協(xié)議版本。
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    //1.對協(xié)議配置的一些檢查,如果配置不合法將會拋出錯誤
    //HTTP的話,判斷是否配置了不允許明文傳輸或者Android平臺規(guī)定了不允許明文傳輸。不滿足的拋出錯誤。
    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協(xié)議,但是又通過Http協(xié)議代理的話,將會滿足判定。
        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】建立協(xié)議。
        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協(xié)議,獲取最大并發(fā)流限制
    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

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

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

【4.3】connectTunnel():建立隧道連接

private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
      EventListener eventListener) throws IOException {
    //1. 創(chuàng)建用于隧道連接用的請求。
    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. 創(chuàng)建隧道
      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);
    }
  }

總結: 創(chuàng)建隧道連接,就是在Http代理的代理上建立Https連接。主要的做了如下事情:

  1. 創(chuàng)建一個tunnelRequest:一個通過代理建里TLS隧道的請求。由于是未加密的,它的頭部信息是只包涵了最小的頭集,這也是為了避免傳遞一些敏感的數據給到Http,比如cookie等。
  2. 進行Socket連接。
  3. 進行隧道連接:傳入剛剛創(chuàng)建的tunnelRequset,構建出一個Http1ExchangeCodec對象,用于Http1協(xié)議的流操作實現(xiàn)類,對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連接。

  1. 獲得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”協(xié)議,代表是一個支持明文的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協(xié)議
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);
    
    //建立http2連接
    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }

總結: 該方法根據請求協(xié)議,來確定建立的連接是否需要進一步協(xié)議處理。具體的如下:

  1. 如果是http請求但是包涵“h2_prior_knowledge”或者是http2協(xié)議,都進一步構建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進行配置協(xié)議。
      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中如果配置了進行擴展,那么在這里將會取到協(xié)議協(xié)商的結果。
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      
      //8. 將剛才完成握手和協(xié)議校驗的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進行協(xié)議配置。
  3. 如果有需要,對SLL協(xié)議進行擴展配置。
  4. 開始三次握手。
  5. 對主機地址一致性進行校驗,防止握手過程中丟失。
  6. 對服務器回傳回來的證書進行合法行校驗。
  7. 如果需要,取得握手過程中,協(xié)議協(xié)商選擇出的協(xié)議。
  8. 將完成握手和協(xié)議校驗的SSLSocket保存起來,并獲得用于IO傳輸的source、sink。

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

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創(chuàng)建的時候有默認值:
OkHttpClient.java
 static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
      ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
  1. 協(xié)議的應用最終會調用到ConnectionSpec.apply() 方法,對SSLScoket進行tsl版本,設置密碼套件。

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

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

總結: 這個方法在后續(xù)的解析中會涉及到,所以先放在這里講了。主要是用來判斷這個連接可不可以復用的。判斷條件如注釋。


五、ConnectiongPool:連接池

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

【5.1】RealConnection 的出生

OkHttpClient.Builder.java
public Builder() {
      ...
      connectionPool = new ConnectionPool();
    }

在Builder()里創(chuàng)建默認的連接池。

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 {
            //等待喚醒執(zhí)行清理任務。
            RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };

  //連接集合,采用雙向鏈標數據結構
  private final Deque<RealConnection> connections = new ArrayDeque<>();
  
  //路由數據庫
  final RouteDatabase routeDatabase = new RouteDatabase();
  
  //清除任務執(zhí)行標志
  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 {
      //通知清理任務執(zhí)行。
      notifyAll(); 
      connection limit.
      return false;
    }
  }

總結: 將一個連接變?yōu)榭臻e連接。如果此時這個連接不可用的話,將連接從連接集合中移除,并返回true。如果還可以,通知清理任務執(zhí)行,并返回false。

【5.5】put()

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

總結: 該方法是將一個連接放入連接池中,然后執(zhí)行清理任務,不過它會被堵塞住,直到【5.4】方法觸發(fā)。

【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. 如果連接還在用,繼續(xù)遍歷
        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后再執(zhí)行。
        return keepAliveDurationNs;
      } else {
        // 沒有空閑或者在用的連接,清理結束。
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // 已經清理了一個,會立即再執(zhí)行清理任務。
    return 0;
  }

總結: 這是一個清理連接的方法,它做的使其如下:

  1. 遍歷連接,如果連接還在用跳過。
  2. 找出最長空閑時間和其連接。
  3. 如果滿足條件下,將連接移除連接池。然后觸發(fā)再執(zhí)行一次清理任務。
  4. 如果沒有找到,返回下次需要清理的時間或者-1代表結束清理。

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

image
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。