OkHttp 原始碼剖析系列(四)——連線建立概述

N0tExpectErr0r發表於2020-01-03

你好,我是 N0tExpectErr0r,一名熱愛技術的 Android 開發

我的個人部落格:blog.N0tExpectErr0r.cn

OkHttp 原始碼剖析系列文章目錄:

OkHttp 原始碼剖析系列(一)——請求的發起及攔截器機制概述

OkHttp 原始碼剖析系列(二)——攔截器整體流程分析

OkHttp 原始碼剖析系列(三)——快取機制

OkHttp 原始碼剖析系列(四)——連線建立概述

OkHttp 原始碼剖析系列(五)——代理路由選擇

OkHttp 原始碼剖析系列(六)——連線複用機制及連線的建立

OkHttp 原始碼剖析系列(七)——請求的發起及響應的讀取

前面的文章分析了 OkHttp 中的快取機制,接下來讓我們繼續研究 OkHttp 在 ConnectInterceptor 中所進行的連線建立的相關原理。由於連線建立的過程涉及到很多在 OkHttp 中非常重要的機制,因此將分為多篇文章進行介紹,這篇文章主要是對連線建立的大體流程進行介紹。

連線建立流程概述

ConnectInterceptor.intercept 方法中真正實現了連線的建立的程式碼如下:

// 如果請求是GET格式,需要一些額外的檢查
boolean doExtensiveHealthChecks = !request.method().equals("GET");
Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
複製程式碼

根據上面的程式碼我們可以推測,這個 Exchange 類與我們的連線是有一些關係的,真正連線的建立過程在 transmitter.newExchange 中實現。

我們看到 transmitter.newExchange 方法:

/**
 * Returns a new exchange to carry a new request and response.
 */
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
        if (noMoreExchanges) {
            throw new IllegalStateException("released");
        }
        if (exchange != null) {
            throw new IllegalStateException("cannot make a new request because the previous response "
                    + "is still open: please call response.close()");
        }
    }
    // 尋找ExchangeCodec物件
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    // 通過找到的codec物件構建Exchange物件
    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 物件,之後通過這個 codec 物件構建了一個 Exchange 物件並返回

那麼什麼是 ExchangeCodec 物件呢?我們先看到 exchangeFinder.find 方法:

public ExchangeCodec find(
        OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();
    try {
        RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
                writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
        return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
        trackFailure();
        throw e;
    } catch (IOException e) {
        trackFailure();
        throw new RouteException(e);
    }
}
複製程式碼

可以看到這裡呼叫到了 findHealthyConnection 方法從而獲取 RealConnection 物件,看來這個就是我們的連線了,之後呼叫了 RealConnection.newCodec 方法獲取 ExchangeCodec 物件。

尋找可用連線

我們先看到 findHealthyConnection 方法:

/**
 * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
 * until a healthy connection is found.
 */
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
                                             int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
                                             boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
        RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
                pingIntervalMillis, connectionRetryEnabled);
        // If this is a brand new connection, we can skip the extensive health checks.
        synchronized (connectionPool) {
            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)) {
            candidate.noNewExchanges();
            continue;
        }
        return candidate;
    }
}
複製程式碼

可以看到這裡是一個迴圈,不斷地在呼叫 findConnection 方法尋找連線,若找不到 Healthy(可用)的連線,則繼續迴圈直到找到為止。

尋找連線

