目錄
一、重試重定向攔截器 RetryAndFollowUpInterceptor
這篇文章都是乾貨,沒有多餘廢話。
OkHttp介紹:
https://square.github.io/okhttp/
由Square公司貢獻的一個處理網絡請求的開源項目,是目前Android使用最廣泛的網絡框架。從Android4.4開始HttpURLConnection的底層實現採用的是OkHttp。
OkHttp特點:
- 支持HTTP/2並允許對同一主機的所有請求共享一個套接字
- 通過連接池,減少了請求延遲
- 默認通過GZip壓縮數據
- 響應緩存,避免了重複請求的網絡
- 請求失敗自動重試主機的其他ip,自動重定向
Okhttp的基本使用
首先我們在gradle中引入
Implementation "com.squareup.okhttp3:okhttp:$Version"
OkHttpClient client = new OkHttpClient.Builder().read(5, TimeUnit.SECONDS).buid();
Request request = new Request.Builder()
.url(url)
.build();
Call call = client.newCall(request);
//1 執行同步Get請求
Response response = call.execute();
//2 執行異步
call.enqueue(callback);
RequestBody requestBody = new FormBody.Builder().add("name", "新歡").build();
Request request = new Request.Builder()
.url(url)
.post(requestBody).build();
Call call = client.newCall(request);
//執行同步Post請求
Response response = call.execute();
//獲得響應
ResponseBody body = response.body();
System.out.println(body.string());
主要的類:OkHttpClient、Request、Call、Response
Okhttp內部精髓在於分發器和攔截器完成了請求邏輯。
分發器:內部維護隊列與線程池,完成請求調配;
攔截器:五大默認攔截器完成整個請求過程。
分發器Dispatcher:
看一下Call對象的創建
Call call = client.newCall(request);
那麼Call對象是如何獲取的,我們看看OkHttpClient類中的源碼
public class OkHttpClient{
public static final class Builder {
public Builder() {
dispatcher = new Dispatcher();//創建了分發器
//...
}
}
@Override
public Call newCall(Request request) {
//RealCall是Call的唯一實現類
return RealCall.newRealCall(this, request, false);
}
}
我們再看看
同步請求Response response = call.execute();和異步請求是如何執行的,也就是RealCall的execute()方法和enqueue()方法
//同步請求調用此方法
@Override
public Response execute() throws IOException {
//....省略一部分代碼
try {
//從OkhttpClient獲取到分發器,調用分發器執行請求,並加入runningSyncCalls隊列
client.dispatcher().executed(this);
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);//移除隊列
}
}
//異步請求調用此方法
@Override
public void enqueue(Callback responseCallback) {
//....省略一部分代碼
//從OkhttpClient獲取到分發器,調用分發器執行
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
我們看看Dispatcher類中的executed()方法和enqueue()方法
//Dspatcher.java
//等待異步隊列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//正在執行的異步隊列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//同步隊列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
//同步請求調用
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);//將call加入了同步隊列中
}
//異步請求調用
synchronized void enqueue(AsyncCall call) {
//正在請求隊列的數量是有限制的,默認64
//統一域名正在請求的個數也是有限制的,默認5
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);//加入正在運行的隊列
executorService().execute(call);//放入線程池,通過線程池執行
} else {
readyAsyncCalls.add(call);
}
}
同步請求將請求加入了同步隊列中。
我們再看看異步請求的方法,Dispatcher中有三個隊列,其中異步請求隊列有兩個。根據不同的情況放入的隊列也不一樣。
(一)如何決定將請求放入readyAsyncCalls隊列還是runningAsyncCalls隊列?
我們看異步請求調用的方法enqueue()中的if語句的判斷
maxRequests默認是64,當然是可以自己配置的,maxRequestsPerHost默認是5。
如果正在請求的隊列數量小於64並且同一域名正在請求的個數小於5,那麼就將這個請求任務加入到正在執行隊列runningAsyncCalls中,否則就加入到等待隊列readyAsyncCalls中。
(二)從ready移動running的條件是什麼?
我們看上面代碼enqueue()方法中if語句中將任務放入了正在執行的異步隊列中後,將任務AsyncCall提交到了線程池中,那麼AsyncCall肯定是一個Runnable。我們看一下AsyncCall,他是一個RealCall中的內部類。AsyncCall繼承了NameRunnable,而NameRunnable他是一個Runnable,其中run()方法也是在NameRunnable這個抽象類中實現的,在run()方法中調用了一個抽象方法execute(),而這個抽象方法execute()方法就是子類AsyncCall實現的,那麼run()方法的執行就是execute()方法的執行。
final class AsyncCall extends NamedRunnable {
@Override
protected void execute() {
boolean signalledCallback = false;
try {
//...省略
} catch (IOException e) {
//...省略
} finally {
//分發器中的finished()方法中的邏輯就是將等待隊列移動到正在執行的隊列
client.dispatcher().finished(this);
}
}
}
上面的代碼,我們知道無論成功失敗,代碼最終都會調用finally中的代碼塊。那麼就會調用分發器中的finished()方法
我們接下來看分發器Dispatcher.java中的finished()方法
/**
* Used by {@code AsyncCall#run} to signal completion.
*/
void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
//calls即runningAsyncCalls,將請求結束的請求任務從正在請求的隊列中移除
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();//移動調整隊列
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
//主要根據條件將等待異步隊列中移除的請求任務添加到正在執行的異步隊列中
private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {//遍歷等待隊列
AsyncCall call = i.next();
//如果獲取到的等待請求的host在正在請求的列表當中小於5個
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();//從等待異步隊列中移除
runningAsyncCalls.add(call);//添加到正在執行的異步隊列中
executorService().execute(call);//將請求任務添加到線程池中執行
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
/**
* Returns the number of running calls that share a host with {@code call}.
*/
private int runningCallsForHost(AsyncCall call) {
int result = 0;
for (AsyncCall c : runningAsyncCalls) {//遍歷正在執行的請求
if (c.get().forWebSocket) continue;
if (c.host().equals(call.host())) result++;//等待異步隊列中的請求與正在執行異步隊列的host匹配上就+1
}
return result;
}
promoteCalls()方法中再次判斷,同樣的判斷,判斷正在請求不能超過64個,判斷等待隊列不能爲空,同一域名正在請求的個數小於5.
到這裏你是否對分發器有了認識,分發器主要將請求的任務根據不同的情況加入到不同的隊列,並且將任務提交到線程池中。
(三)Dispatcher中的線程池:
接下來我們分析分發器Dispatcher.java類中的線程池
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
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;
}
//...省略
synchronized void enqueue(AsyncCall call) {
//...
}
synchronized void executed(RealCall call) {
//...
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
//...
}
//...省略
}
我在這裏簡單的介紹一下爲什麼很多框架都用線程池,一提到池就應該想到複用,線程池就是不需要我們頻繁的創建對象,可以複用裏面的線程,優化了資源的開銷。額外說一下使用Handler時Message有消息池的概念,也是對消息對象的複用。
我們主要看Dispatcher類中默認線程池的參數,由於第五個參數使用的是SynchronousQueue,它是一個不存儲元素的阻塞隊列,所以當有任務提交,就會加入這個隊列失敗,那麼就會開啓線程,由於第二個參數最大線程數量是Integer.MAX_VALUE,因此不管有對少個任務提交都不會被阻塞等待,都會開啓線程立刻去執行。這種默認的線程池其實也就是java默認實現的四種線程池中的newCachedThreadPool這種方式創建出來的線程池。Okhttp使用的就是這樣的線程池,能夠滿足最大併發量。
攔截器
默認五大攔截器
採用責任鏈模式
攔截器依次調用的工作過程
我們再次查看RelaCall類中的同步和異步的執行方法execute()和enqueue()
final class RealCall implements Call {
//...省略一部分代碼
@Override //同步調用
public Response execute() throws IOException {
//...
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();//
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
//...
} finally {
client.dispatcher().finished(this);
}
}
@Override //異步調用
public void enqueue(Callback responseCallback) {
//...
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
final class AsyncCall extends NamedRunnable {
//...
@Override
protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();//
if (retryAndFollowUpInterceptor.isCanceled()) {//取消
signalledCallback = true;
//子線程中回調
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
//子線程中回調
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
//...
} finally {
client.dispatcher().finished(this);
}
}
}
//同步和異步都調用此方法返回Response
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));
//創建鏈條並把所有攔截器的集合傳入,注意其中index參數傳入爲0
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
}
上面的代碼可知,無論同步還是異步都調用了getResponseWithInterceptorChain()方法返回Response,getResponseWithInterceptorChain()方法中創建鏈條,並所有攔截器集合傳入這個鏈條中,需要注意index參數傳入爲0,最終調用了鏈條的proceed()。
接下來我們查看這個鏈條RealInterceptorChain類中的proceed
public final class RealInterceptorChain implements Interceptor.Chain {
//...省略一部分代碼
@Override public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
//...省略一部分代碼
//又創建了一個鏈條,並且參數index傳入值爲1(index+1 = 1)
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);//從攔截器集合中取出第0個位置的攔截器(index爲0),即重試重定向攔截器,執行攔截器的intercept()方法
Response response = interceptor.intercept(next);//將鏈條傳入第一個重試重定向攔截器RetryAndFollowUpInterceptor中
//...省略一部分代碼
return response;
}
}
我們查看RetryAndFollowUpInterceptor攔截器中的intercept()方法
public final class RetryAndFollowUpInterceptor implements Interceptor {
//...省略一部分代碼
@Override
public Response intercept(Chain chain) throws IOException {
//...
RealInterceptorChain realChain = (RealInterceptorChain) chain;//獲取到了index爲1的鏈條
//...
//調用了鏈條的proceed()方法
response = realChain.proceed(request, streamAllocation, null, null);
//...
}
}
上面的代碼首先獲取到了index爲1的鏈條,然後調用了鏈條的proceed方法。那麼又回到了RealInterceptorChain中,RealInterceptorChain的proceed方法再次執行,這次執行和上面說的執行邏輯一樣,只不過之前的index爲0,這次index爲1.以此類推,依次調用了攔截器。這就是執行攔截器的過程。
默認五個攔截器的作用:
1、重試攔截器在交出(交給下一個攔截器)之前,負責判斷用戶是否取消了請求;在獲得了結果之後,會根據響應碼判斷是否需要重定向,如果滿足條件那麼就會重啓執行所有攔截器。
2、橋接攔截器在交出之前,負責將HTTP協議必備的請求頭加入其中(如:Host)並添加一些默認的行爲(如:GZIP壓縮);在獲得了結果後,調用保存cookie接口並解析GZIP數據。
3、緩存攔截器顧名思義,交出之前讀取並判斷是否使用緩存;獲得結果後判斷是否緩存。
4、連接攔截器在交出之前,負責找到或者新建一個連接,並獲得對應的socket流;在獲得結果後不進行額外的處理。
5、請求服務器攔截器進行真正的與服務器的通信,向服務器發送數據,解析讀取的響應數據。
一、重試重定向攔截器 RetryAndFollowUpInterceptor
大體流程,發起一次網絡請求,由於網絡波動導致超時了,Okhttp會幫助我們重試。重定向就是當發起一次請求,請求成功服務器返回的響應碼爲30x,同時攜帶Location響應頭,這時框架會將Location字段讀出來,讀出攜帶的新地址,然後根據新的url重新請求。
我們都知道OkHttp是可以取消請求的,我們看RealCall中有cancel()方法,這個方法調用了重試重定向攔截器
@Override
public void cancel() {
retryAndFollowUpInterceptor.cancel();
}
如果請求在經過這個攔截器之後用戶取消了,那麼用戶的響應雖然接不到了,但此時已經和服務器進行了請求連接,但是並不代表與服務器的請求也會取消,與服務器的連接會繼續完成交互,只不過當響應返回的數據,最終返回這個重試重定向攔截器而被攔截了。不再給用戶處理了。
重試:什麼時候會重試,例如,請求一個地址,例如http://www.xxxx.com,那麼通過dns解析出的ip很有可能有多個ip(爲了減少服務器的壓力,一般都多個服務器,也就是多個ip),那麼當我們連接與第一個服務器的ip請求失敗了,會判斷是否有多個ip,如果有多個ip,那麼就會重新與下一個ip連接。
重定向:響應碼30x,響應頭攜帶Location字段,例如響應頭Location:www.baidu.com,那麼Okhttp就會自動爲你請求www.baidu.com這個鏈接。
接下來我們詳細的分析重試重定向攔截器
(一)什麼情況下Okhttp會幫我們重試?
1 路由或者IO異常,纔會重新發起請求
2 判斷OkhttpClient配置允許重試。(如果用戶配置了retryOnConnectionFailure(false),那麼久不允許了。默認是允許的。)
3 是否是可重試的異常,因爲有些異常是不會重試的。
哪些異常是可重試的異常呢?
(1)不是協議異常
例如服務器響應204,204代表無響應體,但是服務器同時響應數據包中包含Content-Length不爲0,出現了衝突。這種情況很少見。在CallServerInterceptor攔截器中,就判斷了如果返回響應204並且Content-Length大於0,那麼拋出協議異常。
(2)是Socket超時異常
(3)SSL證書(格式)沒問題
(4)SSL證書校驗沒有問題,證書匹配的情況下
4 判斷是否有多個IP。如果有才能重試。(一般一個域名如www.baidu.com,通過DNS服務器會解析出多個IP。如果有多個IP,會與其中一個服務器交互,如果失敗了就會重新與另一個服務器建立連接請求數據。)
核心代碼如下:
public final class RetryAndFollowUpInterceptor implements Interceptor {
//...省略一部分代碼
@Override
public Response intercept(Chain chain) throws IOException {
while (true) {//循環中不斷的重試
//...省略一部分代碼
try {
//調用下一個攔截器獲取響應
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//Okhttp自定義的一個異常,路由異常,有可能設置了代理,與代理服務器發生了異常。連接未成功,請求還沒發出去
//判斷是否滿足重試條件
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;//在循環中繼續重試
} catch (IOException e) {
//請求發出去了,但是和服務器通信失敗了。(socket流正在讀寫數據的時候斷開連接)(超時異常也屬於IO異常)
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
//判斷是否滿足重試條件
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
//不是前兩種的失敗,那直接關閉清理所有資源
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
//...省略一部分代碼
}
//判斷是否滿足重試條件
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
//1、在配置OkhttpClient是設置了不允許重試(默認允許),則一旦發生請求失敗就不再重試
if (!client.retryOnConnectionFailure()) return false;
//2、由於requestSendStarted只在http2的io異常中爲true,http1情況會爲false
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
return false;
//3、判斷是不是屬於重試的異常,即是不是允許重試的異常,(有些異常是不允許重試的)
if (!isRecoverable(e, requestSendStarted)) return false;
//4、不存在更多的路由,不存在更多的ip(DNS根據域名解析出來的服務器ip只有一個的情況下)
if (!streamAllocation.hasMoreRoutes()) return false;
return true;
}
//判斷是否屬於能夠重試的異常
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
//如果有協議問題,不要重試
if (e instanceof ProtocolException) {
return false;
}
if (e instanceof InterruptedIOException) {
//超時異常 重試
return e instanceof SocketTimeoutException && !requestSendStarted;
}
// 證書格式損壞,不重試
if (e instanceof SSLHandshakeException) {
if (e.getCause() instanceof CertificateException) {
return false;
}
}
//如果證書校驗失敗,不匹配,不重試
if (e instanceof SSLPeerUnverifiedException) {
return false;
}
return true;
}
}
(二)什麼情況下Okhttp會幫我們重定向?
還是RetryAndFollowUpInterceptor類中,這次我們關心重定向邏輯
public final class RetryAndFollowUpInterceptor implements Interceptor {
//...省略一部分代碼
@Override
public Response intercept(Chain chain) throws IOException {
while (true) {//循環中不斷的重試
//...省略一部分代碼
try {
//調用下一個攔截器獲取響應
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//判斷是否滿足重試條件
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 {
//...
}
//成功服務器返回結果後,纔會執行下面代碼
//...省略一部分代碼
//處理3和4xx的一些狀態碼,如301 302重定向
//followUpRequest方法中判斷是否重定向
Request followUp = followUpRequest(response, streamAllocation.route());
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
//如果followUp爲空,那麼不需要重定向,直接返回跳出循環,否則重定向
return response;
}
closeQuietly(response.body());
//限制最大重定向次數爲20次,大於20次不再重定向
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
}
}
上面的代碼中followUpRequest()方法中的邏輯是怎樣的呢?我在這裏列出了表格:
響應碼 | 說明 | 重定向條件 |
407 | 代理需要授權,如付費代理,需要驗證身份 | 通過proxyAuthenticator獲 得到了Request。 例: 添加 Proxy-Authorization 請求頭 |
401 | 服務器需要授權,如某些接口需要登陸才能使用 (不安全,基本上沒用了) | 通過authenticator獲得到了Request。 例: 添加 Authorization 請求頭 |
300、301、302、303、 |
重定向響應 | 307與308必須爲GET/HEAD請求再繼續判斷 1、用戶允許自動重定向(默認允許) 2、能夠獲得 Location 響應頭,並且值爲有效url 3、如果重定向需要由HTTP到https間切換,需要允許(默認允許) |
408 | 請求超時。服務器覺得你太慢了 | 1、用戶允許自動重試(默認允許) 2、本次請求的結果不是 響應408的重試結果 3、服務器未響應Retry-After(稍後重試),或者響應Retry-After: 0。 |
503 | 服務不可用 | 1、本次請求的結果不是 響應503的重試結果 2、服務器明確響應 Retry-After: 0,立即重試 |
當然,重定向的邏輯細節還是很多的,可能不僅僅是這些,如果感興趣,讀者可自行查看源碼分析。
二、橋接攔截器 BridgeInterceptor
他主要就是補全請求,處理響應的。幫我們添加請求頭的。同時也處理響應的一些信息,轉換成用戶可用的Response。最簡單的攔截器
我們在發起請求,我們一般情況並不用寫如下代碼
new Request.Builder()
//.addHeader("Content-Type","xxxx")//並不需要
//.addHeader("Content-Length","xxx")//並不需要
//...
.url(url)
.build();
就是因爲在橋接攔截器中已經爲我們做好了這些。
我們看一下橋接攔截器BridgeInterceptor的代碼
public final class BridgeInterceptor implements Interceptor {
private final CookieJar cookieJar;
public BridgeInterceptor(CookieJar cookieJar) {
this.cookieJar = cookieJar;
}
/**
* 添加請求頭,並默認使用gzip壓縮,同時將響應體重新設置爲gzip讀取。
*/
@Override
public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
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");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");//默認keep-Alive保持長連接
}
// If we add an "Accept-Encoding: gzip" header field we're responsible for also
// decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
//交給下一個攔截器並得到響應
Response networkResponse = chain.proceed(requestBuilder.build());
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
//客戶端支持Gzip壓縮,服務端返回的是經過Gzip壓縮.返回的頭部有body體
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
//如果服務器給的是gzip壓縮的格式,就將響應封裝成GzipSource,以Gzip的形式解析響應
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();
}
三、緩存攔截器 CacheInterceptor
緩存攔截器首先會判斷是否可以緩存,如果可以緩存就去緩存查找響應,如果不能緩存就去網絡請求,那麼如何判斷是否能夠使用緩存,這裏的細節比較多,緩存是否存在、緩存的有效期必須在有效期內、用戶的請求配置等等。 並且如果緩存過期了重新請求相應後,還要將本地的緩存更新。如果有緩存,優先使用緩存,省流量,加快了響應速度。將Response緩存序列化,採用lru算法存在了文件裏面。
作用:讀取並判斷是否使用緩存,獲得結果後判斷是否緩存
如果不是Get請求,不會緩存。
不僅緩存響應的信息,還緩存了請求信息如url、請求方法、請求頭部信息。
(一)分析緩存
我們先看看如何使用緩存
OkHttpClient client = new OkHttpClient
.Builder()
.cache(new Cache(new File("cache"), 24*1024*1024))
.build();
接下來我們分析Cache類的put和get方法
public final class Cache implements Closeable, Flushable {
//...省略一部分代碼
@Nullable CacheRequest put(Response response) {
//...
if (!requestMethod.equals("GET")) {
//不緩存非Get方法的請求與響應
return null;
}
//...
//將寫入緩存封裝成Entry,包括頭部信息、方法、url等等
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;//採用Lru算法
try {
//對url進行md5加密,作爲關鍵的key值
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
//將緩存寫入磁盤
entry.writeTo(editor);
//返回的對象主要給緩存攔截器使用
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
@Nullable Response get(Request request) {
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
//得到響應
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
}
put方法中,首先判斷是否是GET方法,只有GET方法,纔有可能緩存。然後將要緩存的信息封裝成Entry,通過DiskLruCache一個優秀的開源框架將緩存寫入到本地磁盤中。
get方法中,首先根據URL經過MD5加密計算獲取到key,根據key獲取到對應的緩存,如果有緩存那麼就得到相應。
(二)分析緩存攔截器:
接下來我們分析緩存攔截器:
**
* 負責讀取緩存以及更新緩存。
*/
public final class CacheInterceptor implements Interceptor {
final InternalCache cache;
@Override
public Response intercept(Chain chain) throws IOException {
//通過url的md5數據 從文件緩存查找 (GET請求才有緩存)
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//緩存策略:根據各種條件(請求頭)組成 請求與緩存
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();//-----1
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//...
//調用下一個攔截器
networkResponse = chain.proceed(networkRequest);
//...
//到這裏說明緩存不可用 那就使用網絡的響應
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);
}
//...
}
return response;
}
首先根據請求的URL獲取緩存Response(有可能爲空),根據這個Response對象即cacheCandidate創建了CacheStrategy對象,而CacheStrategy中進行了一系列的邏輯處理判斷。這個CacheStrategy對象中成功創建後有兩個成員變量networkRequest和cacheResonse,即一個是需要發起的請求,另一個是使用緩存。緩存攔截器通過CacheStrategy對象返回的這兩個成員變量判斷是否使用緩存或發起網絡請求,如果networkRequest存在則優先發起網絡請求,否則再使用cacheResponse緩存,若都不存在則請求失敗!
緩存攔截器就是根據CacheStrategy的networkRequest和cacheResonse來判斷使用網絡請求數據還是緩存返回數據的,總結如下表
networkRequest | cacheResponse | 說明 |
Null | Not Null | 直接使用緩存 |
Not Null | Null | 向服務器發起請求 |
Null | Null | 直接失敗,okhttp直接返回504 |
Not Null | Not Null | 發起請求,若得到響應爲304(無修改),則更新緩存響應並返回 |
接下來我們查看CacheStrategy類中是怎麼確定networkRequest和cacheResponse的:
上面的代碼註釋1可知CacheStrategy是通過get方法創建的對象,get()方法中調用了getCandidate()方法,我們主要看getCandidate()方法
public final class CacheStrategy {
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
public static class Factory {
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
//onlyIfCached代表用戶要求只使用緩存
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
//如果可以使用緩存,那networkRequest必定爲null;但這個條件成立這裏說明指定了只使用緩存但是networkRequest又不爲null,衝突。那就使返回504
return new CacheStrategy(null, null);
}
return candidate;
}
//根據各種判斷決定創建的CacheStrategy對象中的networkRequest和cacheResponse的值
private CacheStrategy getCandidate() {
//1、沒有緩存,就進行網絡請求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//2、https請求,但是緩存的響應中沒有握手信息。那麼就進行網絡請求
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//3、主要是通過響應碼以及頭部緩存控制字段判斷響應能不能緩存,不能緩存那就進行網絡請求
if (!isCacheable(cacheResponse, request)) {//-----------------1
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
//4、判斷請求頭設置Cache-Control:no-cache或者if-Modifiled-Since或者if-None-Match
if (requestCaching.noCache() || hasConditions(request)) {//----------2
return new CacheStrategy(request, null);
}
//5、如果緩存響應中存在 Cache-Control:immutable代表響應內容將一直不會改變,可以使用緩存
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
//6、根據 緩存響應的 控制緩存的響應頭 判斷是否允許使用緩存
// 6.1、獲得緩存的響應從創建到現在的時間
long ageMillis = cacheResponseAge();
// 6.2、獲取這個響應有效緩存的時長
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//如果請求中指定了 max-age 表示指定了能拿的緩存有效時長,就需要綜合響應有效緩存時長與請求能拿緩存的時長,獲得最小的能夠使用響應緩存的時長
freshMillis = Math.min(freshMillis,
SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
// 6.3 請求包含 Cache-Control:min-fresh=[秒] 能夠使用還未過指定時間的緩存 (請求認爲的緩存有效時間)
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
// 6.4 Cache-Control:max-stale=[秒] 緩存過期後還能使用指定的時長 如果未指定多少秒,則表示無論過期多長時間都可以;如果指定了,則只要是指定時間內就能使用緩存
// 前者會忽略後者,所以判斷了不必須向服務器確認,再獲得請求頭中的max-stale
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
// 6.5 不需要與服務器驗證有效性 && 響應存在的時間+請求認爲的緩存有效時間 小於 緩存有效時長+過期後還可以使用的時間
// 允許使用緩存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
//如果已過期,但未超過 過期後繼續使用時長,那還可以繼續使用,只用添加相應的頭部字段
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
//如果緩存已超過一天並且響應中沒有設置過期時間也需要添加警告
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
// 7、緩存過期了
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
// 如果設置了 If-None-Match/If-Modified-Since 服務器是可能返回304(無修改)的,使用緩存的響應體
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
}
}
1、判斷是否存在緩存
沒有緩存,就進行網絡請求
2、是https請求,但是緩存的響應中沒有握手信息。那麼就進行網絡請求
3、主要是通過響應碼以及頭部緩存控制字段判斷響應能不能緩存,不能緩存那就進行網絡請求
上面代碼註釋1,isCacheable()方法中,主要是通過響應碼以及頭部緩存控制字段判斷響應能不能緩存,不能緩存那就進行網絡請求,例如響應頭返回了Cache-Control:no-store,表示服務器不希望你緩存,那麼就不能緩存,需要發起網絡請求。
4、判斷請求頭設置Cache-Control:no-cache或者if-Modifiled-Since或者if-None-Match
上面的代碼註釋2,如果 請求包含:CacheControl:no-cache 需要與服務器驗證緩存有效性,(CacheControl:no-cache是用戶不想緩存,用戶就像去請求網絡從服務器獲取數據)。或者請求頭包含 If-Modified-Since:時間 值爲lastModified或者date 如果服務器沒有在該頭部指定的時間之後修改了請求的數據,服務器返回304(無修改)。 或者請求頭包含 If-None-Match:值就是Etag(資源標記)服務器將其與存在服務端的Etag值進行比較;如果匹配,返回304。 請求頭中只要存在三者中任意一個,進行網絡請求
5、如果緩存響應中存在 Cache-Control:immutable代表響應內容將一直不會改變,可以使用緩存
6、判斷緩存有效性。緩存存活時間<緩存有效時間
例如返回響應頭包含了max-age字段,並返回的值爲100秒,代表了資源最大有效時間爲100秒,後續如果在100s之內發起請求就可以使用緩存數據返回給用戶。
這裏面有很多小的細節,內容比較多,具體如下:
(1)緩存存活時間是如何獲取的?
我們看CacheStrategy類中的cacheResponseAge()方法
//響應已存在多少時間
private long cacheResponseAge() {
//客戶端發出響應到服務器發出響應的時間差
long apparentReceivedAge = servedDate != null
? Math.max(0, receivedResponseMillis - servedDate.getTime())
: 0;
//客戶端的緩存在收到時就已經存在了多久
long receivedAge = ageSeconds != -1
? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
: apparentReceivedAge;
//從發起請求到收到響應的時間差
long responseDuration = receivedResponseMillis - sentRequestMillis;
//現在與收到響應的時間差
long residentDuration = nowMillis - receivedResponseMillis;
//響應已存在的總時長
return receivedAge + responseDuration + residentDuration;
}
上面代碼
① apparentReceivedAge,客戶端發出響應到服務器發出響應的時間差。
servedDate.getTime()是服務器返回的時間,即Date響應頭對應的時間。receivedResponseMillis 是客戶端響應的時間。兩者的時間差就代表服務器到達客戶端這段網絡傳輸的時間,即客戶端發出響應到服務器發出響應的時間差
② receivedAge,客戶端的緩存在收到時就已經在服務器中存在了多久
ageSeconds變量是從緩存中獲取的Age響應頭對應的秒數。
我們要知道不僅僅是客戶端可以做緩存,服務器也會有緩存的。Age響應頭代表響應對象在代理緩存中存在的時間,以秒爲單位。解釋:意思是本地緩存的響應是由服務器的緩存返回的。同時也代表着這個緩存在服務器已經存在的時間。例如向發服務器起一個請求,服務器給我們響應,這個響應有可能在服務器已經緩存了100秒之後的緩存數據,那麼返回的age字段的值就是100秒。Age字段的數據意味着我們客戶端獲取到的數據並不是最新的,是服務器緩存的,已經有年齡了。
(2)緩存新鮮度(有效時間)
意思是緩存還能活多長時間
我們分析CacheStrategy類中的computeFreshnessLifetime()方法
//計算緩存的存活時間
private long computeFreshnessLifetime() {
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.maxAgeSeconds() != -1) {//max-age有值
return SECONDS.toMillis(responseCaching.maxAgeSeconds());
} else if (expires != null) {//Expires響應頭有值
//接收的響應時間
long servedMillis = servedDate != null
? servedDate.getTime()
: receivedResponseMillis;
//剩餘時間
long delta = expires.getTime() - servedMillis;
return delta > 0 ? delta : 0;
} else if (lastModified != null
&& cacheResponse.request().url().query() == null) {
// As recommended by the HTTP RFC and implemented in Firefox, the
// max age of a document should be defaulted to 10% of the
// document's age at the time it was served. Default expiration
// dates aren't used for URIs containing a query.
long servedMillis = servedDate != null
? servedDate.getTime()
: sentRequestMillis;
long delta = servedMillis - lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
① 緩存響應包含Cache-Control:max-age=(秒)資源最大有效時間。
如果你的響應包含了這個值,那麼直接拿這個值就代表緩存能活多少時間。例如Cache-Control:max-age=100,代表能存活100秒
② 緩存響應包含Expires:時間,則通過Date或接受該響應時間計算資源有效時間
如果沒有包含Cache-Control,那麼就會判斷Expires所返回的時間。返回的是時間戳,是一個具體的時間。例如2019年9月27下午18:40,那麼在2019年9月27下午18:40之前請求數據就可以使用這個緩存。
③ 緩存響應包含Last-Modified:時間,則通過Date或發送該響應請求的時間計算資源有效時間,並且根據建議以及在Firefox瀏覽器的實現,使用得到結果的10%來作爲資源的有效時間。這個時間是一個大概的時間計算。前兩種是一個精準的時間計算。
(3)緩存最小新鮮度
如果用戶配置添加了這樣的請求頭:addHeader("Cache-Control","min-fresh=20")。代表用戶自己認爲的緩存有效時間。
假設存在max-age=100,min-fresh=20。這代表了用戶認爲這個緩存的響應,從服務器創建響應到能夠緩存使用的時間爲100-20=80s。
(4)緩存過期後仍有效時長:maxStaleMillis
如果用戶設置了這個請求頭爲10s,那麼緩存過期後,沒有超過10s,那麼任然可以使用。
總而言之,攔截器需要判斷緩存,判斷是否能夠緩存,判斷用戶的請求配置緩存,判斷緩存的有效期內。
四、連接攔截器 ConnectInterceptor
發起http請求是要建立Socket連接的。Socket連接相當於一個管道,只要沒有關閉,是可以重複使用的。如果Http一起請求建立了Socket連接,這個連接如果是可以保持長連接的,那麼就會把這個連接放入到連接池中。下一次發起請求,如果是同一臺服務器的話,就可以從連接池中取出已緩存的Socket連接,複用這個連接,給服務器發起請求。鏈接攔截器中還有清理任務,自動將無用的鏈接清理掉。
作用:負責找到或者新建一個連接,並獲得對應的socket流。並把無用的連接清理掉。
首先分析ConnectInterceptor類
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;
public ConnectInterceptor(OkHttpClient client) {
this.client = client;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
//獲取到前面攔截器傳過來的StreamAllocation對象
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
//HttpCodec這個對象主要是編碼Request和解碼Response的
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
//RealConnection對象比較重要,它是進行實際網絡IO
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
連接攔截器的兩個步驟就是,1 獲取Interceptor傳過來的StramAllocation對象,根據這個對象獲取HttpCodec對象。2 將創建的用於網絡IO的RealConnection對象以及對於服務器交互最爲關鍵的HttpCodec等對象傳遞給後面的攔截器。
(一)連接池ConnectionPool:
接下來分析重要的連接池ConnectionPool類中的代碼:
public final class 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));
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);//默認5個連接,默認空閒5分鐘
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {//遍歷所有連接
//根據這次的address和rote與連接池中的連接判斷,判斷證書、host、port等等是否一致
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {//如果清理任務未執行
cleanupRunning = true;
executor.execute(cleanupRunnable);//執行清理線程
}
//添加到連接池隊列中
connections.add(connection);
}
@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;
}
}
構造方法,默認連接池中的連接最多有5個,最長閒置時間爲5分鐘。當然也是可以用戶自由配置。
put方法,很簡單,將連接添加到連接池中。如果清理線程沒有執行,那麼就執行清理任務
get方法,遍歷所有連接,根據這次的信息和連接池中的判斷,判斷NDS解析服務器、證書,代理,協議、host、port等等是否一致,如果一致返回這個可以複用的連接。
(二)連接池的回收
ConnectionPool類中還有清理機制。在線程中不斷的每隔一段時間進行清理
我們來看ConnectionPool類中的cleanupRunnable這個任務
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
//最快多久再清理,cleanup返回下一次清理的時間
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) {
}
}
}
}
}
};
//使用了javaGC回收算法---標記清除算法
long cleanup(long now) {
// Find either a connection to evict, or the time that the next eviction is due.
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;
}
}
//池中最長閒置時間超過了保活時間(5分鐘)或者池內數量超過了(5個)馬上移除,馬上再次檢查
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {//longestIdleDurationNs池中最長閒置時間
//移除
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
//池內存在閒置連接,就等待 保活時間(5分鐘)-最長閒置時間 = 還能閒置多久 再檢查
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//有使用中的連接,就等待5分鐘 再檢查
return keepAliveDurationNs;
} else {
//可能池內沒有任何連接,直接停止清理。put後再次啓動
cleanupRunning = false;
return -1;
}
}
//關閉socket連接
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
池中最長閒置時間超過了保活時間(5分鐘)或者池內數量超過了(5個)馬上移除,並且關閉socket連接。
如何建立連接的呢?
連接分爲無代理連接和代理連接,代理連接有Soks代理連接和http代理連接,其中http代理連接分爲http或https代理連接。這在okhttp源碼要在RealConnection類中都有體現。
五 請求攔截器CallServerInterceptor
將Request對象組裝成http報文寫到流中發送給服務器,並且把服務器返回的響應信息組裝成Response對象給用戶使用。
進行真正的與服務器的通信,向服務器發送數據,解析讀取的響應數據。
第四個攔截器連接攔截器雖然建立了連接,但是需要把請求的數據編碼成請求的報文(字符串)給服務器,響應也是一樣,服務器給我們的數據要解析成Response
public final class CallServerInterceptor implements Interceptor {
//...
@Override
public Response intercept(Chain chain) throws IOException {
//...
//將request對象頭部信息轉換成http請求報文字符串,寫入Socket流中
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
//如果包含Expect:100-continue請求頭,那麼先暫時不發送請求體,先等待服務器返回的響應
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
//讀取服務器的響應,如果服務器返回100,那麼返回對象爲null
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
//...
//將請求體寫入
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);
//讀取響應頭部信息
}
//...
if (code == 100) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
//讀取服務器的響應
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
//...
return response;
}
//...
}
1 將request對象頭部信息轉換成http請求報文字符串,寫入Socket流中
2 向Socket中寫入body信息
3 調用httpCodec.finishRequest()表示整個的網絡請求寫入完成
4 讀取響應的頭部信息
5 讀取響應的body信息