1、OkHttp 的基本使用
OkHttp 是 Square 的一款應用於 Android 和 Java 的 Http 和 Http/2 客戶端。使用的時候只需要在 Gradle 裡面加入下面一行依賴即可引入:
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
複製程式碼
我們知道,Http 請求有多種型別,常用的分為 Get 和 Post,而 POST 又分為 Form 和 Multiple 等。下面我們以 Form 型別的請求為例來看下 OkHttp 的 API 設計邏輯:
OkHttpClient internalHttpClient = new OkHttpClient();
FormBody.Builder formBodyBuilder = new FormBody.Builder();
RequestBody body = formBodyBuilder.build();
Request.Builder builder = new Request.Builder().url("host:port/url").post(body);
Request request = builder.build();
Response response = internalHttpClient.newCall(request).execute();
String retJson = response.body().string();
複製程式碼
這裡我們先用了 FormBody
的構建者模式建立 Form 型別請求的請求體,然後使用 Request
的構建者建立完整的 Form 請求。之後,我們用建立好的 OkHttp 客戶端 internalHttpClient
來獲取一個請求,並從請求的請求體中獲取 Json 資料。
根據 OkHttp 的 API,如果我們希望傳送一個 Multipart 型別的請求的時候就需要使用 MultipartBody
的構建者建立 Multipart 請求的請求體。然後同樣使用 Request
的構建者建立完整的 Multipart 請求,剩下的邏輯相同。
除了使用上面的直接例項化一個 OkHttp 客戶端的方式,我們也可以使用 OkHttpClient
的構建者 OkHttpClient.Builder
來建立 OkHttp 客戶端。
所以,我們可以總結:
- OkHttp 為不同的請求型別都提供了一個構建者方法用來建立請求體
RequestBody
; - 因為請求體只是整個請求的一部分,所以,又要用
Request.Builder
構建一個請求物件Request
; - 這樣我們得到了一個完整的 Http 請求,然後使用
OkHttpClient
物件進行網路訪問得到響應物件Response
。
OkHttp 本身的設計比較友好,思路非常清晰,按照上面的思路搞懂了人家的 API 設計邏輯,自己再基於 OkHttp 封裝一個庫自然問題不大。
2、OkHttp 原始碼分析
上面我們提到的一些是基礎的 API 類,是提供給使用者使用的。這些類的設計只是基於構建者模式,非常容易理解。這裡我們關注點也不在這些 API 類上面,而是 OkHttp 內部的請求執行相關的類。下面我們就開始對 OkHttp 的請求過程進行原始碼分析(原始碼版本:3.10.0)。
2.1 一個請求的大致流程
參考之前的示例程式,拋棄構建請求的過程不講,單從請求的傳送過程來看,我們的線索應該從 OkHttpClient.newCall(Request)
開始。下面是這個方法的定義,它會建立一個 RealCall
物件,並把 OkHttpClient
物件和 Request
物件作為引數傳入進去:
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
複製程式碼
然後,RealCall 呼叫內部的靜態方法 newRealCall
在其中建立一個 RealCall
例項並將其返回:
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
複製程式碼
然後,當返回了 RealCall
之後,我們又會呼叫它的 execute()
方法來獲取響應結果,下面是這個方法的定義:
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
// 加入到一個雙端佇列中
client.dispatcher().executed(this);
// 從這裡拿的響應Response
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
複製程式碼
這裡我們會用 client
物件(實際也就是上面建立 RealCall
的時候傳入的 OkHttpClient
)的 dispatcher()
方法來獲取一個 Dispatcher
物件,並呼叫它的 executed()
方法來將當前的 RealCall
加入到一個雙端佇列中,下面是 executed(RealCall)
方法的定義,這裡的 runningSyncCalls
的型別是 Deque<RealCall>
:
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
複製程式碼
讓我們回到上面的 execute()
方法,在把 RealCall
加入到雙端佇列之後,我們又呼叫了 getResponseWithInterceptorChain()
方法,下面就是該方法的定義。
Response getResponseWithInterceptorChain() throws IOException {
// 新增一系列攔截器,注意新增的順序
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
// 橋攔截器
interceptors.add(new BridgeInterceptor(client.cookieJar()));
// 快取攔截器:從快取中拿資料
interceptors.add(new CacheInterceptor(client.internalCache()));
// 網路連線攔截器:建立網路連線
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
// 伺服器請求攔截器:向伺服器發起請求獲取資料
interceptors.add(new CallServerInterceptor(forWebSocket));
// 構建一條責任鏈
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
// 處理責任鏈
return chain.proceed(originalRequest);
}
複製程式碼
這裡,我們建立了一個列表物件之後把 client
中的攔截器、重連攔截器、橋攔截器、快取攔截器、網路連線攔截器和伺服器請求攔截器等依次加入到列表中。然後,我們用這個列表建立了一個攔截器鏈。這裡使用了責任鏈設計模式
,每當一個攔截器執行完畢之後會呼叫下一個攔截器或者不呼叫並返回結果。顯然,我們最終拿到的響應就是這個鏈條執行之後返回的結果。當我們自定義一個攔截器的時候,也會被加入到這個攔截器鏈條裡。
這裡我們遇到了很多的新類,比如 RealCall
、Dispatcher
以及責任鏈等。下文中,我們會對這些類之間的關係以及責任鏈中的環節做一個分析,而這裡我們先對整個請求的流程做一個大致的梳理。下面是這個過程大致的時序圖:
2.2 分發器 Dispatcher
上面我們提到了 Dispatcher
這個類,它的作用是對請求進行分發。以最開始的示例程式碼為例,在使用 OkHttp 的時候,我們會建立一個 RealCall
並將其加入到雙端佇列中。但是請注意這裡的雙端佇列的名稱是 runningSyncCalls
,也就是說這種請求是同步請求,會在當前的執行緒中立即被執行。所以,下面的 getResponseWithInterceptorChain()
就是這個同步的執行過程。而當我們執行完畢的時候,又會呼叫 Dispatcher
的 finished(RealCall)
方法把該請求從佇列中移除。所以,這種同步的請求無法體現分發器的“分發”功能。
除了同步的請求,還有非同步型別的請求:當我們拿到了 RealCall
的時候,呼叫它的 enqueue(Callback responseCallback)
方法並設定一個回撥即可。該方法會執行下面這行程式碼:
client.dispatcher().enqueue(new AsyncCall(responseCallback));
複製程式碼
即使用上面的回撥建立一個 AsyncCall
並呼叫 enqueue(AsyncCall)
。這裡的 AsyncCall
間接繼承自 Runnable
,是一個可執行的物件,並且會在 Runnable
的 run()
方法裡面呼叫 AsyncCall
的 execute()
方法。AsyncCall
的 execute()
方法與 RealCall
的 execute()
方法類似,都使用責任鏈來完成一個網路請求。只是後者可以放在一個非同步的執行緒中進行執行。
當我們呼叫了 Dispatcher
的 enqueue(AsyncCall)
方法的時候也會將 AsyncCall
加入到一個佇列中,並會在請求執行完畢的時候從該佇列中移除,只是這裡的佇列是 runningAsyncCalls
或者 readyAsyncCalls
。它們都是一個雙端佇列,並用來儲存非同步型別的請求。它們的區別是,runningAsyncCalls
是正在執行的佇列,當正在執行的佇列達到了限制的時候,就會將其放置到就緒佇列 readyAsyncCalls
中:
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
複製程式碼
當把該請求加入到了正在執行的佇列之後,我們會立即使用一個執行緒池來執行該 AsyncCall
。這樣這個請求的責任鏈就會在一個執行緒池當中被非同步地執行了。這裡的執行緒池由 executorService()
方法返回:
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
複製程式碼
顯然,當執行緒池不存在的時候會去建立一個執行緒池。除了上面的這種方式,我們還可以在構建 OkHttpClient
的時候,自定義一個 Dispacher
,並在其構造方法中為其指定一個執行緒池。下面我們類比 OkHttp 的同步請求繪製了一個非同步請求的時序圖。你可以通過將兩個圖對比來了解兩種實現方式的不同:
以上就是分發器 Dispacher
的邏輯,看上去並沒有那麼複雜。並且從上面的分析中,我們可以看出實際請求的執行過程並不是在這裡完成的,這裡只能決定在哪個執行緒當中執行請求並把請求用雙端佇列快取下來,而實際的請求執行過程是在責任鏈中完成的。下面我們就來分析一下 OkHttp 裡的責任鏈的執行過程。
2.3 責任鏈的執行過程
在典型的責任鏈設計模式裡,很多物件由每一個物件對其下級的引用而連線起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個物件決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪一個物件最終處理這個請求,這使得系統可以在不影響客戶端的情況下動態地重新組織和分配責任。責任鏈在現實生活中的一種場景就是面試,當某輪面試官覺得你沒有資格進入下一輪的時候可以否定你,不然會讓下一輪的面試官繼續面試。
在 OkHttp 裡面,責任鏈的執行模式與之稍有不同。這裡我們主要來分析一下在 OkHttp 裡面,責任鏈是如何執行的,至於每個鏈條裡面的具體邏輯,我們會在隨後一一說明。
回到 2.1 的程式碼,有兩個地方需要我們注意:
- 是當建立一個責任鏈
RealInterceptorChain
的時候,我們傳入的第 5 個引數是 0。該引數名為index
,會被賦值給RealInterceptorChain
例項內部的同名全域性變數。 - 當啟用責任鏈的時候,會呼叫它的
proceed(Request)
方法。
下面是 proceed(Request)
方法的定義:
@Override public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}
複製程式碼
這裡又呼叫了內部的過載的 proceed()
方法。下面我們對該方法進行了簡化:
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
// ...
// 呼叫責任鏈的下一個攔截器
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// ...
return response;
}
複製程式碼
注意到這裡使用責任鏈進行處理的時候,會新建下一個責任鏈並把 index+1
作為下一個責任鏈的 index
。然後,我們使用 index
從攔截器列表中取出一個攔截器,呼叫它的 intercept()
方法,並把下一個執行鏈作為引數傳遞進去。
這樣,當下一個攔截器希望自己的下一級繼續處理這個請求的時候,可以呼叫傳入的責任鏈的 proceed()
方法;如果自己處理完畢之後,下一級不需要繼續處理,那麼就直接返回一個 Response
例項即可。因為,每次都是在當前的 index
基礎上面加 1,所以能在呼叫 proceed()
的時候準確地從攔截器列表中取出下一個攔截器進行處理。
我們還要注意的地方是之前提到過重試攔截器,這種攔截器會在內部啟動一個 while
迴圈,並在迴圈體中呼叫執行鏈的 proceed()
方法來實現請求的不斷重試。這是因為在它那裡的攔截器鏈的 index
是固定的,所以能夠每次呼叫 proceed()
的時候,都能夠從自己的下一級執行一遍鏈條。下面就是這個責任鏈的執行過程:
清楚了 OkHttp 的攔截器鏈的執行過程之後,我們來看一下各個攔截器做了什麼邏輯。
2.3 重試和重定向:RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor
主要用來當請求失敗的時候進行重試,以及在需要的情況下進行重定向。我們上面說,責任鏈會在進行處理的時候呼叫第一個攔截器的 intercept()
方法。如果我們在建立 OkHttp 客戶端的時候沒有加入自定義攔截器,那麼
RetryAndFollowUpInterceptor
就是我們的責任鏈中最先被呼叫的攔截器。
@Override public Response intercept(Chain chain) throws IOException {
// ...
// 注意這裡我們初始化了一個 StreamAllocation 並賦值給全域性變數,它的作用我們後面會提到
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
// 用來記錄重定向的次數
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
// 這裡從當前的責任鏈開始執行一遍責任鏈,是一種重試的邏輯
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// 呼叫 recover 方法從失敗中進行恢復,如果可以恢復就返回true,否則返回false
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// 重試與伺服器進行連線
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// 如果 releaseConnection 為 true 則表明中間出現了異常,需要釋放資源
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
// 使用之前的響應 priorResponse 構建一個響應,這種響應的響應體 body 為空
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder().body(null).build())
.build();
}
// 根據得到的響應進行處理,可能會增加一些認證資訊、重定向或者處理超時請求
// 如果該請求無法繼續被處理或者出現的錯誤不需要繼續處理,將會返回 null
Request followUp = followUpRequest(response, streamAllocation.route());
// 無法重定向,直接返回之前的響應
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
// 關閉資源
closeQuietly(response.body());
// 達到了重定向的最大次數,就丟擲一個異常
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
// 這裡判斷新的請求是否能夠複用之前的連線,如果無法複用,則建立一個新的連線
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(followUp.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
priorResponse = response;
}
}
複製程式碼
以上的程式碼主要用來根據錯誤的資訊做一些處理,會根據伺服器返回的資訊判斷這個請求是否可以重定向,或者是否有必要進行重試。如果值得去重試就會新建或者複用之前的連線在下一次迴圈中進行請求重試,否則就將得到的請求包裝之後返回給使用者。這裡,我們提到了 StreamAllocation
物件,它相當於一個管理類,維護了伺服器連線、併發流和請求之間的關係,該類還會初始化一個 Socket
連線物件,獲取輸入/輸出流物件。同時,還要注意這裡我們通過 client.connectionPool()
傳入了一個連線池物件 ConnectionPool
。這裡我們只是初始化了這些類,但實際在當前的方法中並沒有真正用到這些類,而是把它們傳遞到下面的攔截器裡來從伺服器中獲取請求的響應。稍後,我們會說明這些類的用途,以及之間的關係。
2.4 BridgeInterceptor
橋攔截器 BridgeInterceptor
用於從使用者的請求中構建網路請求,然後使用該請求訪問網路,最後從網路響應當中構建使用者響應。相對來說這個攔截器的邏輯比較簡單,只是用來對請求進行包裝,並將伺服器響應轉換成使用者友好的響應:
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
// 從使用者請求中獲取網路請求構建者
Request.Builder requestBuilder = userRequest.newBuilder();
// ...
// 執行網路請求
Response networkResponse = chain.proceed(requestBuilder.build());
// ...
// 從網路響應中獲取使用者響應構建者
Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);
// ...
// 返回使用者響應
return responseBuilder.build();
}
}
複製程式碼
2.5 使用快取:CacheInterceptor
快取攔截器會根據請求的資訊和快取的響應的資訊來判斷是否存在快取可用,如果有可以使用的快取,那麼就返回該快取該使用者,否則就繼續責任鏈來從伺服器中獲取響應。當獲取到響應的時候,又會把響應快取到磁碟上面。以下是這部分的邏輯:
public final class CacheInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;
long now = System.currentTimeMillis();
// 根據請求和快取的響應中的資訊來判斷是否存在快取可用
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest; // 如果該請求沒有使用網路就為空
Response cacheResponse = strategy.cacheResponse; // 如果該請求沒有使用快取就為空
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
// 請求不使用網路並且不使用快取,相當於在這裡就攔截了,沒必要交給下一級(網路請求攔截器)來執行
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// 該請求使用快取,但是不使用網路:從快取中拿結果,沒必要交給下一級(網路請求攔截器)執行
if (networkRequest == null) {
return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();
}
Response networkResponse = null;
try {
// 這裡呼叫了執行鏈的處理方法,實際就是交給自己的下一級來執行了
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 這裡當拿到了網路請求之後呼叫,下一級執行完畢會交給它繼續執行,如果使用了快取就把請求結果更新到快取裡
if (cacheResponse != null) {
// 伺服器返回的結果是304,返回快取中的結果
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
cache.trackConditionalCacheHit();
// 更新快取
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
// 把請求的結果放進快取裡
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
}
複製程式碼
對快取,這裡我們使用的是全域性變數 cache
,它是 InternalCache
型別的變數。InternalCache
是一個介面,在 OkHttp 中只有一個實現類 Cache
。在 Cache
內部,使用了 DiskLruCache
來將快取的資料存到磁碟上。DiskLruCache
以及 LruCache
是 Android 上常用的兩種快取策略。前者是基於磁碟來進行快取的,後者是基於記憶體來進行快取的,它們的核心思想都是 Least Recently Used,即最近最少使用演算法。我們會在以後的文章中詳細介紹這兩種快取框架,也請繼續關注我們的文章。
另外,上面我們根據請求和快取的響應中的資訊來判斷是否存在快取可用的時候用到了 CacheStrategy
的兩個欄位,得到這兩個欄位的時候使用了非常多的判斷,其中涉及 Http 快取相關的知識,感興趣的話可以自己參考原始碼。
2.6 連線複用:ConnectInterceptor
連線攔截器 ConnectInterceptor
用來開啟到指定伺服器的網路連線,並交給下一個攔截器處理。這裡我們只開啟了一個網路連線,但是並沒有傳送請求到伺服器。從伺服器獲取資料的邏輯交給下一級的攔截器來執行。雖然,這裡並沒有真正地從網路中獲取資料,而僅僅是開啟一個連線,但這裡有不少的內容值得我們去關注。因為在獲取連線物件的時候,使用了連線池 ConnectionPool
來複用連線。
public final class ConnectInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
複製程式碼
這裡的 HttpCodec
用來編碼請求並解碼響應,RealConnection
用來向伺服器發起連線。它們會在下一個攔截器中被用來從伺服器中獲取響應資訊。下一個攔截器的邏輯並不複雜,這裡萬事具備之後,只要它來從伺服器中讀取資料即可。可以說,OkHttp 中的核心部分大概就在這裡,所以,我們就先好好分析一下,這裡在建立連線的時候如何藉助連線池來實現連線複用的。
根據上面的程式碼,當我們呼叫 streamAllocation
的 newStream()
方法的時候,最終會經過一系列的判斷到達 StreamAllocation
中的 findConnection()
方法。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
// ...
synchronized (connectionPool) {
// ...
// 嘗試使用已分配的連線,已經分配的連線可能已經被限制建立新的流
releasedConnection = this.connection;
// 釋放當前連線的資源,如果該連線已經被限制建立新的流,就返回一個Socket以關閉連線
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// 已分配連線,並且該連線可用
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// 如果該連線從未被標記為獲得,不要標記為釋出狀態,reportedAcquired 通過 acquire() 方法修改
releasedConnection = null;
}
if (result == null) {
// 嘗試供連線池中獲取一個連線
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
// 關閉連線
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if (result != null) {
// 如果已經從連線池中獲取到了一個連線,就將其返回
return result;
}
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) {
// 根據一系列的 IP 地址從連線池中獲取一個連結
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
// 從連線池中獲取一個連線
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
// 建立一個新的連線,並將其分配,這樣我們就可以在握手之前進行終端
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}
// 如果我們在第二次的時候發現了一個池連線,那麼我們就將其返回
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
// 進行 TCP 和 TLS 握手
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
// 將該連線放進連線池中
Internal.instance.put(connectionPool, result);
// 如果同時建立了另一個到同一地址的多路複用連線,釋放這個連線並獲取那個連線
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
複製程式碼
該方法會被放置在一個迴圈當中被不停地呼叫以得到一個可用的連線。它優先使用當前已經存在的連線,不然就使用連線池中存在的連線,再不行的話,就建立一個新的連線。所以,上面的程式碼大致分成三個部分:
- 判斷當前的連線是否可以使用:流是否已經被關閉,並且已經被限制建立新的流;
- 如果當前的連線無法使用,就從連線池中獲取一個連線;
- 連線池中也沒有發現可用的連線,建立一個新的連線,並進行握手,然後將其放到連線池中。
在從連線池中獲取一個連線的時候,使用了 Internal
的 get()
方法。Internal
有一個靜態的例項,會在 OkHttpClient 的靜態程式碼快中被初始化。我們會在 Internal
的 get()
中呼叫連線池的 get()
方法來得到一個連線。
從上面的程式碼中我們也可以看出,實際上,我們使用連線複用的一個好處就是省去了進行 TCP 和 TLS 握手的一個過程。因為建立連線本身也是需要消耗一些時間的,連線被複用之後可以提升我們網路訪問的效率。那麼這些連線被放置在連線池之後是如何進行管理的呢?我們會在下文中分析 OkHttp 的 ConnectionPool
中是如何管理這些連線的。
2.7 CallServerInterceptor
伺服器請求攔截器 CallServerInterceptor
用來向伺服器發起請求並獲取資料。這是整個責任鏈的最後一個攔截器,這裡沒有再繼續呼叫執行鏈的處理方法,而是把拿到的響應處理之後直接返回給了上一級的攔截器:
public final class CallServerInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
// 獲取 ConnectInterceptor 中初始化的 HttpCodec
HttpCodec httpCodec = realChain.httpStream();
// 獲取 RetryAndFollowUpInterceptor 中初始化的 StreamAllocation
StreamAllocation streamAllocation = realChain.streamAllocation();
// 獲取 ConnectInterceptor 中初始化的 RealConnection
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
realChain.eventListener().requestHeadersStart(realChain.call());
// 在這裡寫入請求頭
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
// 在這裡寫入請求體
if (responseBuilder == null) {
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
// 寫入請求體
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
streamAllocation.noNewStreams();
}
}
httpCodec.finishRequest();
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
// 讀取響應頭
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
// 讀取響應體
int code = response.code();
if (code == 100) {
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
realChain.eventListener().responseHeadersEnd(realChain.call(), response);
if (forWebSocket && code == 101) {
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
// ...
return response;
}
}
複製程式碼
2.8 連線管理:ConnectionPool
與請求的快取類似,OkHttp 的連線池也使用一個雙端佇列來快取已經建立的連線:
private final Deque<RealConnection> connections = new ArrayDeque<>();
複製程式碼
OkHttp 的快取管理分成兩個步驟,一邊當我們建立了一個新的連線的時候,我們要把它放進快取裡面;另一邊,我們還要來對快取進行清理。在 ConnectionPool
中,當我們向連線池中快取一個連線的時候,只要呼叫雙端佇列的 add()
方法,將其加入到雙端佇列即可,而清理連線快取的操作則交給執行緒池來定時執行。
在 ConnectionPool
中存在一個靜態的執行緒池:
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */,
60L /* keepAliveTime */,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
Util.threadFactory("OkHttp ConnectionPool", true));
複製程式碼
每當我們向連線池中插入一個連線的時候就會呼叫下面的方法,將連線插入到雙端佇列的同時,會呼叫上面的執行緒池來執行清理快取的任務:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
// 使用執行緒池執行清理任務
executor.execute(cleanupRunnable);
}
// 將新建的連線插入到雙端佇列中
connections.add(connection);
}
複製程式碼
這裡的清理任務是 cleanupRunnable
,是一個 Runnable
型別的例項。它會在方法內部呼叫 cleanup()
方法來清理無效的連線:
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
複製程式碼
下面是 cleanup()
方法:
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
synchronized (this) {
// 遍歷所有的連線
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// 當前的連線正在使用中
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// 如果找到了一個可以被清理的連線,會嘗試去尋找閒置時間最久的連線來釋放
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// 該連線的時長超出了最大的活躍時長或者閒置的連線數量超出了最大允許的範圍,直接移除
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// 閒置的連線的數量大於0,停頓指定的時間(等會兒會將其清理掉,現在還不是時候)
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 所有的連線都在使用中,5分鐘後再清理
return keepAliveDurationNs;
} else {
// 沒有連線
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
return 0;
}
複製程式碼
在從快取的連線中取出連線來判斷是否應該將其釋放的時候使用到了兩個變數 maxIdleConnections
和 keepAliveDurationNs
,分別表示最大允許的閒置的連線的數量和連線允許存活的最長的時間。預設空閒連線最大數目為5個,keepalive
時間最長為5分鐘。
上面的方法會對快取中的連線進行遍歷,以尋找一個閒置時間最長的連線,然後根據該連線的閒置時長和最大允許的連線數量等引數來決定是否應該清理該連線。同時注意上面的方法的返回值是一個時間,如果閒置時間最長的連線仍然需要一段時間才能被清理的時候,會返回這段時間的時間差,然後會在這段時間之後再次對連線池進行清理。
總結:
以上就是我們對 OkHttp 內部網路訪問的原始碼的分析。當我們發起一個請求的時候會初始化一個 Call 的例項,然後根據同步和非同步的不同,分別呼叫它的 execute()
和 enqueue()
方法。雖然,兩個方法一個會在當前的執行緒中被立即執行,一個會線上程池當中執行,但是它們進行網路訪問的邏輯都是一樣的:通過攔截器組成的責任鏈,依次經過重試、橋接、快取、連線和訪問伺服器等過程,來獲取到一個響應並交給使用者。其中,快取和連線兩部分內容是重點,因為前者涉及到了一些計算機網路方面的知識,後者則是 OkHttp 效率和框架的核心。