我們先看到 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,
                                      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
        if (transmitter.isCanceled()) throw new IOException("Canceled");
        hasStreamFailure = false; // This is a fresh attempt.
        
        // 嘗試使用之前已分配的連線,但可能該連線不能用來建立新的Exchange
        releasedConnection = transmitter.connection;
        // 如果當前的連線不能被用來建立新的Exchange,則將連線釋放並返回對應Socket準備close
        toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
                ? transmitter.releaseConnectionNoEvents()
                : null;
        if (transmitter.connection != null) {
            // 存在已分配的連線,將其置為result,並置releasedConnection為null
            result = transmitter.connection;
            releasedConnection = null;
        }
        if (result == null) {
            // 如果不存在已經分配的連線,則嘗試從連線池中獲取連線
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
                foundPooledConnection = true;
                result = transmitter.connection;
            } else if (nextRouteToTry != null) {
            	// 修改當前選擇路由為下一個路由
                selectedRoute = nextRouteToTry;
                nextRouteToTry = null;
            } else if (retryCurrentRoute()) {
           		// 如果當前Connection的路由應當重試,則將選擇的路由設定為當前路由
                selectedRoute = transmitter.connection.route();
            }
        }
    }
    closeQuietly(toClose);
    if (releasedConnection != null) {
        eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
    	// 如果已經找到了已分配的或從連線池中取出的Connection,則直接返回
        return result;
    }
    // 如果需要進行路由選擇,則進行一次路由選擇
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
        newRouteSelection = true;
        routeSelection = routeSelector.next();
    }
    List<Route> routes = null;
    synchronized (connectionPool) {
        if (transmitter.isCanceled()) throw new IOException("Canceled");
        if (newRouteSelection) {
            // 路由選擇過後如今有了一組IP地址,我們再次嘗試從連線池中獲取連線
            routes = routeSelection.getAll();
            if (connectionPool.transmitterAcquirePooledConnection(
                    address, transmitter, routes, false)) {
                foundPooledConnection = true;
                result = transmitter.connection;
            }
        }
        if (!foundPooledConnection) {
            if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }
            // 如果第二次嘗試從連線池獲取連線仍然失敗,則建立新的連線。
            result = new RealConnection(connectionPool, selectedRoute);
            connectingConnection = result;
        }
    }
    if (foundPooledConnection) {
		// 如果第二次嘗試從連線池獲取連線成功,則將其返回
		eventListener.connectionAcquired(call, result);
        return result;
    }
    // 執行TCP+TLS握手,這是個阻塞的過程
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
            connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());
    Socket socket = null;
    synchronized (connectionPool) {
        connectingConnection = null;
        // 最後一次嘗試從連線池中獲取連線,這種情況只可能在一個host下多個併發連線這種情況下
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
            // 如果成功拿到則關閉我們前面建立的連線的Socket,並返回連線池中的連線
            result.noNewExchanges = true;
            socket = result.socket();
            result = transmitter.connection;
        } else {
        	// 如果失敗則在連線池中放入我們剛剛建立的連線,並將其設定為transmitter中的連線
            connectionPool.put(result);
            transmitter.acquireConnectionNoEvents(result);
        }
    }
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    return result;
}
複製程式碼

這個尋找連線的過程是非常複雜的,主要是下列幾個步驟:

  1. 嘗試獲取 transmitter 中已經存在的連線,也就是當前 Call 之前建立的連線。
  2. 若獲取不到,則嘗試從連線池中呼叫 transmitterAcquirePooledConnection 方法獲取連線,傳入的 routes 引數為 null
  3. 若仍獲取不到連線,判斷是否需要路由選擇,如果需要,呼叫 routeSelector.next 進行路由選擇
  4. 如果進行了路由選擇,則再次嘗試從連線池中呼叫 transmitterAcquirePooledConnection 方法獲取連線,傳入的 routes 為剛剛路由選擇後所獲取的路由列表
  5. 若仍然獲取不到連線,則呼叫 RealConnection 的建構函式建立新的連線,並對其執行 TCP + TLS握手。
  6. TCP + TSL握手之後,會再次嘗試從連線池中通過 transmitterAcquirePooledConnection 方法獲取連線,這種情況只會出現在一個 Host 對應多個併發連線的情況下(因為 HTTP/2 支援了多路複用,使得多個請求可以併發執行,此時可能有其他使用該 TCP 連線的請求也建立了連線,就不需要重新建立了)。
  7. 若最後一次從連線池中獲取連線獲取成功,會釋放之前建立的連線的相關資源。
  8. 若仍獲取不到,則將該連線放入連線池,並將其設定為 transmitter 的連線。

可以看到,尋找連線的過程主要被分成了三種行為,分別是

  • 嘗試獲取 transmitter 中已經分配的連線
  • 嘗試從執行緒池中呼叫 transmitterAcquirePooledConnection 獲取連線
  • 建立新連線。

有點類似圖片載入的三級快取,顯然自上而下是越來越消耗資源的,因此 OkHttp 更偏向於前面直接能夠獲取到連線,尤其是嘗試從連線池進行獲取連線這一操作進行了三次

不過我們現在只是知道了大體流程,還有許多疑問沒有解開。比如路由選擇是怎樣的?OkHttp 中的連線池是如何實現的?連線的建立過程是如何實現的?等等疑問都還沒有解開,我們將在後續文章中介紹到。

判斷連線是否可用

