ConnectInterceptor連接攔截器分析
源碼地址:https://github.com/square/okhttp
經過前幾個攔截器的預熱,終於來到了攔截器的重頭戲了,連接相關的攔截器。這個也耗費了較多時間去準備。(代碼較多,擼代碼請慎重)
在分析第一個攔截器中RetryAndFollowUpInterceptor,我們知道,當時初始化了一個StreamAllocation的連接對象,也提供了一些對連接對象操作的方法,如取消連接等,但是卻沒有立刻的做連接,只是一直把這個對象往下傳遞。而在各種初始化之後(Gzip, Header, 以及cookie的處理攔截器,緩存攔截器),再進行連接操作。
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 類,而這個類,創建時候傳入了一個ConnectionPool,以及地址相關信息
StreamAllocation streamAllocation = realChain.streamAllocation();
//通過streamAllocation ,newStream,這個裏面會創建連接等一系列的操作。
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
//這裏是是獲取前一步的connection.
RealConnection connection = streamAllocation.connection();
//這裏是把前面創建的連接,傳遞到下一個攔截器
return realChain.proceed(request, streamAllocation, httpCodec, connection);
步驟詳細分析
PS: 源代碼較多,大部分分析會在代碼以註釋的形式存在。
基本步驟就上面展示了,我們理清楚以下幾個類的調用關係,來分析一下連接是如何一步步建立的:
- StreamAllocation
- ConnectionPool
- RealConnection
1. StreamAllocation實體
首先,StreamAllocation的初始化在第一個攔截器裏面,
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);
傳入了三個參數,一個連接池,一個地址類,一個調用堆棧跟蹤相關的。
在StreamAllocation構造函數中,主要是把這個三個參數保存爲內部變量,供後面使用,還有一個就是同時創建了一個線路選擇器:
this.routeSelector = new RouteSelector(address, routeDatabase());
用於後面選擇線路使用。
2. newStream()方法
StreamAllocation 的 newStream()是一個建立連接的重要方法,接下來就是一步步對裏面的代碼擼一擼:
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);
}
}
在上面的代碼中最重要的,是註釋 第二點,獲取健康可用的連接,那我們繼續深入:
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;
}
}
上面的代碼,重要的也是註釋的第二點:繼續去挖掘,尋找連接, 我們一直在找連接,但是到現在爲止,都是還沒到真正的連接部分 ~~#!
我們繼續擼啊擼:(其實你看到下面一大段代碼,你就知道,其實應該就是我們要找的地方了)
/**
* 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
// If we need a route, make one. This is a blocking operation.
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;
}
總結一下,上面的代碼分爲以下幾步:
- 前置做些判斷,是否已經釋放,是否編解碼類爲空,是否用戶已經取消;
- 嘗試用一下現在的連接,判斷一下,是否有可用的連接,有就返回;
- 嘗試在連接池中獲取一個連接(線路爲空);
- 獲取線路;
- 通過獲取到的線路,再去連接池取,是否有可用連接,有就返回;
- 前面都找不到可用連接,新建一個;
- 對新建的連接,進行連接操作(Socket);
- 把剛新建的連接,丟到連接池裏面。
到這裏爲止,我們就已經獲取到了一個連接了,這個連接攔截器的主要功能其實已經達到了。
迴歸到攔截器,下一個方法:streamAllocation.connection()。其實這個非常簡單,就是獲取前面創建的 realConnection而已。
最後的最後,就是把我們的StreamAlloaction, RealConnection, 以及新建的HttpCodec(請求,結果編解碼類),傳遞到下一個攔截器去。
總結:
這是很重要的一個攔截器,這裏面把連接建立起來了。同時新建了一個編解碼的類,爲後面的數據交換讀取做了鋪墊。
其實分析還是比較粗糙的,有很多地方,還需要深入去解剖,也留下了一下學習的空間:
- RouteSelector,線路的選擇,是通過什麼來選擇線路的?
- 從連接池裏獲取已有的連接,是如何判斷它是否可用的?
- 新建連接,進行連接操作時候,http 和 https是有什麼差異的?
- http2的多路複用,是如何實現的?
系列(簡書地址):
OKhttp源碼學習(一)—— 基本請求流程
OKhttp源碼學習(二)—— OkHttpClient
OKhttp源碼學習(三)—— Request, RealCall
OKhttp源碼學習(四)—— RetryAndFollowUpInterceptor攔截器
OKhttp源碼學習(五)—— BridgeInterceptor攔截器
OKhttp源碼學習(六)—— CacheInterceptor攔截器
OKhttp源碼學習(八)——CallServerInterceptor攔截器
OKhttp源碼學習(九)—— 任務管理(Dispatcher)