OKHttp原始碼分析(六)連線管理 ConnectInterceptor 、StreamAllocation 和 RealConnection

語言: CN / TW / HK

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中,這個攔截器內部才是請求網路的主戰場。這篇我們先分析ConnectInterceptorStreamAllocation```。
讓我們先巨集觀看下這最後兩個攔截器和他們之中元件的功能。

巨集觀分析

我們先巨集觀分析下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的程式碼,從外部使用角度分析下StreamAllocationRealConnection兩個類。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物件,主要通過兩個方法進行賦值,releaseAndAcquireacquire,前者是被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 routes = routeSelection.getAll(); for (int i = 0, size = routes.size(); i < size; i++) { Route route = routes.get(i); //只通過address+router進行匹配 Internal.instance.get(connectionPool, address, this, route); if (connection != null) { foundPooledConnection = true; result = connection; this.route = route; break; } } } 整體邏輯都是呼叫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,複用的協議就更加的靈活了。分兩部分看下

協議共有判斷
  1. 快取的Connection,可以建立的流的數量不能超過allocationLimit限制,上面也說過這個變數,Http1.1只能建立一個流
  2. 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(); }
  3. 如果上面的兩部通過了,判斷url的host()部分,如果一樣,說明這兩個Connection是可以進行復用的。
  4. 如果兩個host不一樣呢,還有補救的措施,就是使用的是Http2.0協議,並且滿足一些條件。
Http2.0獨有判斷
  1. 外部必須通過router進行匹配了,並且快取的代理和新請求的代理型別不能是直連線,也就是沒有代理。說明這種情況需要有代理的情況下使用。
  2. 後面包括了Https的相關處理,這裡先略過了,包括certificatePinnerhostnameVerifier

上面就是從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的不同配置連線等。

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