OKHttp原始碼分析(六)連線管理 ConnectInterceptor 、StreamAllocation 和 RealConnection
ConnectInterceptor
作為CacheInterceptor
的後一個快取,已經沒有快取這條退路了,必須要真刀真槍的請求網路了。到了現在才真正開始請求網路獲取資料。從名字可以看出這個類是負責連線的。
看下具體攔截intercept方法的實現:
```
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// 建立HttpCodec和RealConnection
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
// 執行下一個攔截器
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
ConnectInterceptor
的攔截邏輯比較簡單,主要是準備RealConnection和HttpCodec,這兩個是請求的核心類。這兩個類都是通過StreamAllocation創建出來的,StreamAllocation在第一個攔截器
RetryAndFollowUpInterceptor就已經創建出來了。
在
ConnectInterceptor執行後,就達到了萬事俱備的條件(已經連線成功),那麼東風的到來就是到了最後一個攔截器
CallServerInterceptor中,這個攔截器內部才是請求網路的主戰場。這篇我們先分析
ConnectInterceptor和
StreamAllocation```。
讓我們先巨集觀看下這最後兩個攔截器和他們之中元件的功能。
巨集觀分析
我們先巨集觀分析下OKHttp的網路請求部分,OKHttp的執行在網路的應用層之上,主要是實現了Http的邏輯,那麼要使用網路請求,那麼肯定需要往下層走,也就是TCP/UDP的網路層,在安卓的程式碼中,怎麼使用這兩種請求呢,其實安卓的作業系統已經給我們實現好了,我們只要簡單的呼叫一下,傳入我們需要傳輸的Http資料報,自然就完成了剩下的所有的工作。
OKHttp在這方面的工作也是一樣的,單純的使用系統函式進行網路請求,誰都會,但是還要考慮效率和其他的一些應用,這就比較難了。OKHttp在這方面的做法從上面介紹的最後兩個攔截器中就可以看出,分為ConnectInterceptor
負責連線,CallServerInterceptor
負責傳輸資料。
連線過程
連線是什麼,這是一個非常抽象的詞。在網路中,就是指連線到伺服器,建立一條通道,Http底層TCP是需要提前進行連線的,也就是三次握手,完成雙發之間的約定,下面就可以正式的傳遞資料了。不建
立連線可否進行傳輸資料呢,UDP就是這個邏輯,但是不建立連線就不能保證可靠傳輸。連線需要進行三次握手,成功的建立是非常耗費資源的,所以能複用則複用,如果下次也是往這個伺服器傳送資料,當然是可以複用這個連線的。在OKHttp中,這個連線就是RealConnection
,而負責複用連線的類就是ConnectInterceptor
。
底層的連線時通過Socket#connect()
進行得。
傳輸過程
在上一步通過Socket#connect()
建立連線以後,我們就可以往輸入流中輸入資料了,就是這麼簡單的完成了網路資料的傳輸,等著輸出流的資料到來。至於底層的資料怎麼在鏈路上進行傳送,我們一概不用知道。CallServerInterceptor
就是負責這個功能的。在OkHttp中負責傳送資料的就是HttpCodec
,他內部通過okio封裝了Socket的輸入/輸出流,可以簡單的理解為一個流,一個OkHttp的流。
那StreamAllocation
的作用是什麼呢,因為連線過程的RealConnection
需要和傳輸過程的HttpCodec
進行通訊,StreamAllocation
相當於一箇中介,使用到了中介者模式,這樣連線和傳送就能更專心的去完成自己的工作了。負責協調兩者之間的工作,不但可以讓它們各自的核心邏輯封裝在各自的類內,不用關心和其他部分的通訊部分。
經過上面對整體的OkHttp網路請求的具體分析,應該對大概有了瞭解。下面我們先分析連線部分。下一篇再分析傳輸部分。 本篇先跳過Http2.0和Https的處理程式碼,先主要分析整個流程。
ConnectInterceptor
這裡我們先看下頂層ConnectInterceptor
的程式碼,從外部使用角度分析下StreamAllocation
和 RealConnection
兩個類。ConnectInterceptor攔截方法中,對StreamAllocation的外部呼叫很簡單。
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
先通過StreamAllocation的newSteam建立了HttpCodec,也就是資料傳輸的控制元件。後面直接呼叫了connection方法直接獲取了連線類RealConnection。
簡單的兩部連線和傳輸的兩大控制元件都已經建立好了。ConnectInterceptor就是這麼的簡單。下面我們先分析StreamAllocation。先通過newStream()和connection()為入口。
StreamAllocation
connection()的程式碼比較簡單,直接獲取了connection變數,可見在newStream()方法中就已經完成了連線和傳輸兩個功能控制元件建立的工作。主要的邏輯都在newStream()中。
public synchronized RealConnection connection() {
return connection;
}
newStream()具體程式碼如下。
```
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
。。。
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
直接呼叫了findHealthyConnection找到可以使用的RealConnection,再通過RealConnection的newCodec新建一個傳輸的控制元件HttpCodec。findHealthyConnection直接呼叫了findConnection方法,核心的邏輯都在findConnection中。
findConnection的方法比較長,總覽就是獲取快取Connection,和RecyclerView獲取快取有點像。獲取分為三步。
1. 看
StreamAllocation#connection是否可用
2. 沒有,從
ConnectionPool```獲取,這部分獲取分為兩步,首先不帶router獲取,再獲取router再次獲取
3. 沒有,建立一個新的Connection
是不是和RecyclerView獲取ViewHolder快取類似呢,只不過這裡是兩級快取,下面我們逐步分析下每個獲取步驟。
一級快取:本地StreamAllocation#connection是否可用
``` releasedConnection = this.connection; toClose = releaseIfNoNewStreams(); if (this.connection != null) { result = this.connection; releasedConnection = null; }
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
route = connection.route();
return result;
}
```
result變數就是最終的目標,如果result不為空,那麼表示我們已經找到了可用的連線,直接return即可。那這個連線是否可以複用,是通過releaseIfNoNewStreams這個方法判斷的,如果沒有被回收。那麼就是可以進行復用的。
下面看下StreamAllocation#connection的賦值來源。
StreamAllocation#connection的來源
StreamAllocation
中的connection物件,主要通過兩個方法進行賦值,releaseAndAcquire
和acquire
,前者是被Http2.0使用的,這裡主要分析acquire
。
```
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
this.reportedAcquired = reportedAcquired;
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
```
acquire的邏輯比較簡單,直接對connection進行賦值。並對allocations進行填充,因為一個連線可以有多個流,在Http1.1版本中,一個連線只可以有一個流,而Http2.0中一個連線可以有很多個流。如果在Http1.1中,connection.allocations這個集合只會有一個元素。
acquire呼叫又有兩個地方,一個是從ConnectionPool獲取成功後,一個是建立新的Connection後。我們發現這兩個部分都是在findConnection的後面才進行的。
一級快取生效條件
所以這個變數的重新利用,肯定是在第一次請求後,執行過findConnection後面的邏輯,找到真正可以使用的Connection後,並重復進行請求的場景下,進行的。比如在RetryAndFollowUpInterceptor
攔截器中的重試邏輯等。也就是肯定是在第二次重試中才會複用這個連線。
一級快取可用性判斷
主要邏輯在releaseIfNoNewStreams方法中:
private Socket releaseIfNoNewStreams() {
assert (Thread.holdsLock(connectionPool));
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && allocatedConnection.noNewStreams) {
return deallocate(false, false, true);
}
return null;
}
主要邏輯就在noNewStreams這個變數中,如果這個變數為true,表示這個連線上不能建立新的流了,那麼就會呼叫
deallocate進行回收,deallocate方法內部會置空connection變數,反之則不會進行回收。什麼場景下這個值會為true呢?
比如伺服器返回的資料中connection配置為close,那麼這個連線就需要關閉了,noNewStreams就為true。當然還有其他很多場景。
總結是:這裡指檢查了noNewStreams,如果不為true,就可以進行復用。
二級快取:ConnectionPool獲取
如果上面的Connection不能使用,就會從ConnectionPool中獲取,下面是獲取的主要程式碼。 ``` StreamAllocation.java if (result == null) { //只通過address進行匹配 Internal.instance.get(connectionPool, address, this, null); if (connection != null) { foundPooledConnection = true; result = connection; } else { selectedRoute = route; } }
boolean newRouteSelection = false; if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) { newRouteSelection = true; routeSelection = routeSelector.next(); }
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
List整體邏輯都是呼叫
Internal.instance.get(connectionPool, address, this, route);```從connectionPool獲取可以複用的Connection。但是獲取分為兩部
1. 只通過傳入的address匹配
2. 通過newRouteSelection判斷是否根據router和adress進行匹配
整體邏輯就是這樣,上面有兩個點,需要分析下
什麼是Router
Router翻譯過來表示路由,通過RouteSelector
進行獲取,內部包含三個變數
final Address address;
final Proxy proxy;
final InetSocketAddress inetSocketAddress;
| 欄位 | 意義 | | --- | --- | | proxy | 代理,表示連線的代理伺服器 | | address | 表示連線到源伺服器的規範,包括Url和協議和dns等 | | inetSocketAddress | IP地址 |
while (hasNextProxy()) {
Proxy proxy = nextProxy();
for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
Route route = new Route(address, proxy, inetSocketAddresses.get(i));
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
} else {
routes.add(route);
}
}
}
router的建立主要通過上面的程式碼進行獲取,inetSocketAddresses是從dns獲取的IP地址列表,而nextProxy()獲取下一個可用的程式碼。這裡通過雙層巢狀的迴圈,獲取了代理proxy和可選IP地址的笛卡爾集。
可以得到:Router表示連線到代理伺服器的途徑,通過不同的IP地址或者不同的代理。當一條router不可用時,就可以使用下一條router。
ConnectionPool匹配規則
主要通過get方法進行獲取
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
內部遍歷了快取的所有connections,之後通過connection.isEligible進行匹配,所有的匹配邏輯都在connection.isEligible中。如果匹配了,就通過上面談過的acquire方法繼續進行處理,
```
public boolean isEligible(Address address, @Nullable Route route) {
// 如果這個連線不接受新的流,不能複用
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// 如果地址的非主機欄位不重疊,不能複用
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// 如果主機完全匹配,可以複用
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
// 主機也不匹配,如果使用的是Http2.0協議 還有機會 if (http2Connection == null) return false; // Http2.0協議下,如果複用需要滿足的條件 if (route == null) return false; if (route.proxy().type() != Proxy.Type.DIRECT) return false; if (this.route.proxy().type() != Proxy.Type.DIRECT) return false; if (!this.route.socketAddress().equals(route.socketAddress())) return false;
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false; if (!supportsUrl(address.url())) return false;
try { address.certificatePinner().check(address.url().host(), handshake().peerCertificates()); } catch (SSLPeerUnverifiedException e) { return false; }
return true; // The caller's address can be carried by this connection. } ``` 如果正在使用的協議是Http2.0,複用的協議就更加的靈活了。分兩部分看下
協議共有判斷
- 快取的Connection,可以建立的流的數量不能超過
allocationLimit
限制,上面也說過這個變數,Http1.1只能建立一個流 Internal.instance.equalsNonHost
判讀請求的address和快取的address的變數,必須全部一致。包括以下變數。boolean equalsNonHost(Address that) { return this.dns.equals(that.dns) && this.proxyAuthenticator.equals(that.proxyAuthenticator) && this.protocols.equals(that.protocols) && this.connectionSpecs.equals(that.connectionSpecs) && this.proxySelector.equals(that.proxySelector) && equal(this.proxy, that.proxy) && equal(this.sslSocketFactory, that.sslSocketFactory) && equal(this.hostnameVerifier, that.hostnameVerifier) && equal(this.certificatePinner, that.certificatePinner) && this.url().port() == that.url().port(); }
- 如果上面的兩部通過了,判斷url的
host()
部分,如果一樣,說明這兩個Connection是可以進行復用的。 - 如果兩個host不一樣呢,還有補救的措施,就是使用的是Http2.0協議,並且滿足一些條件。
Http2.0獨有判斷
- 外部必須通過router進行匹配了,並且快取的代理和新請求的代理型別不能是直連線,也就是沒有代理。說明這種情況需要有代理的情況下使用。
- 後面包括了Https的相關處理,這裡先略過了,包括
certificatePinner
和hostnameVerifier
上面就是從ConnectionPool獲取快取的全部邏輯,那麼ConnectionPool什麼時候插入快取呢。
ConnectionPool插入時機
插入的程式碼在通過Socket傳送連線請求的下面,也就是說如果成功的進行了連線,才會插入到ConnectionPool。通過Internal.instance.put(connectionPool, result);
進行插入。
如果連線失敗了就不會插入了。
獲取快取失敗:新建Connection
上面兩部都沒有獲取可用的連線,只能建立一個新的連線了。 ``` if (selectedRoute == null) { selectedRoute = routeSelection.next(); }
route = selectedRoute; refusedStreamCount = 0; result = new RealConnection(connectionPool, selectedRoute); acquire(result, false); ``` 這裡選擇了第一個router,並建立了一個新的RealConnection。下面呼叫了acquire進行賦值。
通過上面的三步獲取,肯定拿到了合適的Connection了,下面就到了激動人心的時候了,正式開始連線。
連線過程
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
直接呼叫了RealConnection的connect方法。
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
EventListener eventListener) {
。。。
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
break;
}
} else {
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
。。。
}
省略了部分程式碼,核心邏輯就是這麼幾行
這裡判斷了是否需要隧道,OkHttp支援是使用Http隧道傳輸Https的資料。隧道的支援後面會說。
connectSocket內部直接呼叫了
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
呼叫安卓系統提供的rawSocket,直接使用Tcp請求網路。
establishProtocol方法內部判斷了是否使用Https和Http2.0,並提供了支援,這些部分專門後面會分析。
總結
縱觀整個連線的過程,底層的邏輯比較簡單,就是呼叫Socket的connect方法,但是這裡涉及到了連線的複用和路由等處理。通過上面的分析,應該對OKHttp怎麼建立連線,有了瞭解吧。 OkHttp連線和傳輸的3個主要重要的類分別是StreamAllocation 、RealConnection、HttpCodec。他們之間的關係如下
mermaid
graph TD
RealConnection --> StreamAllocation1
RealConnection --> StreamAllocation2
RealConnection --> StreamAllocation3
StreamAllocation1 --> HttpCodec1
StreamAllocation2 --> HttpCodec2
StreamAllocation3 --> HttpCodec3
也就是一個連線上可以建立多個流,當然只在Http2.0的情況下可以。StreamAllocation相當於一箇中介,連線兩者的關係,功能包括獲取一個連線,在連線上新建一個流。RealConnection就是一個連線,包括隧道、TSL、Http1.1、HTTP2.0的不同配置連線等。
我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿。
- OKHttp原始碼分析(六)連線管理 ConnectInterceptor 、StreamAllocation 和 RealConnection
- OkHttp原始碼分析(五)快取中樞CacheInterceptor
- OKHttp原始碼分析(四)BridgeInterceptor
- OkHttp原始碼分析(三)RetryAndFollowUpInterceptor
- RecyclerView原始碼分析(六)區域性重新整理
- RecyclerView原始碼分析(五)快取提取和回收
- RecyclerView原始碼分析(四)滑動機制分析 和 預載入
- RecycleView原始碼分析(四)LayoutManager原始碼分析
- RecyclerView原始碼分析(二)測繪流程下篇
- LeetCode二叉樹專題 (2) 相同的樹