一、引言
在我們日常開發中,OkHttp
可謂是最常用的開源庫之一,目前就連Android API
中的網絡請求接口都是用的OkHttp
,好吧,真的很強。
在上學期間我也曾閱讀和分析過OkHttp
的源碼,並記錄在筆記中,不過現在再去翻看的時候發現當時很多地方並沒有正確理解,因此也趁着這個過年假期重新閱讀和整理一遍,感興趣的童鞋可以看看。
本文純屬基於個人理解,因此受限於知識水平,有些地方可能依然沒有理解到位,還請發現問題的童鞋理性指出。
溫馨提示: 本文很長,源碼基於
OKHttp-3.11.0
二、從一個簡單請求入手分析整體運作流程
1. 先來點關於 OkHttpClient 的官方釋義
OkHttpClient
作爲Call
的工廠類,用於發送HTTP
請求並讀取相應數據。
OkHttpClient
應當被共享.
使用OkHttpClient
的最佳使用方式是創建一個OkHttpClient
單例,然後複用這個單例進行所有的HTTP
請求。爲啥呢?因爲每個OkHttpClient
自身都會持有一個連接池和線程池,所以符用連接和線程可以減少延遲、節約內存。相反地,如果給每個請求都創建一個OkHttpClient
的話,那就是浪費閒置線程池的資源。
可以如下使用new OkHttpClient()
創建一個默認配置的共享OkHttpClient
.
public final OkHttpClient client = new OkHttpClient();
或使用 new OkHttpClient.Builder()
來創建一個自定義配置的共享實例:
public final OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor())
.cache(new Cache(cacheDir, cacheSize))
.build();
可以通過newBuilder()
來自定義OkHttpClient
,這樣創建出來的OkHttpClient
具有與原對象相同的連接池、線程池和配置。使用這個方法可以派生一個具有自己特殊配置的OkHttpClient
以符合我們的特殊要求。
如下示例演示的就是如何派生一個讀超時爲500毫秒的OkHttpClient
:
OkHttpClient eagerClient = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
Response response = eagerClient.newCall(request).execute();
關閉(Shutdown
)不是必須的。
線程和連接會一直被持有,直到當它們保持閒置時自動被釋放。但是如果你編寫的應用需要主動釋放無用資源,那麼你也可以主動去關閉。通過shutdown()
方法關閉分發器dispatcher
的執行服務,這將導致之後OkHttpClient
收到的請求全部被拒絕掉。
client.dispatcher().executorService().shutdown();
清空連接池可以用evictAll()
,不過連接池的守護線程可能不會馬上退出。
client.connectionPool().evictAll();
如果相關比緩存,可以調用close()
,注意如果緩存已經關閉了再創建call
的話就會出現錯誤,並且會導致call
崩潰。
client.cache().close();
OkHttp
同樣會爲所有HTTP/2
連接建立守護線程,並且再它們保持閒置狀態時自動關閉掉它們。
2. 常規使用
上面的官方釋義描述了OkHttpClient
的最佳實踐原則和清理操作,接下來我們根據一個簡單的GET
請求操作來引出我們要分析的問題:
如下創建一個OkHttpClient
實例,添加了Intercepter
,並在工程目錄下建了個名爲cache
的Cache
緩存:
Interceptor logInterceptor = chain -> {
Request request = chain.request();
System.out.println(request.url());
System.out.println(request.method());
System.out.println(request.tag());
System.out.println(request.headers());
return chain.proceed(request);
};
okHttpClient = new OkHttpClient.Builder()
.cache(new Cache(new File("cache/"), 10 * 1024 * 1024))
.addInterceptor(logInterceptor)
.build();
然後一個普通的GET
請求是這樣的,這裏以獲取 玩Android 首頁列表爲例。
public void getHomeList(int page){
// 1. 建立HTTP請求
Request request = new Request.Builder()
.url(String.format("http://wanandroid.com/article/list/%d/json", page))
.get()
.build();
// 2. 基於 Request創建 Call
okhttp3.Call call = okHttpClient.newCall(request);
// 3. 執行Call
call.enqueue(new Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(okhttp3.Call call, Response response) throws IOException {
System.out.println(response.message());
System.out.println(response.code());
System.out.println(response.headers());
if (response.isSuccessful()){
ResponseBody body = response.body();
if (body == null) return;
System.out.println(body.string());
// 每個ResponseBody只能使用一次,使用後需要手動關閉
body.close();
}
}
});
}
3. 執行流程分析
注意到上面的okHttpClient.newCall(request)
,對應的源碼如下,可知它創建的實際上是Call
的實現類RealCall
。
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
Call
提供請求任務的執行和取消和相關狀態操作方法。類似於FutureTask
,是任務執行單元,其核心的執行方法代碼如下,包含同步執行(execute()
)和異步執行(enqueue()
)兩種方式。對於同步方法而言,RealCall
僅僅通過executed()
方法將自身記錄在Dispatcher
(分發器)的同步請求隊列中,這是爲了在分發器中統計請求數量,在請求結束之後則通過finished()
方法將自身從分發器中的同步請求隊列中移除,而真正進行數據請求的是在攔截器Intercepter
,如下源碼:
@Override public Response execute() throws IOException {
// ...
eventListener.callStart(this);
try {
// 1. 僅僅將這個 Call記錄在分發器 ( Dispatcher )的同步執行隊列中
client.dispatcher().executed(this);
// 2. 通過攔截器鏈獲取響應數據,這裏纔會真正的執行請求
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
// 3. 拿到響應數據後從分發器的同步執行隊列中移除當前請求
client.dispatcher().finished(this);
}
}
跟進至getResponseWithInterceptorChain()
,可以注意到,除了我們在創建OkHttpClient
時添加的攔截器外,每個HTTP
請求都會默認添加幾個固有的攔截器,如
RetryAndFollowUpInterceptor
、BridgeInterceptor
、CacheInterceptor
、ConnectInterceptor
、CallServerInterceptor
。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
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);
}
關於它們的源碼實現會在後面的核心類解讀中詳細分析,這裏先了解一個它們各自的作用:
- RetryAndFollowUpInterceptor:用於失敗時恢復以及在必要時進行重定向。
- BridgeInterceptor:應用代碼與網絡代碼的橋樑。首先根據用戶請求建立網絡請求,然後執行這個網絡請求,最後根據網絡請求的響應數據建立一個用戶響應數據。
- CacheInterceptor:用於從本地緩存中讀取數據,以及將服務器數據寫入到緩存。
- ConnectInterceptor:用於打開一個到目標服務器的連接,並切換至下一個攔截器。
- CallServerInterceptor:這是攔截器鏈的最後一環,至此將真正的進行服務器請求。
請求時整個攔截器的調用鏈的執行次序如下:
對於請求時攔截器的調用鏈你可能會有所疑惑,爲什麼它是按這個次序執行的呢?咱看看RealInterceptorChain#proceed(...)
方法的主要源碼,發現,雖然這裏看起來只進行了一次調用,但是如果你結合這些攔截器一起分析的話,你就會發現,其實這裏對攔截器集合進行了遞歸取值,因爲每次執行proceed()
方法時集合索引index
會 +1
, 並將index
傳入新建的RealInterceptorChain
,而攔截器集合唯一,因此相當於每次proceed
都是依次取得攔截器鏈中的下一個攔截器並使用這個新建的RealInterceptorChain
,執行RealInterceptorChain#proceed
方法,直到集合遞歸讀取完成。
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException {
// ...
// 每次執行 proceed() 方法時 index+1, 然後傳入新建的 RealInterceptorChain
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;
}
遞歸? 是的,如果你觀察的夠仔細的話,你會發現,其實BridgeInterceptor
、RetryAndFollowUpInterceptor
、CacheInterceptor
、ConnectInterceptor
都會執行RealInterceptorChain#proceed
方法,相當於這個方法在不斷地調用自己,符合遞歸的執行特性,因此Response
響應數據的返回次序剛好是與請求時相反的。BridgeInterceptor#intercept
相應抽取的源碼如下:
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
// do something ...
Response networkResponse = chain.proceed(requestBuilder.build());
// do something ...
return responseBuilder.build();
}
}
因而攔截器鏈的響應數據返回次序如下:
我靠,是不是覺得設計的非常巧妙,這也是我熱衷於源碼的重要原因之一,因爲不看看別人的代碼你就永遠不知道別人有多騷。。
根據上面的分析,我們已經知道了原來正真執行請求、處理響應數據是在攔截器,並且對於同步請求,分發器Dispatcher
僅僅是記錄下了同步請求的Call
,用作請求數量統計用的,並沒有參與到實際請求和執行中來。
OK,來看看異步請求RealCall#enqueue()
和Dispatcher#enqueue()
,毫無疑問,異步請求肯定是運行在線程池中了
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
Dispatcher#enqueue()
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
對於上面的AsyncCall
,核心源碼如下,注意到getResponseWithInterceptorChain()
,是不是非常地熟悉了,在上面的同步請求那裏已經詳細解釋過了,就不再累贅了。
final class AsyncCall extends NamedRunnable {
// ...
@Override protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
// ...
} catch (IOException e) {
// ...
} finally {
client.dispatcher().finished(this);
}
}
}
至此,OkHttp
的主體運作流程是不是已經清晰了,不過有沒有感覺還少點什麼,我們只是分析了運作流程,具體到怎麼連接的問題還沒有分析。
好吧,既然是建立連接,那麼極速定位到ConnectInterceptor
,沒毛病吧, 核心源碼如下:
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;
// ...
@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);
}
}
注意到上面的streamAllocation.newStream(..)
,源碼如下:
public HttpCodec newStream( OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
// ...
// 1. 查找可用連接
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
// 2. 建立 HTTP 或 HTTP2 連接
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
}
繼續定位到 findHealthyConnection
:
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
while (true) { // 完全阻塞式查找,找不到不罷休
// 1. 查找已有連接
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
// 2. 如果這是一條全新的連接,那麼可以跳過大量的健康檢查,直接返回
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
// 3. 做一個速度超慢的檢查,以確保池中的連接仍然可用,
// 如果不可用了就將其從池中剔除,然後繼續查找
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
定位到StreamAllocation#findConnection
,這裏查找連接的規則是:先查看當前是否存在可用連接,如果不存在,再從連接池中查找,如果還沒有,那就新建一個,用來承載新的數據流。 需要注意的一個細節就是,從連接池查找連接時會查詢兩次,第一次只是根據當前目標服務器地址去查,如果沒有查到,則第二次會重新選擇路由表,然後用該地址去匹配。最終如果存在已經創建好的連接,則直接返回使用,如果不存在,則新建一個連接,進行TCP
和TLS
握手,完事之後將這個連接的路由信息記錄在路由表中,並把這個連接保存到連接池。還需要注意的一點是:如果有個連接與當前建立的連接的地址相同,那麼將釋放掉當前建立好的連接,而使用後面創建的連接(保證連接是最新的)
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
synchronized (connectionPool) {
// ...
// 1. 檢查當前有沒有可用連接,如果有,那麼直接用當前連接
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) { // 可用
result = this.connection;
releasedConnection = null;
}
// ...
if (result == null) {
// 2. 不存在已有連接或者已有連接不可用,則嘗試從連接池中獲得可用連接
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
closeQuietly(toClose);
// ...
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();
}
// 3. 最後實在沒找到已有的連接,那麼就只能重新建立連接了
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;
}
根據以上分析可得出以下主體執行流程:
當然這是同步請求的流程,而對於異步請求而言,也僅僅是把攔截器鏈放到了線程池執行器中執行而已。
三、核心類解讀
至此,我們已經清楚了OkHttp
的主幹,當然,我們僅僅是把流程給走通了,在本節中,我們將根據源碼具體分析OkHttp
中各核心類的作用及其實現,內容很長,請做好心理準備。
1. 攔截器(Intercepter
)
1). RetryAndFollowUpInterceptor
作用:用於失敗時恢復以及在必要時進行重定向。
作爲核心方法,RetryAndFollowUpInterceptor#intercept
體現了RetryAndFollowUpInterceptor
的工作流程,源碼如下,我們來分析分析它是怎麼恢復和重定向的,具體實現流程還請看註釋:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
// 僅僅創建流的承載對象,此時並沒有建立流
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
int followUpCount = 0; // 用於記錄重定向和需要授權請求的數量
Response priorResponse = null;
while (true) {
// 1. 如果此時請求被取消了,那麼關閉連接,釋放資源
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
// 2. 推進執行攔截器鏈,請求並返回響應數據
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// 3. 如果連接失敗了,嘗試使用失敗的地址恢復一下,此時請求可能還沒有發送
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// 4. 嘗試重新與交流失敗的服務器重新交流,這個時候請求可能已經發送了
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// 5. 如果是位置異常,那麼釋放掉所有資源
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
// 6. 如果記錄的上一個請求大的響應數據存在,那麼將其響應體置空
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Request followUp;
try {
// 7. 處理請求的認證頭部、重定向或請求超時問題,如果這些操作都不必要
// 或者應用不了,那麼返回 null
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release();
throw e;
}
//需要處理認證、重定向和超時問題,那麼結束處理,返回響應數據
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 (!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;
}
}
2). BridgeInterceptor
作用:應用代碼與網絡代碼的橋樑。首先根據用戶請求建立網絡請求,然後執行這個網絡請求,最後根據網絡請求的響應數據建立一個用戶響應數據。
BridgeInterceptor#intercept
源碼如下,主要做了以下事情:
-
用於請求: 這個是在推進請求攔截器鏈時進行的,也就是說此時尚未真正地進行網絡請求。此時會補充缺失的請求頭參數,如
Content-Type
、Transfer-Encoding
、Host
、Connection
、Accept-Encoding
、User-Agent
、Cookie
。如果在請求時添加了gzip
請求頭參數,即開啓了gzip
壓縮,那麼在取得響應數據時需要對數據進行解壓。 -
用於響應: 這個實在取得網絡響應數據後回退攔截器鏈時進行的,即已經取得了網絡響應數據。此時會對相應頭部進行處理,如果請求時開啓了
gzip
壓縮,那麼此時會對響應數據進行解壓。
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
// 這裏處於請求之前
// 1. 此時主要爲請求添加缺失的請求頭參數
RequestBody body = userRequest.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
// ...
// 如果啓用了GZIP壓縮,那麼需要負責解壓響應數據
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
//...
// 2. 推進執行攔截器鏈,進行請求、返回數據
Response networkResponse = chain.proceed(requestBuilder.build());
// 取得網絡響應數據後
// 3. 處理響應頭,如果請求時開啓了GZIP壓縮,那麼這裏需要將響應數據解壓
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
// 建立用戶響應數據
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
綜上所述,BridgeInterceptor
主要用於請求前對用戶請求進行完善,補充缺失參數,然後推進請求攔截器鏈,並等待響應數據返回,取得響應數據後則是將其轉換成用戶響應數據,此時如果數據進行過gzip
壓縮,那麼會在這裏進行解壓,然後重新封裝成用戶數據。
3). CacheInterceptor
作用:用於從本地緩存中讀取數據,以及將服務器數據寫入到緩存。
CacheInterceptor#intercept
源碼如下,攔截器鏈執行到這一步主要做了如下事情:
-
請求:
如果開啓了緩存,且請求策略是禁用網絡僅讀緩存的話,那麼首先會根據當前請求去查找緩存,如果匹配到了緩存,則將緩存封裝成響應數據返回,如果沒有匹配到,那麼返回一個
504
的響應,這將導致請求攔截器鏈執行終止,進而返回執行響應攔截器鏈。如果請求策略是網絡加緩存,當那麼然網絡請求優先,所以就推進請求攔截器鏈執行請求,
-
網絡響應:
在得到網絡響應數據後,如果開啓了緩存策略其匹配到了舊緩存,那麼根據最新網絡請求響應數據更新緩存,然後返回響應數據;如果沒有匹配到緩存但是開啓了緩存,那麼將響應數據寫入緩存後返回;而如果開啓了緩存,但是並不使用緩存策略,那麼根據響應數據移除緩存中對應的數據緩存。
@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; // 如果僅讀緩存,那麼網絡請求會爲 null
Response cacheResponse = strategy.cacheResponse;
// ...
// 如果禁止使用網絡而僅讀取緩存的話,那麼沒匹配到緩存時返回 504
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
// ...
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
// ...
.body(Util.EMPTY_RESPONSE)
.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) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
// ...
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// ...
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;
}
緩存時判斷邏輯比較多,不過這裏重點在於理解緩存策略,一般會有:僅網絡、僅緩存、網絡加緩存 三種請求策略。
4). ConnectInterceptor
作用:用於打開一個到目標服務器的連接,並切換至下一個攔截器
因爲在上一節末尾分析OkHttp
如何建立連接的問題上已經分析過了,所以不做過多描述。
這裏回憶一下連接規則:建立連接時,首先會查看當前是否有可用連接,如果沒有,那麼會去連接池中查找,如果找到了,當然就使用這個連接,如果沒有,那麼就新建一個連接,進行TCP
和TLS
握手以建立聯繫,接着把連接放入連接池以備後續複用,最後推進請求攔截器鏈執行,將打開的連接交給下一個攔截器去處理。
5). CallServerInterceptor
作用:這是攔截器鏈的最後一環,至此將真正的進行服務器請求
CallServerInterceptor#intercept
源碼如下,作爲攔截器鏈的最後一環,當然要真正地做點實事了,大致操作步驟是:
發送請求頭部 --> 讀取一下GET
、HEAD
之外的請求(如POST
)的響應數據 --> 結束請求的發送動作 --> 讀取響應頭部 --> 讀取響應數據 --> 封裝後返回。
@Override public Response intercept(Chain chain) throws IOException {
// ...
long sentRequestMillis = System.currentTimeMillis();
// 1. 發送請求頭部
httpCodec.writeRequestHeaders(request);
Response.Builder responseBuilder = null;
// 2. 檢查是否是 GET 或 HEAD 以外的請求方式、讀取響應數據
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// 如果請求頭部中包含"Expect: 100-continue",那麼在轉換請求體前等待"HTTP/1.1 100 Continue"
// 響應。如果沒有讀取到"HTTP/1.1 100 Continue"的響應,那麼就不轉換請求體(request body)了,
// 而直接將我們得到的響應返回(如 狀態爲 4XX 的響應 );
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
// 讀取、轉換響應頭部
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
// 如果存在"Expect: 100-continue",則寫入請求體
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
} else if (!connection.isMultiplexed()) {
// 如果不存在"Expect: 100-continue",那麼禁止 HTTP/1 連接複用。
// 不過我們仍然必須轉換請求體以使連接達到一個固定的狀態。
streamAllocation.noNewStreams();
}
}
// 3. 結束請求的發送動作
httpCodec.finishRequest();
// 4. 讀取響應頭部
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
// 5. 構建響應數據
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
// 如果服務器返回 100-continue 響應即使我們並沒有這麼請求,則重新讀取一遍響應數據;
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
// 6. 填充響應數據
if (forWebSocket && code == 101) {
// 連接正在更新,不過我們需要確保攔截器收到的 non-null 的響應體
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
// 這裏將 http 輸入流包裝到響應體
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
// 其他情況...
return response;
}
綜上,作爲攔截器最後一環的CallServerInterceptor
終於把請求給終結了,完成了與服務器的溝通交流,把需要的數據拿了回來。請求的時候每個攔截器都會插上一腳,響應的時候也一樣,把數據轉換的工作分給了各個攔截器處理。
2. 分發器(Dispatcher
)
爲什麼叫分發器呢?如果叫做執行器(Executor
)可能會更好理解一些,因爲它的工作就是執行異步請求,雖然會統計請求的數量....嗯~~好吧,換個角度,如果理解爲它用於把異步任務分發給線程池執行,起到任務分發的作用,那就理解爲啥叫分發器了。
OK,先來觀察一下Dispatcher
的構成,部分源碼如下,可以先看看註釋:
public final class Dispatcher {
private int maxRequests = 64; // 同時執行的最大異步請求數量,數量超過該值時,新增的請求會放入異步請求隊列中
private int maxRequestsPerHost = 5; // 每個主機最多同時存在的請求數量
private @Nullable Runnable idleCallback;
// 線程池執行器
private @Nullable ExecutorService executorService;
// 尚未執行的任務隊列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
// 正在執行的任務隊列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
// 同步執行的任務隊列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public Dispatcher(ExecutorService executorService) {
this.executorService = 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;
}
// ...
}
簡單描述一下:Dispatcher
包含三個任務隊列,分別用於記錄尚未執行的異步請求、正在執行的異步請求、正在執行的同步請求。包含一個線程池,用於執行異步請求,這個線程池執行器的核心線程數量爲 0
, 最大線程數量不限(整型的最大值2^31-1
,相當於不限),閒置線程的最大等待超時時間爲60
秒,線程池的任務隊列使用非公平機制的SynchronousQueue
。這就是Dispatcher
的主要配置。
我們來看看它是如何限制每個主機的請求數量的,直接看註釋好了。
synchronized void enqueue(AsyncCall call) {
// 正在執行的異步請求數量小於限定值,且同一主機的正在執行的異步請求數量小於限定值時
// 添加到正在執行的異步請求隊列中,並執行。
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else { // 否則就添加到等待隊列中
readyAsyncCalls.add(call);
}
}
runningCallsForHost
用於計算當前正在執行的連接到相同主機上異步請求的數量
private int runningCallsForHost(AsyncCall call) {
int result = 0;
for (AsyncCall c : runningAsyncCalls) {
if (c.get().forWebSocket) continue;
if (c.host().equals(call.host())) result++;
}
return result;
}
3. 連接池(ConnetionPool
)
作用:用於管理HTTP
和HTTP/2
連接的複用以減少網絡延遲,因爲使用相同地址的請求可能共享一個連接。所以ConnetionPool
實現了維護已打開連接已被後續使用的機制。
線程池啥的就不多說了,這裏主要分析一下ConnetionPool
如何維護已打開的連接。從ConnetionPool#put
着手:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) { // 如果當前不在執行清理任務,那麼現在執行
cleanupRunning = true;
executor.execute(cleanupRunnable); // 線程池的作用就是執行清理任務
}
connections.add(connection); // 同時添加到連接隊列中
}
cleanupRunnable
源碼如下,根據ConnetionPool#put
可知每當我們往連接池中添加一個連接時,如果當前不在執行清理任務(cleanupRunnable
),那麼立馬會執行cleanupRunnable
,而cleanupRunnable
中會循環執行cleanup
,直到所有連接都因閒置超時而被清理掉,具體還請先看註釋。
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime()); // 執行清理
if (waitNanos == -1) return; // cleanup中的情況 4)
if (waitNanos > 0) { // cleanup中的情況 2) 和 3)
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos); // 等待超時
} catch (InterruptedException ignored) {
}
}
}
// 至此情況 1), 2), 3) 都會導致 `cleanup`被循環執行
}
}
};
cleanup
源碼如下,它的作用是查找、清理超過了keep-alive
時間限制或者閒置超時閒置的連接。具體還請看註釋。
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
// 1. 查找超時連接
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// 1). 如果正在使用,那麼跳過,繼續查找
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++; // 記錄閒置連接的數量
// 2). 如果閒置時間超過了最大允許閒置時間,則記錄下來在後面清除
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
// 2. 查找完成了,將在這裏對閒置連接進行處理
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// 1). 確定已經超時了,那麼從連接池中清除,關閉動作會在同步塊外面進行
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// 2). 存在閒置連接,但是尚未超時
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 3). 如果所有連接都正在使用,那麼最多保持個`keep-alive`超時時間就又會重新執行清理動作
return keepAliveDurationNs;
} else {
// 4). 壓根沒有連接,那不管了,標記爲非清理狀態,並返回-1
cleanupRunning = false;
return -1;
}
}
// 3. 關閉上面查找到的處於情況1)的閒置超時連接
closeQuietly(longestIdleConnection.socket());
// 返回 0 ,表示馬上會重新回來執行清理操作
return 0;
}
綜上,ConnectionPool
會設定keepAliveDurationNs
、longestIdleDurationNs
兩個超時時間,而每次往連接池中添加一個新連接時,如果當前處於非清理裝填,都會導致線程池執行器開個線程執行清理動作,而對於清理動作而言,會遍歷連接池,查找閒置超時的連接,並記錄閒置連接的數量,而遍歷完成後,將根據情況 2. 1)、2)、3)、4) 進行相應的處理,而如果是情況 2. 4), 則會當即結束清理循環,意味着連接池中已經沒有連接了,此時線程會執行完成而退出,其他幾種情況都不會中斷循環,因此實際上這個線程池最多隻會存在一個連接池維護線程。
四、總結
一般來說,當使用OKHttp
通過URL
請求時,它做了以下事情:
- 使用
URL
並且配置OKHttpClient
來創建地址(Address
),這個地址指定了我們連接web
服務器的方式。 - 嘗試從連接池(
ConnectionPool
)中取出與該地址相同的連接。 - 如果在連接池中沒有對應該地址的連接,那麼它會選擇一條新路線(
route
)去嘗試,這通常意味着將進行DNS
請求以獲取對應服務器的IP
地址,然後如果需要的話還會選擇TLS
版本和代理服務器。 - 如果這是一條新路線,它會通過
Socket
連、TLS
隧道(HTTP代理的HTTPS)或者TLS
連接,然後根據需要進行TCP
、TLS
握手。 - 發送
HTTP
請求,然後讀取響應數據。
如果連接出現問題,OKHttp
會選擇另一條路線再次嘗試,這使得OKHttp
在服務器地址子集無法訪問時能夠恢復,而當從連接池中拿到的連接已經過期,或者TLS
版本不支持的情況下,這種方式同樣很有用。一旦接收到響應數據,該連接就會返回到連接池中以備後續請求使用,而連接池中的連接也會在一定時間的不活動狀態後被清除掉。
對於整體框架而言,本文已經詳細分析了OkHttp
的整體工作流程,相關細節還請回到文中去,這裏就不再累贅了。