OkHttp原理解析
OkHttp 3.10.0版本,最新OkHttp為:4.0.1邏輯與3版本并沒有太大變化邻储,但是改為kotlin實(shí)現(xiàn)。
OkHttp介紹
OkHttp是當(dāng)下Android使用最頻繁的網(wǎng)絡(luò)請求框架旧噪,由Square公司開源吨娜。Google在Android4.4以后開始將源碼中的HttpURLConnection底層實(shí)現(xiàn)替換為OKHttp,同時(shí)現(xiàn)在流行的Retrofit框架底層同樣是使用OKHttp的淘钟。
優(yōu)點(diǎn):
- 支持Spdy蕉扮、Http1.X票渠、Http2、Quic以及WebSocket
- 連接池復(fù)用底層TCP(Socket),減少請求延時(shí)
- 無縫的支持GZIP減少數(shù)據(jù)流量
- 緩存響應(yīng)數(shù)據(jù)減少重復(fù)的網(wǎng)絡(luò)請求
- 請求失敗自動(dòng)重試主機(jī)的其他ip酬蹋,自動(dòng)重定向
- …….
使用流程
[圖片上傳失敗...(image-8e958d-1572367003543)]
在使用OkHttp發(fā)起一次請求時(shí)乡话,對于使用者最少存在OkHttpClient
势似、Request
與Call
三個(gè)角色新娜。其中OkHttpClient
和Request
的創(chuàng)建可以使用它為我們提供的Builder
(建造者模式)。而Call
則是把Request
交給OkHttpClient
之后返回的一個(gè)已準(zhǔn)備好執(zhí)行的請求慧耍。
建造者模式:將一個(gè)復(fù)雜的構(gòu)建與其表示相分離身辨,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示。實(shí)例化OKHttpClient和Request的時(shí)候芍碧,因?yàn)橛刑嗟膶傩孕枰O(shè)置煌珊,而且開發(fā)者的需求組合千變?nèi)f化,使用建造者模式可以讓用戶不需要關(guān)心這個(gè)類的內(nèi)部細(xì)節(jié)师枣,配置好后怪瓶,建造者會(huì)幫助我們按部就班的初始化表示對象
同時(shí)OkHttp在設(shè)計(jì)時(shí)采用的門面模式,將整個(gè)系統(tǒng)的復(fù)雜性給隱藏起來践美,將子系統(tǒng)接口通過一個(gè)客戶端OkHttpClient統(tǒng)一暴露出來。
OkHttpClient
中全是一些配置找岖,比如代理的配置陨倡、ssl證書的配置等。而Call
本身是一個(gè)接口许布,我們獲得的實(shí)現(xiàn)為:RealCall
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
Call
的execute
代表了同步請求兴革,而enqueue
則代表異步請求。兩者唯一區(qū)別在于一個(gè)會(huì)直接發(fā)起網(wǎng)絡(luò)請求,而另一個(gè)使用OkHttp內(nèi)置的線程池來進(jìn)行杂曲。這就涉及到OkHttp的任務(wù)分發(fā)器庶艾。
分發(fā)器
Dispatcher
,分發(fā)器就是來調(diào)配請求任務(wù)的擎勘,內(nèi)部會(huì)包含一個(gè)線程池咱揍。可以在創(chuàng)建OkHttpClient
時(shí)棚饵,傳遞我們自己定義的線程池來創(chuàng)建分發(fā)器煤裙。
這個(gè)Dispatcher中的成員有:
//異步請求同時(shí)存在的最大請求
private int maxRequests = 64;
//異步請求同一域名同時(shí)存在的最大請求
private int maxRequestsPerHost = 5;
//閑置任務(wù)(沒有請求時(shí)可執(zhí)行一些任務(wù),由使用者設(shè)置)
private @Nullable Runnable idleCallback;
//異步請求使用的線程池
private @Nullable ExecutorService executorService;
//異步請求等待執(zhí)行隊(duì)列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//異步請求正在執(zhí)行隊(duì)列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//同步請求正在執(zhí)行隊(duì)列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
同步請求
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
因?yàn)橥秸埱蟛恍枰€程池噪漾,也不存在任何限制硼砰。所以分發(fā)器僅做一下記錄。
異步請求
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
當(dāng)正在執(zhí)行的任務(wù)未超過最大限制64欣硼,同時(shí)runningCallsForHost(call) < maxRequestsPerHost
同一Host的請求不超過5個(gè)题翰,則會(huì)添加到正在執(zhí)行隊(duì)列,同時(shí)提交給線程池诈胜。否則先加入等待隊(duì)列豹障。
加入線程池直接執(zhí)行沒啥好說的,但是如果加入等待隊(duì)列后耘斩,就需要等待有空閑名額才開始執(zhí)行沼填。因此每次執(zhí)行完一個(gè)請求后,都會(huì)調(diào)用分發(fā)器的finished
方法
//異步請求調(diào)用
void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}
//同步請求調(diào)用
void finished(RealCall call) {
finished(runningSyncCalls, call, false);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
//不管異步還是同步括授,執(zhí)行完后都要從隊(duì)列移除(runningSyncCalls/runningAsyncCalls)
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
//異步任務(wù)和同步任務(wù)正在執(zhí)行的和
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
// 沒有任務(wù)執(zhí)行執(zhí)行閑置任務(wù)
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
需要注意的是 只有異步任務(wù)才會(huì)存在限制與等待坞笙,所以在執(zhí)行完了移除正在執(zhí)行隊(duì)列中的元素后,異步任務(wù)結(jié)束會(huì)執(zhí)行promoteCalls()
荚虚。很顯然這個(gè)方法肯定會(huì)重新調(diào)配請求薛夜。
private void promoteCalls() {
//如果任務(wù)滿了直接返回
if (runningAsyncCalls.size() >= maxRequests) return;
//沒有等待執(zhí)行的任務(wù),返回
if (readyAsyncCalls.isEmpty()) return;
//遍歷等待執(zhí)行隊(duì)列
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
//等待任務(wù)想要執(zhí)行版述,還需要滿足:這個(gè)等待任務(wù)請求的Host不能已經(jīng)存在5個(gè)了
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
在滿足條件下梯澜,會(huì)把等待隊(duì)列中的任務(wù)移動(dòng)到runningAsyncCalls
并交給線程池執(zhí)行。所以分發(fā)器到這里就完了渴析。邏輯上還是非常簡單的晚伙。
請求流程
用戶是不需要直接操作任務(wù)分發(fā)器的,獲得的RealCall
中就分別提供了execute
與enqueue
來開始同步請求或異步請求俭茧。
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
//調(diào)用分發(fā)器
client.dispatcher().executed(this);
//執(zhí)行請求
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);
}
}
異步請求的后續(xù)同時(shí)是調(diào)用getResponseWithInterceptorChain()
來執(zhí)行請求
@Override
public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
//調(diào)用分發(fā)器
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
如果該RealCall
已經(jīng)執(zhí)行過了咆疗,再次執(zhí)行是不允許的。異步請求會(huì)把一個(gè)AsyncCall
提交給分發(fā)器母债。
AsyncCall
實(shí)際上是一個(gè)Runnable
的子類,使用線程啟動(dòng)一個(gè)Runnable
時(shí)會(huì)執(zhí)行run
方法午磁,在AsyncCall
中被重定向到execute
方法:
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
//線程池執(zhí)行
@Override
protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
//.......
} catch (IOException e) {
//......
} finally {
//請求完成
client.dispatcher().finished(this);
}
}
}
public abstract class NamedRunnable implements Runnable {
protected final String name;
public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);
}
@Override
public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
同時(shí)AsyncCall
也是RealCall
的普通內(nèi)部類尝抖,這意味著它是持有外部類RealCall
的引用,可以獲得直接調(diào)用外部類的方法迅皇。
可以看到無論是同步還是異步請求實(shí)際上真正執(zhí)行請求的工作都在getResponseWithInterceptorChain()
中昧辽。這個(gè)方法就是整個(gè)OkHttp的核心:攔截器責(zé)任鏈。但是在介紹責(zé)任鏈之前登颓,我們再來回顧一下線程池的基礎(chǔ)知識(shí)搅荞。
分發(fā)器線程池
前面我們提過,分發(fā)器就是來調(diào)配請求任務(wù)的挺据,內(nèi)部會(huì)包含一個(gè)線程池取具。當(dāng)異步請求時(shí),會(huì)將請求任務(wù)交給線程池來執(zhí)行扁耐。那分發(fā)器中默認(rèn)的線程池是如何定義的呢暇检?為什么要這么定義?
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(
0, //核心線程
Integer.MAX_VALUE, //最大線程
60, //空閑線程閑置時(shí)間
TimeUnit.SECONDS, //閑置時(shí)間單位
new SynchronousQueue<Runnable>(), //線程等待隊(duì)列
Util.threadFactory("OkHttp Dispatcher", false) //線程創(chuàng)建工廠
);
}
return executorService;
}
在OkHttp的分發(fā)器中的線程池定義如上婉称,其實(shí)就和Executors.newCachedThreadPool()
創(chuàng)建的線程一樣块仆。首先核心線程為0,表示線程池不會(huì)一直為我們緩存線程王暗,線程池中所有線程都是在60s內(nèi)沒有工作就會(huì)被回收悔据。而最大線程Integer.MAX_VALUE
與等待隊(duì)列SynchronousQueue
的組合能夠得到最大的吞吐量。即當(dāng)需要線程池執(zhí)行任務(wù)時(shí)俗壹,如果不存在空閑線程不需要等待科汗,馬上新建線程執(zhí)行任務(wù)!等待隊(duì)列的不同指定了線程池的不同排隊(duì)機(jī)制绷雏。一般來說头滔,等待隊(duì)列BlockingQueue
有:ArrayBlockingQueue
、LinkedBlockingQueue
與SynchronousQueue
涎显。
假設(shè)向線程池提交任務(wù)時(shí)坤检,核心線程都被占用的情況下:
ArrayBlockingQueue
:基于數(shù)組的阻塞隊(duì)列,初始化需要指定固定大小期吓。
? 當(dāng)使用此隊(duì)列時(shí)早歇,向線程池提交任務(wù),會(huì)首先加入到等待隊(duì)列中讨勤,當(dāng)?shù)却?duì)列滿了之后箭跳,再次提交任務(wù),嘗試加入隊(duì)列就會(huì)失敗潭千,這時(shí)就會(huì)檢查如果當(dāng)前線程池中的線程數(shù)未達(dá)到最大線程衅码,則會(huì)新建線程執(zhí)行新提交的任務(wù)。所以最終可能出現(xiàn)后提交的任務(wù)先執(zhí)行脊岳,而先提交的任務(wù)一直在等待。
LinkedBlockingQueue
:基于鏈表實(shí)現(xiàn)的阻塞隊(duì)列,初始化可以指定大小割捅,也可以不指定奶躯。
? 當(dāng)指定大小后,行為就和ArrayBlockingQueu
一致亿驾。而如果未指定大小嘹黔,則會(huì)使用默認(rèn)的Integer.MAX_VALUE
作為隊(duì)列大小。這時(shí)候就會(huì)出現(xiàn)線程池的最大線程數(shù)參數(shù)無用莫瞬,因?yàn)闊o論如何儡蔓,向線程池提交任務(wù)加入等待隊(duì)列都會(huì)成功。最終意味著所有任務(wù)都是在核心線程執(zhí)行疼邀。如果核心線程一直被占喂江,那就一直等待。
SynchronousQueue
: 無容量的隊(duì)列旁振。
? 使用此隊(duì)列意味著希望獲得最大并發(fā)量获询。因?yàn)闊o論如何,向線程池提交任務(wù)拐袜,往隊(duì)列提交任務(wù)都會(huì)失敗吉嚣。而失敗后如果沒有空閑的非核心線程,就會(huì)檢查如果當(dāng)前線程池中的線程數(shù)未達(dá)到最大線程蹬铺,則會(huì)新建線程執(zhí)行新提交的任務(wù)尝哆。完全沒有任何等待,唯一制約它的就是最大線程數(shù)的個(gè)數(shù)甜攀。因此一般配合Integer.MAX_VALUE
就實(shí)現(xiàn)了真正的無等待秋泄。
但是需要注意的時(shí),我們都知道赴邻,進(jìn)程的內(nèi)存是存在限制的印衔,而每一個(gè)線程都需要分配一定的內(nèi)存。所以線程并不能無限個(gè)數(shù)姥敛。那么當(dāng)設(shè)置最大線程數(shù)為Integer.MAX_VALUE
時(shí)奸焙,OkHttp同時(shí)還有最大請求任務(wù)執(zhí)行個(gè)數(shù): 64的限制。這樣即解決了這個(gè)問題同時(shí)也能獲得最大吞吐彤敛。
攔截器責(zé)任鏈
OkHttp最核心的工作是在getResponseWithInterceptorChain()
中進(jìn)行与帆,在進(jìn)入這個(gè)方法分析之前,我們先來了解什么是責(zé)任鏈模式墨榄,因?yàn)榇朔椒ň褪抢玫呢?zé)任鏈模式完成一步步的請求玄糟。
責(zé)任鏈顧名思義就是由一系列的負(fù)責(zé)者構(gòu)成的一個(gè)鏈條,類似于工廠流水線袄秩,你們懂的阵翎,很多同學(xué)的男朋友/女朋友就是這么來的逢并。
責(zé)任鏈模式
為請求創(chuàng)建了一個(gè)接收者對象的鏈。這種模式給予請求的類型郭卫,對請求的發(fā)送者和接收者進(jìn)行解耦砍聊。在這種模式中,通常每個(gè)接收者都包含對另一個(gè)接收者的引用贰军。如果一個(gè)對象不能處理該請求玻蝌,那么它會(huì)把相同的請求傳給下一個(gè)接收者,依此類推词疼。比如:
七夕節(jié)剛過去俯树。周周同學(xué)(我也不知道為什么第一個(gè)想到的就是周周同學(xué))在讀書的時(shí)候就是單身狗一條,看到自習(xí)室每天都很多美女后贰盗,每天晚上跑去自習(xí)都干同一件事情许饿。
周周每天晚上都坐到自習(xí)室最后一排,找張紙條寫上:“Hi,可以做我的女朋友嗎童太?我的特長就是特別的長米辐,如果不愿意請向前傳”。紙條就一個(gè)接一個(gè)的傳上去了书释,最后傳給了掃地阿姨翘贮。最后和掃地阿姨過上了幸福的生活,這真是一個(gè)....令人高興的故事爆惧。
那整個(gè)過程是什么樣子的呢狸页?
//傳送者
abstract class Transmit{
//責(zé)任鏈中下一個(gè)傳遞者
protected Transmit nextTransmit;
boolean request(String msg);
public void setNextTransmit(Transmit transmit){
nextTransmit = transmit;
}
}
public class Zero extends Transmit{
public boolean request(String msg){
boolean resp = nextTransmit.request(msg);
return resp;
}
}
public class Alvin extends Transmit{
public boolean request(String msg){
boolean resp = nextTransmit.request();
return resp;
}
}
public class Lucy extends Transmit{
public boolean request(String msg){
return true;
}
}
private static Transmit getTransmits(){
Transmit zero = new Zero();
Transmit alvin = new Alvin();
Lucy lucy = new Lucy();
zero.setNextTransmit(alvin);
alvin.setNextTransmit(lucy);
return errorLogger;
}
public static void main(String[] args) {
Transmit transmit = getTransmits();
}
在責(zé)任鏈模式中,每一個(gè)對象對其下家的引用而接起來形成一條鏈扯再。請求在這個(gè)鏈上傳遞芍耘,直到鏈上的某一個(gè)對象決定處理此請求∠ㄗ瑁客戶并不知道鏈上的哪一個(gè)對象最終處理這個(gè)請求斋竞,系統(tǒng)可以在不影響客戶端的 情況下動(dòng)態(tài)的重新組織鏈和分配責(zé)任。處理者有兩個(gè)選擇:承擔(dān)責(zé)任或者把責(zé)任推給下家秃殉。一個(gè)請求可以最終不被任何接收端對象所接受坝初。
攔截器流程
而OkHttp中的getResponseWithInterceptorChain()
中經(jīng)歷的流程為
[圖片上傳失敗...(image-f4ed8d-1572367003543)]
請求會(huì)被交給責(zé)任鏈中的一個(gè)個(gè)攔截器。默認(rèn)情況下有五大攔截器:
-
RetryAndFollowUpInterceptor
第一個(gè)接觸到請求钾军,最后接觸到響應(yīng)鳄袍;負(fù)責(zé)判斷是否需要重新發(fā)起整個(gè)請求
-
BridgeInterceptor
補(bǔ)全請求,并對響應(yīng)進(jìn)行額外處理
-
CacheInterceptor
請求前查詢緩存吏恭,獲得響應(yīng)并判斷是否需要緩存
-
ConnectInterceptor
與服務(wù)器完成TCP連接
-
CallServerInterceptor
與服務(wù)器通信拗小;封裝請求數(shù)據(jù)與解析響應(yīng)數(shù)據(jù)(如:HTTP報(bào)文)
攔截器詳情
一、重試及重定向攔截器
第一個(gè)攔截器:RetryAndFollowUpInterceptor
樱哼,主要就是完成兩件事情:重試與重定向哀九。
重試
請求階段發(fā)生了 RouteException 或者 IOException會(huì)進(jìn)行判斷是否重新發(fā)起請求剿配。
RouteException
catch (RouteException e) {
//todo 路由異常,連接未成功勾栗,請求還沒發(fā)出去
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
}
IOException
catch (IOException e) {
//todo 請求發(fā)出去了惨篱,但是和服務(wù)器通信失敗了。(socket流正在讀寫數(shù)據(jù)的時(shí)候斷開連接)
// ConnectionShutdownException只對HTTP2存在。假定它就是false
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
}
兩個(gè)異常都是根據(jù)recover
方法判斷是否能夠進(jìn)行重試,如果返回true
瓷们,則表示允許重試雁竞。
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
//todo 1、在配置OkhttpClient是設(shè)置了不允許重試(默認(rèn)允許)漾抬,則一旦發(fā)生請求失敗就不再重試
if (!client.retryOnConnectionFailure()) return false;
//todo 2宿亡、由于requestSendStarted只在http2的io異常中為true,先不管http2
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
return false;
//todo 3纳令、判斷是不是屬于重試的異常
if (!isRecoverable(e, requestSendStarted)) return false;
//todo 4挽荠、有沒有可以用來連接的路由路線
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
所以首先使用者在不禁止重試的前提下,如果出現(xiàn)了某些異常平绩,并且存在更多的路由線路圈匆,則會(huì)嘗試換條線路進(jìn)行請求的重試。其中某些異常是在isRecoverable
中進(jìn)行判斷:
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
// 出現(xiàn)協(xié)議異常捏雌,不能重試
if (e instanceof ProtocolException) {
return false;
}
// requestSendStarted認(rèn)為它一直為false(不管http2),異常屬于socket超時(shí)異常,直接判定可以重試
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
// SSL握手異常中跃赚,證書出現(xiàn)問題,不能重試
if (e instanceof SSLHandshakeException) {
if (e.getCause() instanceof CertificateException) {
return false;
}
}
// SSL握手未授權(quán)異常 不能重試
if (e instanceof SSLPeerUnverifiedException) {
return false;
}
return true;
}
1性湿、協(xié)議異常纬傲,如果是那么直接判定不能重試;(你的請求或者服務(wù)器的響應(yīng)本身就存在問題,沒有按照http協(xié)議來定義數(shù)據(jù)肤频,再重試也沒用)
2叹括、超時(shí)異常,可能由于網(wǎng)絡(luò)波動(dòng)造成了Socket管道的超時(shí)宵荒,那有什么理由不重試汁雷?(后續(xù)還會(huì)涉及到路由)
3、SSL證書異常/SSL驗(yàn)證失敗異常骇扇,前者是證書驗(yàn)證失敗摔竿,后者可能就是壓根就沒證書,或者證書數(shù)據(jù)不正確少孝,那還怎么重試
經(jīng)過了異常的判定之后继低,如果仍然允許進(jìn)行重試,就會(huì)再檢查當(dāng)前有沒有可用路由路線來進(jìn)行連接稍走。簡單來說袁翁,比如 DNS 對域名解析后可能會(huì)返回多個(gè) IP柴底,在一個(gè)IP失敗后,嘗試另一個(gè)IP進(jìn)行重試粱胜。
重定向
如果請求結(jié)束后沒有發(fā)生異常并不代表當(dāng)前獲得的響應(yīng)就是最終需要交給用戶的柄驻,還需要進(jìn)一步來判斷是否需要重定向的判斷。重定向的判斷位于followUpRequest
方法
private Request followUpRequest(Response userResponse) throws IOException {
if (userResponse == null) throw new IllegalStateException();
Connection connection = streamAllocation.connection();
Route route = connection != null
? connection.route()
: null;
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
// 407 客戶端使用了HTTP代理服務(wù)器焙压,在請求頭中添加 “Proxy-Authorization”鸿脓,讓代理服務(wù)器授權(quán)
case HTTP_PROXY_AUTH:
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
// 401 需要身份驗(yàn)證 有些服務(wù)器接口需要驗(yàn)證使用者身份 在請求頭中添加 “Authorization”
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
// 308 永久重定向
// 307 臨時(shí)重定向
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// 如果請求方式不是GET或者HEAD,框架不會(huì)自動(dòng)重定向請求
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// 300 301 302 303
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// 如果用戶不允許重定向涯曲,那就返回null
if (!client.followRedirects()) return null;
// 從響應(yīng)頭取出location
String location = userResponse.header("Location");
if (location == null) return null;
// 根據(jù)location 配置新的請求 url
HttpUrl url = userResponse.request().url().resolve(location);
// 如果為null野哭,說明協(xié)議有問題,取不出來HttpUrl幻件,那就返回null拨黔,不進(jìn)行重定向
if (url == null) return null;
// 如果重定向在http到https之間切換,需要檢查用戶是不是允許(默認(rèn)允許)
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
Request.Builder requestBuilder = userResponse.request().newBuilder();
/**
* 重定向請求中 只要不是 PROPFIND 請求绰沥,無論是POST還是其他的方法都要改為GET請求方式篱蝇,
* 即只有 PROPFIND 請求才能有請求體
*/
//請求不是get與head
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
// 除了 PROPFIND 請求之外都改成GET請求
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
// 不是 PROPFIND 的請求,把請求頭中關(guān)于請求體的數(shù)據(jù)刪掉
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// 在跨主機(jī)重定向時(shí)徽曲,刪除身份驗(yàn)證請求頭
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408 客戶端請求超時(shí)
case HTTP_CLIENT_TIMEOUT:
// 408 算是連接失敗了零截,所以判斷用戶是不是允許重試
if (!client.retryOnConnectionFailure()) {
return null;
}
// UnrepeatableRequestBody實(shí)際并沒發(fā)現(xiàn)有其他地方用到
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
// 如果是本身這次的響應(yīng)就是重新請求的產(chǎn)物同時(shí)上一次之所以重請求還是因?yàn)?08,那我們這次不再重請求了
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
return null;
}
// 如果服務(wù)器告訴我們了 Retry-After 多久后重試疟位,那框架不管了瞻润。
if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
// 503 服務(wù)不可用 和408差不多,但是只在服務(wù)器告訴你 Retry-After:0(意思就是立即重試) 才重請求
case HTTP_UNAVAILABLE:
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
return userResponse.request();
}
return null;
default:
return null;
}
}
整個(gè)是否需要重定向的判斷內(nèi)容很多甜刻,記不住绍撞,這很正常,關(guān)鍵在于理解他們的意思得院。如果此方法返回空傻铣,那就表示不需要再重定向了,直接返回響應(yīng)祥绞;但是如果返回非空非洲,那就要重新請求返回的Request
,但是需要注意的是蜕径,我們的followup
在攔截器中定義的最大次數(shù)為20次两踏。
總結(jié)
本攔截器是整個(gè)責(zé)任鏈中的第一個(gè),這意味著它會(huì)是首次接觸到Request
與最后接收到Response
的角色兜喻,在這個(gè)攔截器中主要功能就是判斷是否需要重試與重定向梦染。
重試的前提是出現(xiàn)了RouteException
或者IOException
。一但在后續(xù)的攔截器執(zhí)行過程中出現(xiàn)這兩個(gè)異常,就會(huì)通過recover
方法進(jìn)行判斷是否進(jìn)行連接重試帕识。
重定向發(fā)生在重試的判定之后泛粹,如果不滿足重試的條件,還需要進(jìn)一步調(diào)用followUpRequest
根據(jù)Response
的響應(yīng)碼(當(dāng)然肮疗,如果直接請求失敗晶姊,Response
都不存在就會(huì)拋出異常)。followup
最大發(fā)生20次伪货。
二们衙、橋接攔截器
BridgeInterceptor
,連接應(yīng)用程序和服務(wù)器的橋梁超歌,我們發(fā)出的請求將會(huì)經(jīng)過它的處理才能發(fā)給服務(wù)器砍艾,比如設(shè)置請求內(nèi)容長度,編碼巍举,gzip壓縮,cookie等凝垛,獲取響應(yīng)后保存Cookie等操作懊悯。這個(gè)攔截器相對比較簡單。
補(bǔ)全請求頭:
請求頭 | 說明 |
---|---|
Content-Type |
請求體類型,如:application/x-www-form-urlencoded
|
Content-Length /Transfer-Encoding
|
請求體解析方式 |
Host |
請求的主機(jī)站點(diǎn) |
Connection: Keep-Alive |
保持長連接 |
Accept-Encoding: gzip |
接受響應(yīng)支持gzip壓縮 |
Cookie |
cookie身份辨別 |
User-Agent |
請求的用戶信息梦皮,如:操作系統(tǒng)炭分、瀏覽器等 |
在補(bǔ)全了請求頭后交給下一個(gè)攔截器處理,得到響應(yīng)后剑肯,主要干兩件事情:
1捧毛、保存cookie,在下次請求則會(huì)讀取對應(yīng)的數(shù)據(jù)設(shè)置進(jìn)入請求頭让网,默認(rèn)的CookieJar
不提供實(shí)現(xiàn)
2呀忧、如果使用gzip返回的數(shù)據(jù),則使用GzipSource
包裝便于解析溃睹。
總結(jié)
橋接攔截器的執(zhí)行邏輯主要就是以下幾點(diǎn)
對用戶構(gòu)建的Request
進(jìn)行添加或者刪除相關(guān)頭部信息而账,以轉(zhuǎn)化成能夠真正進(jìn)行網(wǎng)絡(luò)請求的Request
將符合網(wǎng)絡(luò)請求規(guī)范的Request交給下一個(gè)攔截器處理,并獲取Response
如果響應(yīng)體經(jīng)過了GZIP壓縮因篇,那就需要解壓泞辐,再構(gòu)建成用戶可用的Response
并返回
三、緩存攔截器
CacheInterceptor
竞滓,在發(fā)出請求前咐吼,判斷是否命中緩存。如果命中則可以不請求商佑,直接使用緩存的響應(yīng)锯茄。 (只會(huì)存在Get請求的緩存)
步驟為:
1、從緩存中獲得對應(yīng)請求的響應(yīng)緩存
2莉御、創(chuàng)建CacheStrategy
,創(chuàng)建時(shí)會(huì)判斷是否能夠使用緩存撇吞,在CacheStrategy
中存在兩個(gè)成員:networkRequest
與cacheResponse
俗冻。他們的組合如下:
networkRequest | cacheResponse | 說明 |
---|---|---|
Null | Not Null | 直接使用緩存 |
Not Null | Null | 向服務(wù)器發(fā)起請求 |
Null | Null | 直接gg,okhttp直接返回504 |
Not Null | Not Null | 發(fā)起請求牍颈,若得到響應(yīng)為304(無修改)迄薄,則更新緩存響應(yīng)并返回 |
3、交給下一個(gè)責(zé)任鏈繼續(xù)處理
4煮岁、后續(xù)工作讥蔽,返回304則用緩存的響應(yīng);否則使用網(wǎng)絡(luò)響應(yīng)并緩存本次響應(yīng)(只緩存Get請求的響應(yīng))
緩存攔截器的工作說起來比較簡單画机,但是具體的實(shí)現(xiàn)冶伞,需要處理的內(nèi)容很多。在緩存攔截器中判斷是否可以使用緩存步氏,或是請求服務(wù)器都是通過CacheStrategy
判斷响禽。
緩存策略
CacheStrategy
。首先需要認(rèn)識(shí)幾個(gè)請求頭與響應(yīng)頭
響應(yīng)頭 | 說明 | 例子 |
---|---|---|
Date | 消息發(fā)送的時(shí)間 | Date: Sat, 18 Nov 2028 06:17:41 GMT |
Expires | 資源過期的時(shí)間 | Expires: Sat, 18 Nov 2028 06:17:41 GMT |
Last-Modified | 資源最后修改時(shí)間 | Last-Modified: Fri, 22 Jul 2016 02:57:17 GMT |
ETag | 資源在服務(wù)器的唯一標(biāo)識(shí) | ETag: "16df0-5383097a03d40" |
Age | 服務(wù)器用緩存響應(yīng)請求荚醒,該緩存從產(chǎn)生到現(xiàn)在經(jīng)過多長時(shí)間(秒) | Age: 3825683 |
Cache-Control | - | - |
請求頭 | 說明 | 例子 |
---|---|---|
If-Modified-Since |
服務(wù)器沒有在指定的時(shí)間后修改請求對應(yīng)資源,返回304(無修改) | If-Modified-Since: Fri, 22 Jul 2016 02:57:17 GMT |
If-None-Match |
服務(wù)器將其與請求對應(yīng)資源的Etag 值進(jìn)行比較芋类,匹配返回304 |
If-None-Match: "16df0-5383097a03d40" |
Cache-Control |
- | - |
其中Cache-Control
可以在請求頭存在,也能在響應(yīng)頭存在界阁,對應(yīng)的value可以設(shè)置多種組合:
-
max-age=[秒]
:資源最大有效時(shí)間; -
public
:表明該資源可以被任何用戶緩存侯繁,比如客戶端,代理服務(wù)器等都可以緩存資源; -
private
:表明該資源只能被單個(gè)用戶緩存泡躯,默認(rèn)是private贮竟。 -
no-store
:資源不允許被緩存 -
no-cache
:(請求)不使用緩存 -
immutable
:(響應(yīng))資源不會(huì)改變 -
min-fresh=[秒]
:(請求)緩存最小新鮮度(用戶認(rèn)為這個(gè)緩存有效的時(shí)長) -
must-revalidate
:(響應(yīng))不允許使用過期緩存 -
max-stale=[秒]
:(請求)緩存過期后多久內(nèi)仍然有效
假設(shè)存在max-age=100,min-fresh=20较剃。這代表了用戶認(rèn)為這個(gè)緩存的響應(yīng)咕别,從服務(wù)器創(chuàng)建響應(yīng) 到 能夠緩存使用的時(shí)間為100-20=80s。但是如果max-stale=100重付。這代表了緩存有效時(shí)間80s過后顷级,仍然允許使用100s,可以看成緩存有效時(shí)長為180s确垫。
[圖片上傳失敗...(image-b71ccd-1572367003543)]
詳細(xì)流程
如果從緩存中獲得了本次請求URL對應(yīng)的Response
弓颈,首先會(huì)從響應(yīng)中獲得以上數(shù)據(jù)備用。
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
//對應(yīng)響應(yīng)的請求發(fā)出的本地時(shí)間 和 接收到響應(yīng)的本地時(shí)間
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
判斷緩存的命中會(huì)使用get()
方法
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
//todo 如果可以使用緩存删掀,那networkRequest必定為null翔冀;指定了只使用緩存但是networkRequest又不為null,沖突披泪。那就gg(攔截器返回504)
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
方法中調(diào)用getCandidate()
方法來完成真正的緩存判斷纤子。
1、緩存是否存在
整個(gè)方法中的第一個(gè)判斷是緩存是不是存在:
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
cacheResponse
是從緩存中找到的響應(yīng),如果為null控硼,那就表示沒有找到對應(yīng)的緩存泽论,創(chuàng)建的CacheStrategy
實(shí)例對象只存在networkRequest
,這代表了需要發(fā)起網(wǎng)絡(luò)請求卡乾。
2翼悴、https請求的緩存
繼續(xù)往下走意味著cacheResponse
必定存在,但是它不一定能用幔妨。后續(xù)進(jìn)行有效性的一系列判斷
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
如果本次請求是HTTPS鹦赎,但是緩存中沒有對應(yīng)的握手信息,那么緩存無效误堡。
3古话、響應(yīng)碼以及響應(yīng)頭
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
整個(gè)邏輯都在isCacheable
中,他的內(nèi)容是:
public static boolean isCacheable(Response response, Request request) {
// Always go to network for uncacheable response codes (RFC 7231 section 6.1),
// This implementation doesn't support caching partial content.
switch (response.code()) {
case HTTP_OK:
case HTTP_NOT_AUTHORITATIVE:
case HTTP_NO_CONTENT:
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_NOT_FOUND:
case HTTP_BAD_METHOD:
case HTTP_GONE:
case HTTP_REQ_TOO_LONG:
case HTTP_NOT_IMPLEMENTED:
case StatusLine.HTTP_PERM_REDIRECT:
// These codes can be cached unless headers forbid it.
break;
case HTTP_MOVED_TEMP:
case StatusLine.HTTP_TEMP_REDIRECT:
// These codes can only be cached with the right response headers.
// http://tools.ietf.org/html/rfc7234#section-3
// s-maxage is not checked because OkHttp is a private cache that should ignore
// s-maxage.
if (response.header("Expires") != null
|| response.cacheControl().maxAgeSeconds() != -1
|| response.cacheControl().isPublic()
|| response.cacheControl().isPrivate()) {
break;
}
// Fall-through.
default:
// All other codes cannot be cached.
return false;
}
// A 'no-store' directive on request or response prevents the response from being cached.
return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}
緩存響應(yīng)中的響應(yīng)碼為 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308 的情況下,只判斷服務(wù)器是不是給了 Cache-Control: no-store
(資源不能被緩存)锁施,所以如果服務(wù)器給到了這個(gè)響應(yīng)頭陪踩,那就和前面兩個(gè)判定一致(緩存不可用)。否則繼續(xù)進(jìn)一步判斷緩存是否可用
而如果響應(yīng)碼是302/307(重定向)悉抵,則需要進(jìn)一步判斷是不是存在一些允許緩存的響應(yīng)頭膊毁。根據(jù)注解中的給到的文檔http://tools.ietf.org/html/rfc7234#section-3中的描述,如果存在Expires
或者Cache-Control
的值為:
-
max-age=[秒]
:資源最大有效時(shí)間; -
public
:表明該資源可以被任何用戶緩存基跑,比如客戶端,代理服務(wù)器等都可以緩存資源; -
private
:表明該資源只能被單個(gè)用戶緩存描焰,默認(rèn)是private媳否。
同時(shí)不存在Cache-Control: no-store
,那就可以繼續(xù)進(jìn)一步判斷緩存是否可用荆秦。
所以綜合來看判定優(yōu)先級如下:
1篱竭、響應(yīng)碼不為 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308,302步绸,307 緩存不可用;
2掺逼、當(dāng)響應(yīng)碼為302或者307時(shí),未包含某些響應(yīng)頭瓤介,則緩存不可用;
3吕喘、當(dāng)存在Cache-Control: no-store
響應(yīng)頭則緩存不可用。
如果響應(yīng)緩存可用刑桑,進(jìn)一步再判斷緩存有效性
4氯质、用戶的請求配置
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
private static boolean hasConditions(Request request) {
return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
}
走到這一步,OkHttp需要先對用戶本次發(fā)起的Request
進(jìn)行判定祠斧,如果用戶指定了Cache-Control: no-cache
(不使用緩存)的請求頭或者請求頭包含 If-Modified-Since
或If-None-Match
(請求驗(yàn)證)闻察,那么就不允許使用緩存。
請求頭 | 說明 |
---|---|
Cache-Control: no-cache |
忽略緩存 |
If-Modified-Since: 時(shí)間 |
值一般為Data 或lastModified ,服務(wù)器沒有在指定的時(shí)間后修改請求對應(yīng)資源,返回304(無修改) |
If-None-Match:標(biāo)記 |
值一般為Etag ,將其與請求對應(yīng)資源的Etag 值進(jìn)行比較辕漂;如果匹配呢灶,返回304 |
這意味著如果用戶請求頭中包含了這些內(nèi)容,那就必須向服務(wù)器發(fā)起請求钉嘹。但是需要注意的是鸯乃,OkHttp并不會(huì)緩存304的響應(yīng),如果是此種情況隧期,即用戶主動(dòng)要求與服務(wù)器發(fā)起請求飒责,服務(wù)器返回的304(無響應(yīng)體),則直接把304的響應(yīng)返回給用戶:“既然你主動(dòng)要求仆潮,我就只告知你本次請求結(jié)果”宏蛉。而如果不包含這些請求頭,那繼續(xù)判定緩存有效性性置。
5拾并、資源是否不變
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
如果緩存的響應(yīng)中包含Cache-Control: immutable
,這意味著對應(yīng)請求的響應(yīng)內(nèi)容將一直不會(huì)改變鹏浅。此時(shí)就可以直接使用緩存嗅义。否則繼續(xù)判斷緩存是否可用
6、響應(yīng)的緩存有效期
這一步為進(jìn)一步根據(jù)緩存響應(yīng)中的一些信息判定緩存是否處于有效期內(nèi)隐砸。如果滿足:
緩存存活時(shí)間 < 緩存新鮮度 - 緩存最小新鮮度 + 過期后繼續(xù)使用時(shí)長
代表可以使用緩存之碗。其中新鮮度可以理解為有效時(shí)間,而這里的 "緩存新鮮度-緩存最小新鮮度" 就代表了緩存真正有效的時(shí)間季希。
// 6.1褪那、獲得緩存的響應(yīng)從創(chuàng)建到現(xiàn)在的時(shí)間
long ageMillis = cacheResponseAge();
//todo
// 6.2、獲取這個(gè)響應(yīng)有效緩存的時(shí)長
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//todo 如果請求中指定了 max-age 表示指定了能拿的緩存有效時(shí)長式塌,就需要綜合響應(yīng)有效緩存時(shí)長與請求能拿緩存的時(shí)長博敬,獲得最小的能夠使用響應(yīng)緩存的時(shí)長
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
// 6.3 請求包含 Cache-Control:min-fresh=[秒] 能夠使用還未過指定時(shí)間的緩存 (請求認(rèn)為的緩存有效時(shí)間)
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
// 6.4
// 6.4.1、Cache-Control:must-revalidate 可緩存但必須再向源服務(wù)器進(jìn)行確認(rèn)
// 6.4.2峰尝、Cache-Control:max-stale=[秒] 緩存過期后還能使用指定的時(shí)長 如果未指定多少秒偏窝,則表示無論過期多長時(shí)間都可以;如果指定了武学,則只要是指定時(shí)間內(nèi)就能使用緩存
// 前者會(huì)忽略后者祭往,所以判斷了不必須向服務(wù)器確認(rèn)主守,再獲得請求頭中的max-stale
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
// 6.5 不需要與服務(wù)器驗(yàn)證有效性 && 響應(yīng)存在的時(shí)間+請求認(rèn)為的緩存有效時(shí)間 小于 緩存有效時(shí)長+過期后還可以使用的時(shí)間
// 允許使用緩存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
//todo 如果已過期蛤奢,但未超過 過期后繼續(xù)使用時(shí)長肖抱,那還可以繼續(xù)使用叛拷,只用添加相應(yīng)的頭部字段
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
//todo 如果緩存已超過一天并且響應(yīng)中沒有設(shè)置過期時(shí)間也需要添加警告
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
6.1旱函、緩存到現(xiàn)在存活的時(shí)間:ageMillis
首先cacheResponseAge()
方法獲得了響應(yīng)大概存在了多久:
long ageMillis = 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;
}
1批钠、apparentReceivedAge
代表了客戶端收到響應(yīng)到服務(wù)器發(fā)出響應(yīng)的一個(gè)時(shí)間差
seredData
是從緩存中獲得的Data
響應(yīng)頭對應(yīng)的時(shí)間(服務(wù)器發(fā)出本響應(yīng)的時(shí)間)前计;
receivedResponseMillis
為本次響應(yīng)對應(yīng)的客戶端發(fā)出請求的時(shí)間
2潜秋、receivedAge
是代表了客戶端的緩存,在收到時(shí)就已經(jīng)存在多久了
ageSeconds
是從緩存中獲得的Age
響應(yīng)頭對應(yīng)的秒數(shù) (本地緩存的響應(yīng)是由服務(wù)器的緩存返回疾捍,這個(gè)緩存在服務(wù)器存在的時(shí)間)
ageSeconds
與上一步計(jì)算結(jié)果apparentReceivedAge
的最大值為收到響應(yīng)時(shí)奈辰,這個(gè)響應(yīng)數(shù)據(jù)已經(jīng)存在多久。假設(shè)我們發(fā)出請求時(shí)乱豆,服務(wù)器存在一個(gè)緩存奖恰,其中
Data: 0點(diǎn)
。
此時(shí)宛裕,客戶端在1小時(shí)候發(fā)起請求瑟啃,此時(shí)由服務(wù)器在緩存中插入Age: 1小時(shí)
并返回給客戶端,此時(shí)客戶端計(jì)算的receivedAge
就是1小時(shí)揩尸,這就代表了客戶端的緩存在收到時(shí)就已經(jīng)存在多久了蛹屿。(不代表到本次請求時(shí)存在多久了)
3、responseDuration
是緩存對應(yīng)的請求岩榆,在發(fā)送請求與接收請求之間的時(shí)間差
4错负、residentDuration
是這個(gè)緩存接收到的時(shí)間到現(xiàn)在的一個(gè)時(shí)間差
receivedAge + responseDuration + residentDuration
所代表的意義就是:
緩存在客戶端收到時(shí)就已經(jīng)存在的時(shí)間 + 請求過程中花費(fèi)的時(shí)間 + 本次請求距離緩存獲得的時(shí)間,就是緩存真正存在了多久勇边。
6.2犹撒、緩存新鮮度(有效時(shí)間):freshMillis
long freshMillis = computeFreshnessLifetime();
private long computeFreshnessLifetime() {
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.maxAgeSeconds() != -1) {
return SECONDS.toMillis(responseCaching.maxAgeSeconds());
} else if (expires != null) {
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;
}
緩存新鮮度(有效時(shí)長)的判定會(huì)有幾種情況,按優(yōu)先級排列如下:
1粒褒、緩存響應(yīng)包含Cache-Control: max-age=[秒]
資源最大有效時(shí)間
2识颊、緩存響應(yīng)包含Expires: 時(shí)間
,則通過Data
或接收該響應(yīng)時(shí)間計(jì)算資源有效時(shí)間
3奕坟、緩存響應(yīng)包含Last-Modified: 時(shí)間
谊囚,則通過Data
或發(fā)送該響應(yīng)對應(yīng)請求的時(shí)間計(jì)算資源有效時(shí)間;并且根據(jù)建議以及在Firefox瀏覽器的實(shí)現(xiàn)执赡,使用得到結(jié)果的10%來作為資源的有效時(shí)間。
6.3函筋、緩存最小新鮮度:minFreshMillis
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
如果用戶的請求頭中包含Cache-Control: min-fresh=[秒]
沙合,代表用戶認(rèn)為這個(gè)緩存有效的時(shí)長。假設(shè)本身緩存新鮮度為: 100毫秒跌帐,而緩存最小新鮮度為:10毫秒首懈,那么緩存真正有效時(shí)間為90毫秒。
6.4谨敛、緩存過期后仍有效時(shí)長:maxStaleMillis
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
這個(gè)判斷中第一個(gè)條件為緩存的響應(yīng)中沒有包含Cache-Control: must-revalidate
(不可用過期資源)究履,獲得用戶請求頭中包含Cache-Control: max-stale=[秒]
緩存過期后仍有效的時(shí)長。
6.5脸狸、判定緩存是否有效
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
//todo 如果已過期最仑,但未超過 過期后繼續(xù)使用時(shí)長藐俺,那還可以繼續(xù)使用,只用添加相應(yīng)的頭部字段
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
//todo 如果緩存已超過一天并且響應(yīng)中沒有設(shè)置過期時(shí)間也需要添加警告
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
最后利用上4步產(chǎn)生的值泥彤,只要緩存的響應(yīng)未指定no-cache
忽略緩存欲芹,如果:
緩存存活時(shí)間+緩存最小新鮮度 < 緩存新鮮度+過期后繼續(xù)使用時(shí)長,代表可以使用緩存吟吝。
假設(shè) 緩存到現(xiàn)在存活了:100 毫秒;
用戶認(rèn)為緩存有效時(shí)間(緩存最小新鮮度)為:10 毫秒;
緩存新鮮度為: 100 毫秒;
緩存過期后仍能使用: 0 毫秒;
這些條件下菱父,首先緩存的真實(shí)有效時(shí)間為: 90毫秒,而緩存已經(jīng)過了這個(gè)時(shí)間剑逃,所以無法使用緩存浙宜。不等式可以轉(zhuǎn)換為: 緩存存活時(shí)間 < 緩存新鮮度 - 緩存最小新鮮度 + 過期后繼續(xù)使用時(shí)長,即
存活時(shí)間 < 緩存有效時(shí)間 + 過期后繼續(xù)使用時(shí)間
總體來說蛹磺,只要不忽略緩存并且緩存未過期粟瞬,則使用緩存。
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 {
//意味著無法與服務(wù)器發(fā)起比較亩钟,只能重新請求
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
//添加請求頭
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);
如果繼續(xù)執(zhí)行,表示緩存已經(jīng)過期無法使用鳖轰。此時(shí)我們判定緩存的響應(yīng)中如果存在Etag
清酥,則使用If-None-Match
交給服務(wù)器進(jìn)行驗(yàn)證;如果存在Last-Modified
或者Data
蕴侣,則使用If-Modified-Since
交給服務(wù)器驗(yàn)證焰轻。服務(wù)器如果無修改則會(huì)返回304,這時(shí)候注意:
由于是緩存過期而發(fā)起的請求(與第4個(gè)判斷用戶的主動(dòng)設(shè)置不同)昆雀,如果服務(wù)器返回304辱志,那框架會(huì)自動(dòng)更新緩存,所以此時(shí)CacheStrategy
既包含networkRequest
也包含cacheResponse
8狞膘、收尾
至此揩懒,緩存的判定結(jié)束,攔截器中只需要判斷CacheStrategy
中networkRequest
與cacheResponse
的不同組合就能夠判斷是否允許使用緩存挽封。
但是需要注意的是已球,如果用戶在創(chuàng)建請求時(shí),配置了onlyIfCached
這意味著用戶這次希望這個(gè)請求只從緩存獲得辅愿,不需要發(fā)起請求智亮。那如果生成的CacheStrategy
存在networkRequest
這意味著肯定會(huì)發(fā)起請求,此時(shí)出現(xiàn)沖突点待!那會(huì)直接給到攔截器一個(gè)既沒有networkRequest
又沒有cacheResponse
的對象阔蛉。攔截器直接返回用戶504
!
//緩存策略 get 方法
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
//緩存攔截器
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();
}
9癞埠、總結(jié)
1状原、如果從緩存獲取的Response
是null聋呢,那就需要使用網(wǎng)絡(luò)請求獲取響應(yīng);
2遭笋、如果是Https請求坝冕,但是又丟失了握手信息,那也不能使用緩存瓦呼,需要進(jìn)行網(wǎng)絡(luò)請求喂窟;
3、如果判斷響應(yīng)碼不能緩存且響應(yīng)頭有no-store
標(biāo)識(shí)央串,那就需要進(jìn)行網(wǎng)絡(luò)請求磨澡;
4、如果請求頭有no-cache
標(biāo)識(shí)或者有If-Modified-Since/If-None-Match
质和,那么需要進(jìn)行網(wǎng)絡(luò)請求稳摄;
5、如果響應(yīng)頭沒有no-cache
標(biāo)識(shí)饲宿,且緩存時(shí)間沒有超過極限時(shí)間厦酬,那么可以使用緩存,不需要進(jìn)行網(wǎng)絡(luò)請求瘫想;
6仗阅、如果緩存過期了,判斷響應(yīng)頭是否設(shè)置Etag/Last-Modified/Date
国夜,沒有那就直接使用網(wǎng)絡(luò)請求否則需要考慮服務(wù)器返回304减噪;
并且,只要需要進(jìn)行網(wǎng)絡(luò)請求车吹,請求頭中就不能包含only-if-cached
筹裕,否則框架直接返回504!
緩存攔截器本身主要邏輯其實(shí)都在緩存策略中窄驹,攔截器本身邏輯非常簡單朝卒,如果確定需要發(fā)起網(wǎng)絡(luò)請求,則下一個(gè)攔截器為
ConnectInterceptor
四乐埠、連接攔截器
ConnectInterceptor
扎运,打開與目標(biāo)服務(wù)器的連接,并執(zhí)行下一個(gè)攔截器饮戳。它簡短的可以直接完整貼在這里:
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 = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
雖然代碼量很少,實(shí)際上大部分功能都封裝到其它類去了洞拨,這里只是調(diào)用而已扯罐。
首先我們看到的StreamAllocation
這個(gè)對象是在第一個(gè)攔截器:重定向攔截器創(chuàng)建的,但是真正使用的地方卻在這里烦衣。
"當(dāng)一個(gè)請求發(fā)出歹河,需要建立連接掩浙,連接建立后需要使用流用來讀寫數(shù)據(jù)";而這個(gè)StreamAllocation就是協(xié)調(diào)請求秸歧、連接與數(shù)據(jù)流三者之間的關(guān)系厨姚,它負(fù)責(zé)為一次請求尋找連接,然后獲得流來實(shí)現(xiàn)網(wǎng)絡(luò)通信键菱。
這里使用的newStream
方法實(shí)際上就是去查找或者建立一個(gè)與請求主機(jī)有效的連接谬墙,返回的HttpCodec
中包含了輸入輸出流,并且封裝了對HTTP請求報(bào)文的編碼與解碼经备,直接使用它就能夠與請求主機(jī)完成HTTP通信拭抬。
StreamAllocation
中簡單來說就是維護(hù)連接:RealConnection
——封裝了Socket與一個(gè)Socket連接池∏置桑可復(fù)用的RealConnection
需要:
public boolean isEligible(Address address, @Nullable Route route) {
// If this connection is not accepting new streams, we're done.
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// If the non-host fields of the address don't overlap, we're done.
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// If the host exactly matches, we're done: this connection can carry the address.
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
// At this point we don't have a hostname match. But we still be able to carry the request if
// our connection coalescing requirements are met. See also:
// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
// https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
// 1. This connection must be HTTP/2.
if (http2Connection == null) return false;
// 2. The routes must share an IP address. This requires us to have a DNS address for both
// hosts, which only happens after route planning. We can't coalesce connections that use a
// proxy, since proxies don't tell us the origin server's IP address.
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;
// 3. This connection's server certificate's must cover the new host.
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 4. Certificate pinning must match the host.
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.
}
1造虎、if (allocations.size() >= allocationLimit || noNewStreams) return false;
? 連接到達(dá)最大并發(fā)流或者連接不允許建立新的流;如http1.x正在使用的連接不能給其他人用(最大并發(fā)流為:1)或者連接被關(guān)閉纷闺;那就不允許復(fù)用算凿;
2、
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.
}
DNS犁功、代理氓轰、SSL證書、服務(wù)器域名波桩、端口完全相同則可復(fù)用戒努;
如果上述條件都不滿足,在HTTP/2的某些場景下可能仍可以復(fù)用(http2先不管)镐躲。
所以綜上储玫,如果在連接池中找到個(gè)連接參數(shù)一致并且未被關(guān)閉沒被占用的連接,則可以復(fù)用萤皂。
總結(jié)
這個(gè)攔截器中的所有實(shí)現(xiàn)都是為了獲得一份與目標(biāo)服務(wù)器的連接撒穷,在這個(gè)連接上進(jìn)行HTTP數(shù)據(jù)的收發(fā)。
五裆熙、請求服務(wù)器攔截器
CallServerInterceptor
端礼,利用HttpCodec
發(fā)出請求到服務(wù)器并且解析生成Response
。
首先調(diào)用httpCodec.writeRequestHeaders(request);
將請求頭寫入到緩存中(直到調(diào)用flushRequest()
才真正發(fā)送給服務(wù)器)入录。然后馬上進(jìn)行第一個(gè)邏輯判斷
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return
// what we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
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()) {
//HTTP2多路復(fù)用蛤奥,不需要關(guān)閉socket,不管僚稿!
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1
// connection
// from being reused. Otherwise we're still obligated to transmit the request
// body to
// leave the connection in a consistent state.
streamAllocation.noNewStreams();
}
}
httpCodec.finishRequest();
整個(gè)if都和一個(gè)請求頭有關(guān): Expect: 100-continue
凡桥。這個(gè)請求頭代表了在發(fā)送請求體之前需要和服務(wù)器確定是否愿意接受客戶端發(fā)送的請求體。所以permitsRequestBody
判斷為是否會(huì)攜帶請求體的方式(POST)蚀同,如果命中if缅刽,則會(huì)先給服務(wù)器發(fā)起一次查詢是否愿意接收請求體啊掏,這時(shí)候如果服務(wù)器愿意會(huì)響應(yīng)100(沒有響應(yīng)體,responseBuilder 即為nul)衰猛。這時(shí)候才能夠繼續(xù)發(fā)送剩余請求數(shù)據(jù)迟蜜。
但是如果服務(wù)器不同意接受請求體,那么我們就需要標(biāo)記該連接不能再被復(fù)用啡省,調(diào)用noNewStreams()
關(guān)閉相關(guān)的Socket娜睛。
后續(xù)代碼為:
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();
這時(shí)responseBuilder
的情況即為:
1、POST方式請求冕杠,請求頭中包含Expect
微姊,服務(wù)器允許接受請求體,并且已經(jīng)發(fā)出了請求體分预,responseBuilder
為null;
2兢交、POST方式請求,請求頭中包含Expect
笼痹,服務(wù)器不允許接受請求體配喳,responseBuilder
不為null
3、POST方式請求凳干,未包含Expect
晴裹,直接發(fā)出請求體,responseBuilder
為null;
4救赐、POST方式請求涧团,沒有請求體,responseBuilder
為null;
5经磅、GET方式請求泌绣,responseBuilder
為null;
對應(yīng)上面的5種情況,讀取響應(yīng)頭并且組成響應(yīng)Response
预厌,注意:此Response
沒有響應(yīng)體阿迈。同時(shí)需要注意的是,如果服務(wù)器接受 Expect: 100-continue
這是不是意味著我們發(fā)起了兩次Request
轧叽?那此時(shí)的響應(yīng)頭是第一次查詢服務(wù)器是否支持接受請求體的苗沧,而不是真正的請求對應(yīng)的結(jié)果響應(yīng)。所以緊接著:
int code = response.code();
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();
}
如果響應(yīng)是100炭晒,這代表了是請求Expect: 100-continue
成功的響應(yīng)待逞,需要馬上再次讀取一份響應(yīng)頭,這才是真正的請求對應(yīng)結(jié)果響應(yīng)頭网严。
然后收尾
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null
// response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response识樱;
forWebSocket
代表websocket的請求,我們直接進(jìn)入else,這里就是讀取響應(yīng)體數(shù)據(jù)牺荠。然后判斷請求和服務(wù)器是不是都希望長連接,一旦有一方指明close
驴一,那么就需要關(guān)閉socket
休雌。而如果服務(wù)器返回204/205,一般情況而言不會(huì)存在這些返回碼肝断,但是一旦出現(xiàn)這意味著沒有響應(yīng)體杈曲,但是解析到的響應(yīng)頭中包含Content-Lenght
且不為0,這表響應(yīng)體的數(shù)據(jù)字節(jié)長度胸懈。此時(shí)出現(xiàn)了沖突担扑,直接拋出協(xié)議異常!
總結(jié)
在這個(gè)攔截器中就是完成HTTP協(xié)議報(bào)文的封裝與解析趣钱。
OkHttp總結(jié)
整個(gè)OkHttp功能的實(shí)現(xiàn)就在這五個(gè)默認(rèn)的攔截器中涌献,所以先理解攔截器模式的工作機(jī)制是先決條件。這五個(gè)攔截器分別為: 重試攔截器首有、橋接攔截器燕垃、緩存攔截器、連接攔截器井联、請求服務(wù)攔截器卜壕。每一個(gè)攔截器負(fù)責(zé)的工作不一樣,就好像工廠流水線烙常,最終經(jīng)過這五道工序轴捎,就完成了最終的產(chǎn)品。
但是與流水線不同的是蚕脏,OkHttp中的攔截器每次發(fā)起請求都會(huì)在交給下一個(gè)攔截器之前干一些事情侦副,在獲得了結(jié)果之后又干一些事情。整個(gè)過程在請求向是順序的蝗锥,而響應(yīng)向則是逆序跃洛。
當(dāng)用戶發(fā)起一個(gè)請求后,會(huì)由任務(wù)分發(fā)起Dispatcher
將請求包裝并交給重試攔截器處理终议。
1汇竭、重試攔截器在交出(交給下一個(gè)攔截器)之前,負(fù)責(zé)判斷用戶是否取消了請求穴张;在獲得了結(jié)果之后细燎,會(huì)根據(jù)響應(yīng)碼判斷是否需要重定向,如果滿足條件那么就會(huì)重啟執(zhí)行所有攔截器皂甘。
2玻驻、橋接攔截器在交出之前,負(fù)責(zé)將HTTP協(xié)議必備的請求頭加入其中(如:Host)并添加一些默認(rèn)的行為(如:GZIP壓縮);在獲得了結(jié)果后璧瞬,調(diào)用保存cookie接口并解析GZIP數(shù)據(jù)户辫。
3、緩存攔截器顧名思義嗤锉,交出之前讀取并判斷是否使用緩存渔欢;獲得結(jié)果后判斷是否緩存。
4瘟忱、連接攔截器在交出之前奥额,負(fù)責(zé)找到或者新建一個(gè)連接,并獲得對應(yīng)的socket流访诱;在獲得結(jié)果后不進(jìn)行額外的處理垫挨。
5、請求服務(wù)器攔截器進(jìn)行真正的與服務(wù)器的通信触菜,向服務(wù)器發(fā)送數(shù)據(jù)艺栈,解析讀取的響應(yīng)數(shù)據(jù)蒜魄。
在經(jīng)過了這一系列的流程后衣式,就完成了一次HTTP請求讲仰!
補(bǔ)充: 代理
在使用OkHttp時(shí),如果用戶在創(chuàng)建OkHttpClient
時(shí)漾峡,配置了proxy
或者proxySelector
攻旦,則會(huì)使用配置的代理,并且proxy
優(yōu)先級高于proxySelector
生逸。而如果未配置牢屋,則會(huì)獲取機(jī)器配置的代理并使用。
//JDK : ProxySelector
try {
URI uri = new URI("http://restapi.amap.com");
List<Proxy> proxyList = ProxySelector.getDefault().select(uri);
System.out.println(proxyList.get(0).address());
System.out.println(proxyList.get(0).type());
} catch (URISyntaxException e) {
e.printStackTrace();
}
因此槽袄,如果我們不需要自己的App中的請求走代理烙无,則可以配置一個(gè)proxy(Proxy.NO_PROXY)
,這樣也可以避免被抓包遍尺。NO_PROXY
的定義如下:
public static final Proxy NO_PROXY = new Proxy();
private Proxy() {
this.type = Proxy.Type.DIRECT;
this.sa = null;
}
代理在Java中對應(yīng)的抽象類有三種類型:
public static enum Type {
DIRECT,
HTTP,
SOCKS;
private Type() {
}
}
DIRECT
:無代理截酷,HTTP
:http代理,SOCKS
:socks代理乾戏。第一種自然不用多說迂苛,而Http代理與Socks代理有什么區(qū)別?
對于Socks代理鼓择,在HTTP的場景下三幻,代理服務(wù)器完成TCP數(shù)據(jù)包的轉(zhuǎn)發(fā)工作;
而Http代理服務(wù)器,在轉(zhuǎn)發(fā)數(shù)據(jù)之外呐能,還會(huì)解析HTTP的請求及響應(yīng)念搬,并根據(jù)請求及響應(yīng)的內(nèi)容做一些處理。
RealConnection
的connectSocket
方法:
//如果是Socks代理則 new Socket(proxy); 否則相當(dāng)于直接:new Socket()
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
//connect方法
socket.connect(address);
設(shè)置了SOCKS代理的情況下,創(chuàng)建Socket時(shí)朗徊,為其傳入proxy首妖,連接時(shí)還是以HTTP服務(wù)器為目標(biāo)地址;但是如果設(shè)置的是Http代理爷恳,創(chuàng)建Socket是與Http代理服務(wù)器建立連接悯搔。
在
connect
方法時(shí)傳遞的address
來自于下面的集合inetSocketAddresses
RouteSelector
的resetNextInetSocketAddress
方法:
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
// ......
if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
//無代理和socks代理,使用http服務(wù)器域名與端口
socketHost = address.url().host();
socketPort = address.url().port();
} else {
SocketAddress proxyAddress = proxy.address();
if (!(proxyAddress instanceof InetSocketAddress)) {
throw new IllegalArgumentException(
"Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
}
InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
socketHost = getHostString(proxySocketAddress);
socketPort = proxySocketAddress.getPort();
}
// ......
if (proxy.type() == Proxy.Type.SOCKS) {
//socks代理 connect http服務(wù)器 (DNS沒用舌仍,由代理服務(wù)器解析域名)
inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
} else {
//無代理,dns解析http服務(wù)器
//http代理,dns解析http代理服務(wù)器
List<InetAddress> addresses = address.dns().lookup(socketHost);
//......
for (int i = 0, size = addresses.size(); i < size; i++) {
InetAddress inetAddress = addresses.get(i);
inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
}
}
}
設(shè)置代理時(shí)通危,Http服務(wù)器的域名解析會(huì)被交給代理服務(wù)器執(zhí)行铸豁。但是如果是設(shè)置了Http代理,會(huì)對Http代理服務(wù)器的域名使用OkhttpClient
配置的dns解析代理服務(wù)器菊碟,Http服務(wù)器的域名解析被交給代理服務(wù)器解析节芥。
上述代碼就是代理與DNS在OkHttp中的使用,但是還有一點(diǎn)需要注意逆害,Http代理也分成兩種類型:普通代理與隧道代理头镊。
其中普通代理不需要額外的操作,扮演「中間人」的角色魄幕,在兩端之間來回傳遞報(bào)文相艇。這個(gè)“中間人”在收到客戶端發(fā)送的請求報(bào)文時(shí),需要正確的處理請求和連接狀態(tài)纯陨,同時(shí)向服務(wù)器發(fā)送新的請求坛芽,在收到響應(yīng)后,將響應(yīng)結(jié)果包裝成一個(gè)響應(yīng)體返回給客戶端翼抠。在普通代理的流程中咙轩,代理兩端都是有可能察覺不到"中間人“的存在。
但是隧道代理不再作為中間人阴颖,無法改寫客戶端的請求活喊,而僅僅是在建立連接后,將客戶端的請求量愧,通過建立好的隧道钾菊,無腦的轉(zhuǎn)發(fā)給終端服務(wù)器。隧道代理需要發(fā)起Http CONNECT請求侠畔,這種請求方式?jīng)]有請求體结缚,僅供代理服務(wù)器使用,并不會(huì)傳遞給終端服務(wù)器软棺。請求頭 部分一旦結(jié)束红竭,后面的所有數(shù)據(jù),都被視為應(yīng)該轉(zhuǎn)發(fā)給終端服務(wù)器的數(shù)據(jù),代理需要把他們無腦的直接轉(zhuǎn)發(fā)茵宪,直到從客戶端的 TCP 讀通道關(guān)閉最冰。CONNECT 的響應(yīng)報(bào)文,在代理服務(wù)器和終端服務(wù)器建立連接后稀火,可以向客戶端返回一個(gè) 200 Connect established
的狀態(tài)碼暖哨,以此表示和終端服務(wù)器的連接,建立成功凰狞。
RealConnection的connect方法
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our
// resources.
break;
}
} else {
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
requiresTunnel
方法的判定為:當(dāng)前請求為https并且存在http代理篇裁,這時(shí)候connectTunnel
中會(huì)發(fā)起:
CONNECT xxxx HTTP/1.1
Host: xxxx
Proxy-Connection: Keep-Alive
User-Agent: okhttp/${version}
的請求,連接成功代理服務(wù)器會(huì)返回200赡若;如果返回407表示代理服務(wù)器需要鑒權(quán)(如:付費(fèi)代理)达布,這時(shí)需要在請求頭中加入Proxy-Authorization
:
Authenticator authenticator = new Authenticator() {
@Nullable
@Override
public Request authenticate(Route route, Response response) throws IOException {
if(response.code == 407){
//代理鑒權(quán)
String credential = Credentials.basic("代理服務(wù)用戶名", "代理服務(wù)密碼");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
}
return null;
}
};
new OkHttpClient.Builder().proxyAuthenticator(authenticator);