OkHttp 源碼解析(二):建立連接

簡介

上一篇文章(OkHttp源碼解析(一):基本流程)介紹了 OkHttp 的基本流程,包括 Request 的創(chuàng)建、Dispatcher 對 Request 的調(diào)度以及 Interceptor 的使用。OkHttp 中默認會添加 RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor、ConnectInterceptor 以及 CallServerInterceptor 這幾個攔截器。本文主要看一下 RetryAndFollupInterceptor 并引出建立連接相關的分析。

RetryAndFollowUpInterceptor

Interceptor 最主要的代碼都在 intercept 中,下面是 RetryAndFollowUpInterceptor#intercept 中的部分代碼:

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

    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);  // 1

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      if (canceled) {
        streamAllocation.release();  // 2
        throw new IOException("Canceled");
      }
    ...
    response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null); // 3
    ...
}

上面注釋 1 處創(chuàng)建了一個 StreamAllocation 對象,注釋 2 處 調(diào)用了其 release 方法,注釋 3 處則把這個對象傳給了下一個 Interceptor。StreamAlloction 這個類很重要,下面就看一下它的用途。

StreamAlloction

StreamAllocation 從名字上看是流分配器,其實它是統(tǒng)籌管理了幾樣東西,注釋寫的非常清楚:

 /**
 * This class coordinates the relationship between three entities:
 *
 * <ul>
 *     <li><strong>Connections:</strong> physical socket connections to remote servers. These are
 *         potentially slow to establish so it is necessary to be able to cancel a connection
 *         currently being connected.
 *     <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on
 *         connections. Each connection has its own allocation limit, which defines how many
 *         concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
 *         at a time, HTTP/2 typically carry multiple.
 *     <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
 *         its follow up requests. We prefer to keep all streams of a single call on the same
 *         connection for better behavior and locality.
 * </ul>

簡單來說, StreamAllocation 協(xié)調(diào)了 3 樣東西:

  • Connections : 物理的 socket 連接
  • Streams:邏輯上的 HTTP request/response 對。每個 Connection 有個變量 allocationLimit ,用于定義可以承載的并發(fā)的 streams 的數(shù)量。HTTP/1.x 的 Connection 一次只能有一個 stream, HTTP/2 一般可以有多個。
  • CallsStreams 的序列。一個初始的 request 可能還會有后續(xù)的 request(如重定向)。OkHttp 傾向于讓一個 call 所有的 streams 運行在同一個 connection 上。

StreamAllocation 提供了一些 API 來釋放以上的資源對象。 在 RetryAndFollowUpInterceptor 中創(chuàng)建的 StreamAllocation 對象下一個用到的地方是 ConnectInterceptor,其 intercept 代碼如下:

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    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, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

在上面的代碼中, streamAllocation 創(chuàng)建了 httpCodec 以及 connection 對象。 httpCodec 即是上面所說的 Streams,而 connection 則是上面的 ConnectionConnection 是一個接口,它的唯一實現(xiàn)類是 RealConnection。

newStream

StreamAllocation 中的 newStream 方法用于尋找新的 RealConnection 以及 HttpCodec,代碼如下:

public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

newStream 中,通過 findHealthyConnection 找到可用的 Connection ,并用這個 Connection 生成一個 HttpCodec 對象。 findHealthyConnection 是找到一個健康的連接,代碼如下:

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

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
      // successCount == 0 表示還未使用過,則可以使用
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

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

public boolean isHealthy(boolean doExtensiveChecks) {
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
      return false;
    }
    ... // 省略 Http2 代碼
    return true;
  }

在一個無限循環(huán)中,通過 findConnection 尋找一個 connection,并判斷是否可用,首先如果沒有使用過的肯定是健康的可直接返回,否則調(diào)用 isHealthy,主要就是判斷 socket 是否關閉。這里的 socket 是在 findConnection 中賦值的,再看看 findConnection 的代碼:

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    Route selectedRoute;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // Attempt to use an already-allocated connection.
      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

      // Attempt to get a connection from the pool.
      // 1. 從 ConnectionPool 取得 connection
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        return connection;
      }

      selectedRoute = route;
    }

    // If we need a route, make one. This is a blocking operation.
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();
    }

    RealConnection result;
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      // Now that we have an IP address, make another attempt at getting a connection from the pool.
      // 2. 有了 ip 地址后再從 connectionpool中取一次
      // 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;
      // 3. ConnectionPool 中沒有,新創(chuàng)建一個
      result = new RealConnection(connectionPool, selectedRoute);
      // 3. 將 StreamAllocation 加入到 `RealConnection` 中的一個隊列中
      acquire(result);
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    // 4. 建立連接,在其中創(chuàng)建 socket
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      // Pool the connection.
      // 5. 將新創(chuàng)建的 connection 放到 ConnectionPool 中 
      Internal.instance.put(connectionPool, result);

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

上面 Connection 的創(chuàng)建大體是以下幾個步驟:

  1. 調(diào)用 Intenal.get 方法從 ConnectionPool 中獲取一個 Connection,主要根據(jù) url 的 host 判斷,相關代碼在 ConnectionPool 中。
  2. 如果沒有并且又獲取了 IP 地址,則再獲取一次。
  3. 如果 ConnectionPool 中沒有, 則新創(chuàng)建一個 RealConnection,并調(diào)用 acquireStreamAllocation 中加入 RealConnection 中的一個隊列中。
  4. 調(diào)用 RealConnection#connect 方法建立連接,在內(nèi)部會創(chuàng)建 Socket
  5. 將新創(chuàng)建的 Connection 加入到 ConnectionPool 中。

獲取到了 Connection 之后,再創(chuàng)建一個 HttpCodec 對象。

public HttpCodec newCodec(
      OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(client.readTimeoutMillis());
      source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
}

根據(jù)是 Http1 還是 Http2 創(chuàng)建對應的 HttpCodec, 其中的 socket 是在 RealConnection 中的 connect 方法創(chuàng)建的。下面具體看看RealConnection

RealConnection

RealConnection 封裝的是底層的 Socket 連接,內(nèi)部必然有一個 Socket 對象,下面是 RealConnection 內(nèi)部的變量:

public final class RealConnection extends Http2Connection.Listener implements Connection {
  private static final String NPE_THROW_WITH_NULL = "throw with null exception";
  private final ConnectionPool connectionPool;
  private final Route route;

  // The fields below are initialized by connect() and never reassigned.

  /** The low-level TCP socket. */
  private Socket rawSocket;

  /**
   * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
   * {@link #rawSocket} itself if this connection does not use SSL.
   */
  private Socket socket;
  private Handshake handshake;
  private Protocol protocol;
  private Http2Connection http2Connection;
  private BufferedSource source;
  private BufferedSink sink;

  // The fields below track connection state and are guarded by connectionPool.

  /** If true, no new streams can be created on this connection. Once true this is always true. */
  public boolean noNewStreams;

  public int successCount;

  /**
   * The maximum number of concurrent streams that can be carried by this connection. If {@code
   * allocations.size() < allocationLimit} then new streams can be created on this connection.
   */
  public int allocationLimit = 1;

  /** Current streams carried by this connection. */
  public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

  /** Nanotime timestamp when {@code allocations.size()} reached zero. */
  public long idleAtNanos = Long.MAX_VALUE;
  ...
}
  • Route 表示的是與服務端建立的路徑,其實內(nèi)部封裝了 Address,Address 則是封裝了請求的 URL。
  • rawSocket 對象代表底層的連接,還有一個 socket 是用于 Https, 對于普通的 Http 請求來說,這兩個對象是一樣的。 sourcesink 則是利用 Okio 封裝 socket 得到的輸入輸出流。(如果想了解 Okio 的原理,可以參考我之前的文章:Okio 源碼解析(一):數(shù)據(jù)讀取流程
  • noNewStream 對象用于標識這個 Connection 不能再用于 Http 請求了,一旦設置為 true, 則不會再變。
  • allocationLimit 指的是這個 Connection 最多能同時承載幾個 Http 流,對于 Http/1 來說只能是一個。
  • allocations 是一個 List 對象,里面保存著正在使用這個 ConnectionStreamAllocation 的弱引用,當 StreamAllocation 調(diào)用 acquire 時,便會將其弱引用加入這個 List,調(diào)用 release 則是移除引用。allocations 為空說明此 Connection 為閑置, ConnectionPool 利用這些信息來決定是否關閉這個連接。

connect

RealConnection 用于建立連接,里面有相應的 connect 方法:

public void connect(
      int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
    ...
    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout);
        } else {
          // 創(chuàng)建socket,建立連接
          connectSocket(connectTimeout, readTimeout);
        }
        // 建立
        establishProtocol(connectionSpecSelector);
        break;
      }
    ...
}