我們接著看看 RealConnection.isHealthy 的實現,看看它是如何判斷一個連線是否可用的:

/**
 * Returns true if this connection is ready to host new streams.
 */
public boolean isHealthy(boolean doExtensiveChecks) {
	// 判斷Socket是否可用
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
        return false;
    }
    // 如果包含Http/2的連線,檢測是否shutdown
    if (http2Connection != null) {
        return !http2Connection.isShutdown();
    }
    if (doExtensiveChecks) {
        try {
            int readTimeout = socket.getSoTimeout();
            try {
            	// 設定一秒延時,檢測Stream是否枯竭,若枯竭則該連線不可用
                socket.setSoTimeout(1);
                    if (source.exhausted()) {
                    return false; // Stream is exhausted; socket is closed.
                }
                return true;
            } finally {
                socket.setSoTimeout(readTimeout);
            }
        } catch (SocketTimeoutException ignored) {
            // Read timed out; socket is good.
        } catch (IOException e) {
            return false; // Couldn't read; socket is closed.
        }
    }
    return true;
}
複製程式碼

可以看到,上面主要是對 SocketHTTP/2連線Stream 進行了檢測,從而判斷該連線是否可用。

什麼是 Exchange

現在我們已經知道了連線究竟是如何尋找到的,現在讓我們回到 Exchange 類,讓我們研究一下究竟什麼是 Exchange,它是用來做什麼的。

讓我們先從它的 JavaDoc 看到:

Transmits a single HTTP request and a response pair. This layers connection management and events
on {@link ExchangeCodec}, which handles the actual I/O.
複製程式碼

可以看到,這裡講到,Exchange 是一個用於傳送 HTTP 請求和讀取響應的類,而真正進行 I/O 的類是它的一個成員變數——ExchangeCodec 。在 Exchange 中暴露了許多對 Stream 進行讀寫的方法,如 writeRequestHeaderscreateRequestBody 等等,在 CallServerInterceptor 中就會通過 Exchange ,向伺服器發起請求,並讀取其所返回的響應。

什麼是 ExchangeCodec

讓我們看看 ExchangeCodec 又是什麼:

/**
 * Encodes HTTP requests and decodes HTTP responses.
 */
public interface ExchangeCodec {
   
    int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
    
    RealConnection connection();
   
    Sink createRequestBody(Request request, long contentLength) throws IOException;
    
    void writeRequestHeaders(Request request) throws IOException;
  
    void flushRequest() throws IOException;
   
    void finishRequest() throws IOException;
   
    @Nullable
    Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;
   
   	long reportedContentLength(Response response) throws IOException;
    
    Source openResponseBodySource(Response response) throws IOException;
    
    Headers trailers() throws IOException;
   
    void cancel();
}
複製程式碼

可以看到,它僅僅是個介面,根據上面的 JavaDoc 可以看出,它的作用是用於對請求進行編碼,以及對響應進行解碼

我們看看它有哪些實現類,通過 Android Studio 我們可以很容易找到它有如下兩個實現類:

  • Http1ExchangeCodec
  • Http2ExchangeCodec

看得出來,OkHttp 採用了一種非常典型的面向介面程式設計,將對 Http 請求的編碼及解碼等功能抽象成了介面,再通過不同的實現類來實現將相同的 Request 物件編碼為 HTTP1 及 HTTP2 的格式的資料,將 HTTP1 及 HTTP2 格式的資料解碼為相同格式的 Response 物件。通過這樣的一種面向介面的設計,大大地提高了 OkHttp 的可擴充套件性,可以通過實現介面的形式對更多的應用層進行支援。

什麼是 Transmitter

接下來我們看看貫穿了我們整個請求流程的 Transimitter,究竟是一個用來做什麼的類。我們先從 JavaDoc 入手:

Bridge between OkHttp's application and network layers. This class exposes high-level application layer primitives: connections, requests, responses, and streams.

根據上面的註釋可以看出,Transmitter 是一座 OkHttp 中應用層與網路層溝通的橋樑。就像我們之前的連線建立,就是在應用層通過了 transmitter.newExchange 方法來通知網路層進行 Exchange 的獲取,並返回給應用層。那麼 Transmitter 是什麼時候建立的呢?

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;
}
複製程式碼

可以看到,它是在 RealCall 被建立的時候進行建立的,也就是說一個 Trasmitter 對應了一個 Call。這個 Call 在應用層通過 trasnmitter 與它的網路層進行通訊。

相關文章