原始碼分析三:OkHttp—ConnectInterceptor

楊昆發表於2018-03-13
ConnectInterceptor負責網路連線,其本質是複用連線池中socket連線。

開始擼原始碼:

intercept(攔截)

 @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    //從攔截器鏈裡得到StreamAllocation物件
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //建立連線等一系列的操作
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    //這裡是獲取前一步的connection.
    RealConnection connection = streamAllocation.connection();
   
    //把前面建立的連線,傳遞到下一個攔截器
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
複製程式碼

步驟詳細分析

基本步驟就上面展示了,程式碼看攔截這裡很簡單,我們理清楚一下幾個類的呼叫關係,來分析一下連線是如何一步步建立的:

  1. StreamAllocation
  2. ConnectionPool
  3. RealConnection

StreamAllocation

首先,StreamAllocation的初始化在第一個攔截器RetryAndFollowUpInterceptor裡面,

streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);
複製程式碼

傳入了三個引數,一個連線池,一個地址類,一個呼叫堆疊跟蹤相關的。

在StreamAllocation建構函式中,主要是把這個三個引數儲存為內部變數,供後面使用,還有一個就是同時建立了一個線路選擇器:

this.routeSelector = new RouteSelector(address, routeDatabase());
複製程式碼

該構造器裡面有兩個重要的引數:
1.使用了Okhttp的連線池ConnectionPool
2.通過url建立了一個Address物件。

Okhttp連線池簡單說明
本篇只是對連線池做最簡單的說明,內部的實現原理暫時不細講。在Okhttp內部的連線池實現類為ConnectionPool,該類持有一個ArrayDeque佇列作為快取池,該佇列裡的元素為RealConnection(通過這個名字應該不難猜出RealConnection是來幹嘛的)。

該連結池在初始化OkhttpClient物件的時候由OkhttpClient的Builder類建立,並且ConnectionPool提供了put、get、evictAll等操作。但是Okhttp並沒有直接對連線池進行獲取,插入等操作;而是專門提供了一個叫Internal的抽象類來操作緩衝池:比如向緩衝池裡面put一個RealConnection,從緩衝池get一個RealConnection物件,該類裡面有一個public且為static的Internal型別的引用:

//抽象類
public abstract class Internal {
  public static Internal instance;
}1234複製程式碼

instance的初始化是在OkhttpClient的static語句塊完成的:

static {
    Internal.instance = new Internal() {
       //省略部分程式碼
    };
  }複製程式碼

newStream()方法

主要做了如下工作:

1)從緩衝池ConnectionPool獲取一個RealConnection物件,如果緩衝池裡面沒有就建立一個RealConnection物件並且放入緩衝池中,具體的說是放入ConnectionPool的ArrayDeque佇列中。

2)獲取RealConnection物件後並呼叫其connect(開啟Socket連結)。

下面具體分析:

public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    //1. 獲取設定的連線超時時間,讀寫超時的時間,以及是否進行重連。 
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
     // 2. 獲取健康可用的連線
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
     
     //3. 通過resultConnection初始化,對請求以及結果 編解碼的類(分http 1.1 和http 2.0)。
     // 這裡主要是初始化,在後面一個攔截器才用到這相關的東西。
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }
複製程式碼

獲取連線

在上面的程式碼中最重要的,是註釋 第二點,獲取健康可用的連線findHealthyConnection,那我們繼續深入:

findHealthyConnection()

 private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {

   // 1. 加了個死迴圈,一直找可用的連線
    while (true) {
     
      // 2. 這裡繼續去挖掘,尋找連線
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          connectionRetryEnabled);

     // 3. 連線池同步獲取,上面找到的連線是否是一個新的連線,如果是的話,就直接返回了,就是我們需要找
    // 的連線了
      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      //4.  如果不是一個新的連線,那麼通過判斷,是否一個可用的連線。
      // 裡面是通過Socket的一些方法進行判斷的,有興趣的,可以繼續研究一下
      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
  }
複製程式碼

上面的程式碼,重要的也是註釋的第二點:尋找連線。繼續看findConnection。

findConnection()