private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();
    // 創(chuàng)建 socket
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    rawSocket.setSoTimeout(readTimeout);
    try {
      // 建立連接,相當于調(diào)用 socket 的 connect 方法
      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;
    }
    
    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);
      }
    }
}

如果不是 Https, 則調(diào)用 connectSocket,在內(nèi)部創(chuàng)建 rawSocket 對象,設置超時時間。緊接著 Platform.get().connectSocket 根據(jù)不同的平臺調(diào)用相應的 connect 方法,這樣 rawSocket 就連接到服務端了。然后是用 Okio 封裝 rawSocket 的輸入輸出流,這里的輸入輸出流最終是交給 HttpCodec 進行 Http 報文的寫入都讀取。通過以上步驟,就實現(xiàn)了 Http 請求的連接。

總結

本文從 RetryAndFollowupIntercept 中創(chuàng)建 StreamAllocation 對象,到 Connection 中創(chuàng)建 RealConnectionHttpCodec,分析了 OkHttp 建立連接的基本過程。可以看出, OkHttp 中的連接由
RealConnection 封裝,Http 流的輸入輸出由 HttpCodec 操作,而 StreamAllocation 則統(tǒng)籌管理這些資源。在連接的尋找與創(chuàng)建過程,有個關鍵的東西是 ConnectionPool, 即連接池。它負責管理所有的 Connection,OkHttp 利用這個連接池進行 Connection 的重用以提高網(wǎng)絡請求的效率。本文并沒有詳細分析 ConnectionPool ,相關內(nèi)容可以參見下一篇: OkHttp源碼解析(三):連接池。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內(nèi)容