/**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {

    Route selectedRoute;
   
   // 1. 同步執行緒池,來獲取裡面的連線
    synchronized (connectionPool) {
        
         // 2.  異常的處理。做些判斷,是否已經釋放,是否編解碼類為空,是否使用者已經取消         if (released) throw new IllegalStateException("released");
         if (codec != null) throw new IllegalStateException("codec != null");
         if (canceled) throw new IOException("Canceled");
         
         // 3. 嘗試用一下現在的連線,判斷一下,是否有可用的連線。(使用已存在的連線)
         // Attempt to use an already-allocated connection.
          RealConnection allocatedConnection = this.connection;
         if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
            return allocatedConnection;
          }
          
         // 4. 嘗試在連線池中獲取一個連線,get方法中會直接呼叫,注意最後一個引數為空
         // 裡面是一個for迴圈,在連線池裡面,尋找合格的連線
         // 而合格的連線會通過,StreamAllocation中的acquire方法,更新connection的值。(從快取中獲取)      
         // Attempt to get a connection from the pool.
         Internal.instance.get(connectionPool, address, this, null);
         if (connection != null) {
             return connection;
          }

          selectedRoute = route;
    }
    //5. 判斷上面得到的線路,是否空,如果為空的,尋找一個可用的線路
    // 對於線路的選,可以深究一下這個RouteSeletor
    // 線路的選擇,多ip的支援       if (selectedRoute == null) {
      selectedRoute = routeSelector.next();// 裡面又個神奇的遞迴
    }

    RealConnection result;

    //6.  以上都不符合,建立一個連線
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");
   
      // 7. 由於上面我們獲取了一個線路,無論是新建的,或者已有的。
      // 我們通過這個線路,繼續在連線池中尋找是否有可用的連線。
      // Now that we have an IP address, make another attempt at getting a connection from the pool.
      // This could match due to connection coalescing.
      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) return connection;

      // 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.
      route = selectedRoute;
      refusedStreamCount = 0;
      
      // 8. 如果前面這麼尋找,都沒在連線池中找打可用的連線,那麼就新建一個
      result = new RealConnection(connectionPool, selectedRoute);
      acquire(result);
    }
    
    // 9. 這裡就是就是連線的操作了,終於找到連線的正主了,這裡會呼叫RealConnection的連線方法,進行連線操作。
    // 如果是普通的http請求,會使用Socket進行連線
    // 如果是https,會進行相應的握手,建立通道的操作。
    // 這裡就不對裡面的操作進行詳細分析了,有興趣可以在進去看看
    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    routeDatabase().connected(result.route());

    Socket socket = null;
    
   // 10. 最後就是同步加到 連線池裡面了
    synchronized (connectionPool) {
      // Pool the connection.
      Internal.instance.put(connectionPool, result);

      // 最後加了一個多路複用的判斷,這個是http2才有的
      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
  }
複製程式碼

建立連線

connect

建立連線是比較重要的一步了。如果是Https還有證照步驟

public void connect(
    int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
  if (protocol != null) throw new IllegalStateException("already connected");
// 線路的選擇
  RouteException routeException = null;
  List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
  ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

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

// 連線開始
  while (true) {
    try {
    // 如果要求通道模式,建立通道連線,通常不是這種
      if (route.requiresTunnel()) {
        connectTunnel(connectTimeout, readTimeout, writeTimeout);
      } else {
      // 一般都走這條邏輯了,實際上很簡單就是socket的連線
        connectSocket(connectTimeout, readTimeout);
      }
      // https的建立
      establishProtocol(connectionSpecSelector);
      break;
    } catch (IOException e) {
      closeQuietly(socket);
      closeQuietly(rawSocket);
      socket = null;
      rawSocket = null;
      source = null;
      sink = null;
      handshake = null;
      protocol = null;
      http2Connection = null;

      if (routeException == null) {
        routeException = new RouteException(e);
      } else {
        routeException.addConnectException(e);
      }

      if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
        throw routeException;
      }
    }
  }

  if (http2Connection != null) {
    synchronized (connectionPool) {
      allocationLimit = http2Connection.maxConcurrentStreams();
    }
  }
}複製程式碼

我們進入connectSocket()函式看看,

connectSocket

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

  // 根據代理型別處理socket,為true
  rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
      ? address.socketFactory().createSocket()
      : new Socket(proxy);

  rawSocket.setSoTimeout(readTimeout);
  try {
    // 連線socket,之所以這樣寫是因為支援不同的平臺
    /**
    * 裡面實際上是  socket.connect(address, connectTimeout);
    *
    */
    Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
  } catch (ConnectException e) {
    ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
    ce.initCause(e);
    throw ce;
  }
  // 得到輸入/輸出流
  source = Okio.buffer(Okio.source(rawSocket));
  sink = Okio.buffer(Okio.sink(rawSocket));
}複製程式碼

如果使用的是https協議,並且配置有真實將會協議升級。

Https協議的建立 connectTls

如果使用的是https協議socket連線完成後還有一步,就是Tls的處理

private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
  Address address = route.address();
  SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
  boolean success = false;
  SSLSocket sslSocket = null;
  try {
    // 在原來socket上加一層ssl
    // Create the wrapper over the connected socket.
    sslSocket = (SSLSocket) sslSocketFactory.createSocket(
        rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

    // Configure the socket's ciphers, TLS versions, and extensions.
    ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
    if (connectionSpec.supportsTlsExtensions()) {
      Platform.get().configureTlsExtensions(
          sslSocket, address.url().host(), address.protocols());
    }

    // Force handshake. This can throw!
    sslSocket.startHandshake();
    Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());

    // Verify that the socket's certificates are acceptable for the target host.
    if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
      X509Certificate cert = (X509Certificate) unverifiedHandshake.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));
    }

    // Check that the certificate pinner is satisfied by the certificates presented.
    address.certificatePinner().check(address.url().host(),
        unverifiedHandshake.peerCertificates());

    // Success! Save the handshake and the ALPN protocol.
    String maybeProtocol = connectionSpec.supportsTlsExtensions()
        ? Platform.get().getSelectedProtocol(sslSocket)
        : null;
    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) {
    if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
    throw e;
  } finally {
    if (sslSocket != null) {
      Platform.get().afterHandshake(sslSocket);
    }
    if (!success) {
      closeQuietly(sslSocket);
    }
  }
}複製程式碼

如果對https熟悉的話,應該知道,https就是在http的基礎上加了一層。

到此連結完成RealConnection例項化完成。


相關文章