面試官:為什么用Okhttp逗余,而不選擇其它網(wǎng)絡(luò)框架饼丘?
支持HTTP2/SPDY,允許所有同一個主機(jī)地址的請求共享同一個Socket連接(SPDY是Google開發(fā)的基于TCP的傳輸層協(xié)議锯玛,用以最小化網(wǎng)絡(luò)延遲慢味,提升網(wǎng)絡(luò)速度阱缓,優(yōu)化用戶的網(wǎng)絡(luò)使用體驗欲诺。)
? ?關(guān)于HTTP2的優(yōu)點嗦锐,主要有:
多路復(fù)用:就是針對同個域名的請求稿蹲,都可以在同一條連接中并行進(jìn)行屿讽,而且頭部和數(shù)據(jù)都進(jìn)行了二進(jìn)制封裝屿储。
二進(jìn)制分幀:傳輸都是基于字節(jié)流進(jìn)行的贿讹,而不是文本,二進(jìn)制分幀層處于應(yīng)用層和傳輸層之間够掠。
頭部壓縮:HTTP1.x每次請求都會攜帶完整的頭部字段民褂,所以可能會出現(xiàn)重復(fù)傳輸,因此HTTP2采用HPACK對其進(jìn)行壓縮優(yōu)化祖屏,可以節(jié)省不少的傳輸流量助赞。
?OkHttp由于基于Http協(xié)議,所以http協(xié)議都支持
?在早期的版本中袁勺,OkHttp支持Http1.0,1.1,SPDY協(xié)議雹食,但是Http2協(xié)議的問世,導(dǎo)致OkHttp也做出了改變期丰,OkHttp鼓勵開發(fā)者使用HTTP2群叶,不再對SPDY協(xié)議給予支持吃挑。另外,新版本的OkHttp還有一個新的亮點就是支持WebScoket街立,這樣我? ?們就可以非常方便的建立長連接了
?2.連接池減少請求延時(socket自動選擇最好路線舶衬,并支持自動重連,擁有自動維護(hù)的socket連接池赎离,減少握手次數(shù)逛犹,減少了請求延遲,共享Socket,減少對服務(wù)器的請求次數(shù)梁剔。)
透明的GZIP壓縮減少響應(yīng)數(shù)據(jù)的大小(基于Headers的緩存策略減少重復(fù)的網(wǎng)絡(luò)請求)
透明的GZIP壓縮減少響應(yīng)數(shù)據(jù)的大小{{{擁有Interceptors輕松處理請求與響應(yīng)(自動處理GZip壓縮)}}}虽画,使用Gzip來壓縮request和response, 減少傳輸數(shù)據(jù)量, 從而減少流量消耗.重試及重定向
面試官:簡述一下OkHttp
OkHttp是一個非常優(yōu)秀的網(wǎng)絡(luò)請求框架,已被谷歌加入到Android的源碼中荣病。
支持http2码撰,對一臺機(jī)器的所有請求共享同一個socket? ?。1.2是否支持呢个盆?脖岛??颊亮?
內(nèi)置連接池柴梆,支持連接復(fù)用,減少延遲
支持透明的gzip壓縮響應(yīng)體
通過緩存避免重復(fù)的請求
請求失敗時自動重試主機(jī)的其他ip编兄,自動重定向
很多設(shè)計模式轩性,好用的API。鏈?zhǔn)秸{(diào)用
面試官:okhttp支持的協(xié)議是什么狠鸳?
android支持http2.0,Okhttp如何開啟的Http2.0
https://blog.csdn.net/weixin_42522669/article/details/117576189
有非常好的握手過程
自動的一個過程:ALPN協(xié)議
當(dāng)后端支持的協(xié)議內(nèi)包含Http2.0時,則就會把請求升級到Http2.0階段悯嗓。
面試官:看過OkHttp的源碼嗎件舵,簡單說一下
三大點:
第一。分發(fā)器(處理高并發(fā)請求)
1.用法解析 同步和異步 怎么實現(xiàn)的
2.okhttp線程池工作原理
3.同步隊列里面脯厨,運(yùn)行隊列和準(zhǔn)備怎么工作的
4.如何實現(xiàn)并發(fā)的铅祸?并發(fā)控制
第二。攔截器(每個攔截器的作用)
1.責(zé)任鏈模式
2.攔截器?緩存機(jī)制 ?緩存基于DiskLruCache
3.自定義攔截器
4.多域名如何封裝合武?測試和正式如何封裝
第三临梗。網(wǎng)絡(luò)攔截器Connection原理
1.攔截器?socker連接池復(fù)用機(jī)制詳解
緩存機(jī)制(攔截器)
2.在無網(wǎng)的時候直接使用緩存,這該怎么做呢稼跳?很簡單盟庞,我們只要自定義一個攔截器,在我們的請求頭中判斷沒有網(wǎng)絡(luò)可用時汤善,緩存策略為強(qiáng)制使用緩存什猖。
3.緩存機(jī)制是怎么樣的票彪?網(wǎng)絡(luò)請求緩存處理,okhttp如何處理網(wǎng)絡(luò)緩存的不狮?
重試機(jī)制(攔截器)
1.請求失敗了怎么做的
2.網(wǎng)絡(luò)重試是怎么實現(xiàn)的
比如:請求失敗之后降铸,會在結(jié)束之后,添加新的摇零?還是繼續(xù)進(jìn)行重試推掸?重試多少次?
面試官:怎么設(shè)計一個自己的網(wǎng)絡(luò)訪問框架,為什么這么設(shè)計驻仅?
同上:并發(fā)终佛,處理請求,處理響應(yīng)雾家,復(fù)用
先參考現(xiàn)有的框架铃彰,找一個比較合適的框架作為啟動點,比如說芯咧,基于上面講到的okhttp的優(yōu)點牙捉,選擇okhttp的源碼進(jìn)行閱讀,并且將主線的流程抽取出敬飒,為什么這么做邪铲,因為okhttp里面雖然涉及到了很多的內(nèi)容,但是我們用到的內(nèi)容并不是特別多无拗;保證先能運(yùn)行起來一個基本的框架带到;
考慮拓展,有了基本框架之后英染,我會按照我目前在項目中遇到的一些需求或者網(wǎng)路方面的問題揽惹,看看能不能基于我這個框架進(jìn)行優(yōu)化,比如服務(wù)器它設(shè)置的緩存策略四康,
我應(yīng)該如何去編寫客戶端的緩存策略去對應(yīng)服務(wù)器的搪搏,還比如說,可能剛剛?cè)ソ⒒镜目蚣軙r闪金,不會考慮HTTPS的問題疯溺,那么也會基于后來都要求https,進(jìn)行拓展哎垦;
面試官:你是怎么封裝okhttp的囱嫩?
同一的請求頭,然后封裝請求體參數(shù)漏设。處理響應(yīng)墨闲,通過泛型,然后回調(diào)里面解析數(shù)據(jù)
面試官:okhttp如何處理并發(fā)的愿题?
.同步和異步
.3隊列 ?64鏈接 ?5 host ? ? ?同步1個隊列损俭,異步2個隊列
異步請求處理:隊列是否滿蛙奖,否則假如到準(zhǔn)備隊列。然后線程池執(zhí)行杆兵,通過5大攔截器返回響應(yīng)
. 處理完成雁仲,移除 。然后判斷準(zhǔn)備隊列大小琐脏。添加到正在執(zhí)行的隊列里面
其中Dispatcher有一個線程池,用于執(zhí)行異步的請求.并且內(nèi)部還維護(hù)了3個雙向任務(wù)隊列,
分別是:準(zhǔn)備異步執(zhí)行的任務(wù)隊列攒砖、正在異步執(zhí)行的任務(wù)隊列、正在同步執(zhí)行的任務(wù)隊列.
/** Executes calls. Created lazily. */
//這個線程池是需要的時候才會被初始化
? ? private @Nullable
? ? ExecutorServiceexecutorService;
? ? /** Ready async calls in the order they'll be run. */
? ? private final DequereadyAsyncCalls =new ArrayDeque<>();
? ? /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
? ? private final DequerunningAsyncCalls =new ArrayDeque<>();
? ? /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
? ? private final DequerunningSyncCalls =new ArrayDeque<>();
? ? public synchronized ExecutorServiceexecutorService() {
if (executorService ==null) {
//注意,該線程池沒有核心線程,線程數(shù)量可以是Integer.MAX_VALUE個(相當(dāng)于沒有限制),超過60秒沒干事就要被回收
? ? ? ? ? ? executorService =new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
? ? ? ? ? ? ? ? ? ? new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
? ? ? ? }
return executorService;
? ? }
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size()
runningAsyncCalls.add(call);
? ? executorService().execute(call);
? }else {
readyAsyncCalls.add(call);
? }
}
2個隊列:重點一:
首先加上同步鎖日裙,然后判斷實際的運(yùn)行請求數(shù)是否小于允許的最大的請求數(shù)量(64) 并且共享主機(jī)的正在運(yùn)行的調(diào)用的數(shù)量小于同時最大的相同Host的請求數(shù)(5)
同一個服務(wù)器地址不超過5個
public final class Dispatcher {? private int maxRequests = 64;? private int maxRequestsPerHost = 5;
如果這個AsyncCall請求符合條件(判斷實際的運(yùn)行請求數(shù)是否小于允許的最大的請求數(shù)量(64) 并且共享主機(jī)的正在運(yùn)行的調(diào)用的數(shù)量小于同時最大的相同Host的請求數(shù)(5)) 才會添加到執(zhí)行異步請求隊列,然后通過線程池進(jìn)行異步請求否則就把這個AsyncCall請求添加到就緒(等待)異步請求隊列當(dāng)中
如果都符合就把請求添加到正在執(zhí)行的異步請求隊列當(dāng)中,然后通過線程池去執(zhí)行這個請求call,否則的話在就緒(等待)異步請求隊列當(dāng)中添加
面試官:為什么是arrayDeque吹艇,這個隊列,有什么好處
說到這LinkedList表示不服昂拂,我們知道LinkedList同樣也實現(xiàn)了Deque接口受神,內(nèi)部是用鏈表實現(xiàn)的雙端隊列,那為什么不用LinkedList呢格侯?
實際上這與readyAsyncCalls向runningAsyncCalls轉(zhuǎn)換有關(guān)鼻听,當(dāng)執(zhí)行完一個請求或調(diào)用enqueue方法入隊新的請求時,會對readyAsyncCalls進(jìn)行一次遍歷联四,將那些符合條件的等待請求轉(zhuǎn)移到runningAsyncCalls隊列中并交給線程池執(zhí)行撑碴。盡管二者都能完成這項任務(wù),但是由于鏈表的數(shù)據(jù)結(jié)構(gòu)致使元素離散的分布在內(nèi)存的各個位置朝墩,CPU緩存無法帶來太多的便利醉拓,另外在垃圾回收時,使用數(shù)組結(jié)構(gòu)的效率要優(yōu)于鏈表收苏。
面試官:為什么要異步用兩個隊列呢亿卤?
因為Dispatcher默認(rèn)支持最大的并發(fā)請求是64個,單個Host最多執(zhí)行5個并發(fā)請求倒戏,
如果超過怠噪,則Call會先被放入到readyAsyncCall中,當(dāng)出現(xiàn)空閑的線程時杜跷,再將readyAsyncCall中的線程移入到runningAsynCalls中,執(zhí)行請求矫夷。先看Dispatcher的流程葛闷,跟著流程讀源碼
面試官:大于64鏈接之后,準(zhǔn)備都隊列是如何處理的双藕?準(zhǔn)備隊列是如何加入到運(yùn)行隊列里面淑趾?
處理完成一個請求之后,會在finlly調(diào)用忧陪,finish方法
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
? ? int runningCallsCount;
? ? Runnable idleCallback;
? ? synchronized (this) {
? ? ? if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");//將請求移除集合
? ? ? if (promoteCalls) promoteCalls();
? ? ?...
? ? }
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();
? ? ? if (runningCallsForHost(call) < maxRequestsPerHost) {
? ? ? ? i.remove();
? ? ? ? runningAsyncCalls.add(call);
? ? ? ? executorService().execute(call);
? ? ? }
? ? ? if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
? ? }
通過結(jié)束后再來一次
通過攔截器鏈得到Response,然后通過重定向攔截器判斷是否取消扣泊,取消調(diào)用callBack的失敗方法近范,沒有取消就直接返回結(jié)果
最后無論是否取消,都會調(diào)用dispatcher的finish方法延蟹,后面會講到
client.dispatcher().finished(this);
同步源代碼分析:
@Override public Responseexecute()throws IOException {
synchronized (this) {
if (executed)throw new IllegalStateException("Already Executed");
? ? executed =true;
? }
captureCallStackTrace();
? try {
client.dispatcher().executed(this);
? ? Response result = getResponseWithInterceptorChain();
? ? if (result ==null)throw new IOException("Canceled");
? ? return result;
? }finally {
client.dispatcher().finished(this);
? }
}
調(diào)用了dispatercher评矩,添加到隊列client.dispatcher().executed(this);
使用案例:
//1.創(chuàng)建OkHttpClient對象
? ? OkHttpClient okHttpClient =new OkHttpClient();
? ? //2.創(chuàng)建Request對象,設(shè)置一個url地址(百度地址),設(shè)置請求方式阱飘。
? ? Request request =new Request.Builder()
.url("http://www.baidu.com")
.get()
.build();
? ? //3.創(chuàng)建一個call對象,參數(shù)就是Request請求對象
? ? final Call call = okHttpClient.newCall(request);
? ? //4.同步調(diào)用會阻塞主線程,這邊在子線程進(jìn)行
? ? new Thread(new Runnable() {
@Override
? ? ? ? public void run() {
try {
//同步調(diào)用,返回Response,會拋出IO異常
? ? ? ? ? ? ? ? Response response =call.execute();
? ? ? ? ? ? }catch (IOException e) {
e.printStackTrace();
? ? ? ? ? ? }
}
}).start();
}
同步請求總結(jié):在哪個線程回調(diào)斥杜,就在哪個線程處理。
面試官:什么場景用同步請求沥匈?
舉例:分片上傳蔗喂!
異步請求:
executorService().execute(call);
public synchronized ExecutorServiceexecutorService() {
if (executorService ==null) {
executorService =new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
? ? ? ? new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
? }
return executorService;
}
面試官:okhttp線程池工作原理是怎樣的?
在Okhttp中高帖,構(gòu)建了一個核心為[0, Integer.MAX_VALUE]的線程池缰儿,它不保留任何最小線程數(shù),隨時創(chuàng)建更多的線程數(shù)散址,當(dāng)線程空閑時只能活60秒
線程池execute乖阵,其實就是要執(zhí)行線程的run方法有封裝了一層,最終看
AsyncCall的excute方法:
@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) {
if (signalledCallback) {
// Do not signal the callback twice!
? ? ? Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
? ? }else {
responseCallback.onFailure(RealCall.this, e);
? ? }
}finally {
client.dispatcher().finished(this);
? }
}
同步和異步總結(jié):
對于同步和異步請求爪飘,唯一的區(qū)別就是異步請求會放在線程池(ThreadPoolExecutor)中去執(zhí)行义起,而同步請求則會在當(dāng)前線程中執(zhí)行,注意:同步請求會阻塞當(dāng)前線程师崎。
同步請求可以得到respones,異步請求通過回調(diào)得到默终。
面試官:okhttp的攔截器是怎么理解的?
1.攔截器主要處理2個東西犁罩,request和respondse.可以看看源碼的攔截器齐蔽,攔截器主要用來觀察,修改以及可能短路的請求輸出和響應(yīng)的回來床估。
在依次介紹各個攔截器之前含滴,先介紹一個比較重要的類:RealInterceptorChain,直譯就是攔截器鏈類丐巫;
Response result = getResponseWithInterceptorChain();
沒錯谈况,在getResponseWithInterceptorChain();方法中我們就用到了這個RealInterceptorChain類。
ResponsegetResponseWithInterceptorChain()throws IOException {
// Build a full stack of interceptors.
? List 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);
? return chain.proceed(originalRequest);
}
2.責(zé)任鏈模式:(遞歸調(diào)用)循環(huán)調(diào)用递胧,通過不停的調(diào)用自己碑韵。然后最后退出
public Response proceed(Request request)throws IOException {
return this.proceed(request, this.streamAllocation, this.httpCodec, this.connection);
面試官:責(zé)任鏈模式是怎么樣的?
責(zé)任鏈模式的類似情況如下:
publicResponse proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
? ? RealConnection connection)throws IOException {
if (index >=interceptors.size())throw new AssertionError();
? calls++;
? // If we already have a stream, confirm that the incoming request will use it.
? if (this.httpCodec !=null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " +interceptors.get(index -1)
+" must retain the same host and port");
? }
// If we already have a stream, confirm that this is the only call to chain.proceed().
? if (this.httpCodec !=null &&calls >1) {
throw new IllegalStateException("network interceptor " +interceptors.get(index -1)
+" must call proceed() exactly once");
? }
// Call the next interceptor in the chain.
? RealInterceptorChain next =new RealInterceptorChain(
interceptors, streamAllocation, httpCodec, connection, index +1, request);
? Interceptor interceptor =interceptors.get(index);
? Response response = interceptor.intercept(next);
? // Confirm that the next interceptor made its required call to chain.proceed().
? if (httpCodec !=null &&index +1
throw new IllegalStateException("network interceptor " + interceptor
+" must call proceed() exactly once");
? }
// Confirm that the intercepted response isn't null.
? if (response ==null) {
throw new NullPointerException("interceptor " + interceptor +" returned null");
? }
圖解:
每個攔截器負(fù)責(zé)一個特殊的職責(zé).最后那個攔截器負(fù)責(zé)請求服務(wù)器,然后服務(wù)器返回了數(shù)據(jù)再根據(jù)這個攔截器的順序逆序返回回去,最終就得到了網(wǎng)絡(luò)數(shù)據(jù).
demo:http://www.reibang.com/p/ecf55b01ec19
OkHttp的這種攔截器鏈采用的是責(zé)任鏈模式缎脾,這樣的好處是將請求的發(fā)送和處理分開祝闻,并且可以動態(tài)添加中間的處理方實現(xiàn)對請求的處理、短路等操作遗菠。
責(zé)任鏈模式:一個對象持有下個對象的引用联喘;
請求:通過5個攔截器华蜒,把一個一個請求拼接起來
結(jié)果:最后一個先響應(yīng),先進(jìn)后出的原理豁遭。
舉一個列子:出現(xiàn)彈框叭喜,然后一個個關(guān)閉。
攔截器得到響應(yīng)的結(jié)果可以看到這里的攔截鏈?zhǔn)褂玫姆浅G擅畹炭颍悬c像棧的數(shù)據(jù)結(jié)構(gòu)域滥。依次將各個攔截器的方法入棧,最后得到response蜈抓,再依次彈棧启绰。如果是我來寫的話,可能就直接一個for循環(huán)依次調(diào)用每個攔截器的攔截方法沟使。但是這樣的話我還得再來一遍反循環(huán)委可,再來依次處理加工response。很明顯這里棧的結(jié)構(gòu)更符合我們的業(yè)務(wù)場景腊嗡。
?demo:Android 手寫okhttp責(zé)任鏈:
面試官:okhttp有哪些攔截器着倾?它們的作用分別是什么?
五種攔截器
看源碼就知道執(zhí)行順序是
retryAndFollowUpInterceptor-》BridgeInterceptor-》CacheInterceptor-》ConnectInterceptor-》CallServerInterceptor
但是看上面這段源碼發(fā)現(xiàn)好像并不是只有這五種對吧燕少!有兩句代碼卡者,分別是addAll方法,添加了兩個集合客们,集合存儲的是啥崇决?
這里其實是自定義攔截器,可以看到底挫,自定義攔截器添加的順序分別又有兩種
根據(jù)順序分別叫做:Application Interceptors和Network Interceptors
CallServerInterceptor是攔截器鏈中最后一個攔截器恒傻,負(fù)責(zé)將網(wǎng)絡(luò)請求提交給服務(wù)器
攔截器的執(zhí)行順序:先執(zhí)行自定義的攔截器
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));
第一攔截器:RetryAndFollowUpInterceptor(重試,重定向攔截器建邓,code:301,302)
常用的重定向方式有:301 redirect盈厘、302 redirect與meta fresh。
用來實現(xiàn)連接失敗的重試和重定向
重試機(jī)制:
他處于責(zé)任鏈的頂端官边,負(fù)責(zé)網(wǎng)絡(luò)請求的開始工作沸手,也負(fù)責(zé)收尾的工作。
一開始注簿,創(chuàng)建了StreamAllocation對象罐氨,他封裝了網(wǎng)絡(luò)請求相關(guān)的信息:連接池,地址信息滩援,網(wǎng)絡(luò)請求,事件回調(diào)塔嬉,負(fù)責(zé)網(wǎng)絡(luò)連接的連接玩徊、關(guān)閉租悄,釋放等操作
followUpCount是用來記錄我們發(fā)起網(wǎng)絡(luò)請求的次數(shù)的,為什么我們發(fā)起一個網(wǎng)絡(luò)請求恩袱,可能okhttp會發(fā)起多次呢泣棋?
例如https的證書驗證,我們需要經(jīng)過:發(fā)起 -> 驗證 -> 響應(yīng)畔塔,三個步驟需要發(fā)起至少兩次的請求潭辈,或者我們的網(wǎng)絡(luò)請求被重定向,在我們第一次請求得到了新的地址后澈吨,再向新的地址發(fā)起網(wǎng)絡(luò)請求把敢。(發(fā)現(xiàn)遷移到新地址,訪問新的服務(wù)器地址谅辣!)
在網(wǎng)絡(luò)請求中修赞,不同的異常,重試的次數(shù)也不同桑阶,okhttp捕獲了兩種異常:RouteException和IOException柏副。
RouteException:所有網(wǎng)絡(luò)連接失敗的異常,包括IOException中的連接失敗異常蚣录;
IOException:除去連接異常的其他的IO異常割择。
這個時候我們需要判斷是否需要重試:
其中的路由地址我們先忽略,這個之后我們還會討論萎河。假定沒有其他路由地址的情況下:
1荔泳、連接失敗,并不會重試公壤;
2换可、如果連接成功,因為特定的IO異常(例如認(rèn)證失斚梅)沾鳄,也不會重試
其實這兩種情況是可以理解的,如果連接異常确憨,例如無網(wǎng)絡(luò)狀態(tài)译荞,重試也只是毫秒級的任務(wù),不會有特別明顯的效果休弃,如果是網(wǎng)絡(luò)很慢吞歼,到了超時時間,應(yīng)該讓用戶及時了解失敗的原因塔猾,如果一味重試篙骡,用戶就會等待多倍的超時時間,用戶體驗并不好。認(rèn)證失敗的情況就更不用多說了糯俗。
3. 默認(rèn)重連3次??????
RetryAndFollowUpInterceptor攔截設(shè)置最大重定向次數(shù)為20次尿褪;
private static final int MAX_FOLLOW_UPS =20;
如果我們非要重試多次怎么辦?
自定義Interceptor得湘,增加計數(shù)器杖玲,重試到你滿意就可以了:
通過 recover 方法檢測該 RouteException 是否能重新連接;
第二攔截器:BridgeInterceptor(橋接攔截器)
用來修改請求和響應(yīng)的?header?信息
負(fù)責(zé)設(shè)置編碼方式淘正,添加頭部摆马,Keep-Alive 連接以及應(yīng)用層和網(wǎng)絡(luò)層請求和響應(yīng)類型之間的相互轉(zhuǎn)換
public final class BridgeInterceptorimplements Interceptor {
private final CookieJar cookieJar;
? ? public BridgeInterceptor(CookieJar cookieJar) {
this.cookieJar = cookieJar;
? ? }
public Response intercept(Chain chain)throws IOException {
Request userRequest = chain.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 != -1L) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
? ? ? ? ? ? ? ? requestBuilder.removeHeader("Transfer-Encoding");
? ? ? ? ? ? }else {
requestBuilder.header("Transfer-Encoding", "chunked");
? ? ? ? ? ? ? ? requestBuilder.removeHeader("Content-Length");
? ? ? ? ? ? }
}
第三攔截器:CacheInterceptor(緩存攔截器):
用來實現(xiàn)響應(yīng)緩存。比如獲取到的 Response 帶有 Date鸿吆,Expires囤采,Last-Modified,Etag 等 header伞剑,表示該 Response 可以緩存一定的時間斑唬,下次請求就可以不需要發(fā)往服務(wù)端,直接拿緩存的
面試官:okhttp的緩存是怎么樣的黎泣?
緩存策略
1恕刘、如果網(wǎng)絡(luò)不可用并且無可用的有效緩存,則返回504錯誤抒倚;
2褐着、繼續(xù),如果不需要網(wǎng)絡(luò)請求托呕,則直接使用緩存含蓉;
3、繼續(xù)项郊,如果需要網(wǎng)絡(luò)可用馅扣,則進(jìn)行網(wǎng)絡(luò)請求;
4着降、繼續(xù)差油,如果有緩存,并且網(wǎng)絡(luò)請求返回HTTP_NOT_MODIFIED任洞,說明緩存還是有效的蓄喇,則合并網(wǎng)絡(luò)響應(yīng)和緩存結(jié)果。同時更新緩存交掏;
5妆偏、繼續(xù),如果沒有緩存盅弛,則寫入新的緩存钱骂;
對于okhttp的緩存解決方案叔锐,我的需求是:
1、有網(wǎng)的時候也可以讀取緩存罐柳,并且可以控制緩存的過期時間掌腰,這樣可以減輕服務(wù)器壓力
2、有網(wǎng)的時候不讀取緩存张吉,比如一些及時性較高的接口請求
3、無網(wǎng)的時候讀取緩存催植,并且可以控制緩存過期的時間
okhttp&http緩存策略
強(qiáng)制緩存:當(dāng)用戶端第一次請求數(shù)據(jù)是肮蛹,服務(wù)端返回了緩存的過期時間(Expires與Cache-Control),沒有過期即可以繼續(xù)使用緩存创南,否則則不適用伦忠,無需再向服務(wù)端訊問。
比照緩存:當(dāng)用戶端第一次請求數(shù)據(jù)時稿辙,服務(wù)端會將緩存標(biāo)識(Etag/If-None-Match與Last-Modified/If-Modified-Since)與數(shù)據(jù)一起返回給用戶端昆码,用戶端將兩者都備份到緩存中 ,再次請求數(shù)據(jù)時邻储,用戶端將上次備份的緩存
標(biāo)識發(fā)送給服務(wù)端赋咽,服務(wù)端根據(jù)緩存標(biāo)識進(jìn)行判斷,假如返回304吨娜,則表示緩存可用脓匿,假如返回200,標(biāo)識緩存不可用宦赠,使用最新返回的數(shù)據(jù)陪毡。
ETag是用資源標(biāo)識碼標(biāo)識資源能否被修改,Last-Modified是用時間戳標(biāo)識資源能否被修改勾扭。ETag優(yōu)先級高于Last-Modified毡琉。
解決辦法:添加攔截器:
/**
? ? * 有網(wǎng)時候的緩存
? ? */finalInterceptorNetCacheInterceptor=newInterceptor(){@OverridepublicResponseintercept(Chainchain)throwsIOException{Requestrequest=chain.request();Responseresponse=chain.proceed(request);intonlineCacheTime=30;//在線的時候的緩存過期時間,如果想要不緩存妙色,直接時間設(shè)置為0returnresponse.newBuilder().header("Cache-Control","public, max-age="+onlineCacheTime).removeHeader("Pragma").build();}};/**
? ? * 沒有網(wǎng)時候的緩存
? ? */finalInterceptorOfflineCacheInterceptor=newInterceptor(){@Override
緩存策略:
那么服務(wù)器是如何對比呢桅滋?有兩種情況,一種是ETag燎斩。服務(wù)器會對資源做一個ETag算法虱歪,生成一個字符串。這個字符串實際上代表了資源在服務(wù)器的版本栅表。服務(wù)器在響應(yīng)頭中會加入這個ETag給客戶端笋鄙。客戶端緩存后下次繼續(xù)請求的時候把緩存中的ETag取出來加入到請求頭怪瓶。服務(wù)器取出請求頭中的ETag萧落,與當(dāng)前服務(wù)器該資源實時的ETag作對比践美。如果不一致則表明服務(wù)器有更新資源,則返回200響應(yīng)碼找岖,并返回資源陨倡。如果客戶端的ETag和服務(wù)端的相同,代表服務(wù)端沒有更新數(shù)據(jù)许布,則返回304響應(yīng)碼兴革,并不返回資源。這樣就達(dá)到了使用緩存去節(jié)省時間和流量的目的蜜唾。另一種是Last-Modified杂曲,它代表的是資源在服務(wù)器更新的時間,和ETag類似客戶端從緩存中取出這個時間加入到請求頭袁余,服務(wù)器根據(jù)最近的一次更新時間與之對比擎勘,決定是返回資源還是返回304。
總結(jié)下颖榜,如果服務(wù)器通過Cache-Control頭規(guī)定了過期時間棚饵,沒過期的話可以直接使用緩存。否則掩完,使用Etag或者Last-Modified加入請求體噪漾,服務(wù)器接收后與最新的數(shù)據(jù)進(jìn)行比對,決定直接返回最新數(shù)據(jù)還是304響應(yīng)碼藤为。是不是其實一點都不復(fù)雜呢怪与?
我們的客戶端也就是OkHttpClient在緩存這塊要做哪些事呢?
1.首先缅疟,需要取緩存分别,并確定緩存有無過期病涨。
2.如果過期核芽,看看有沒有Etag或Last-Modified可以加到請求體。
3.最后如果服務(wù)器返回304枢赔,我們要直接使用緩存桅咆,
4.如果返回200括授,我們需要更新緩存。按照這個順序岩饼,我們來在源碼中找一找荚虚,這些操作是如何實現(xiàn)的。
1)CacheControl( HTTP 中的Cache-Control 和Pragma 緩存控制):指定緩存規(guī)則
2)Cache(緩存類)
3)DiskLruCache(文件化的LRU 緩存類)
(1)讀取緩存:先獲限OkHttpClient 的Cache 緩存對象籍茧,就是上面創(chuàng)建OkHttpClient 設(shè)置的
Cahce; 傳Request 請求到Cache 的get 方法查找緩存響應(yīng)數(shù)據(jù)Response版述;構(gòu)造一個緩存策略,
再調(diào)用它的get 去決策使用網(wǎng)絡(luò)請求還是緩存響應(yīng)寞冯。若使用緩存渴析,它的cacheResponse 不為
空,networkRequest 為空晚伙,用緩存構(gòu)造響應(yīng)直接返回。若使用請求俭茧,則cacheResponse 為
空,networkRequest 不為空咆疗,開始網(wǎng)絡(luò)請求流程。
Cache 的get 獲取緩存方法母债,計算request 的key 值(請求url 進(jìn)行md5 加密)午磁,根據(jù)key 值
去DisLruCache 查找是否存在緩存內(nèi)容,存則則創(chuàng)建繪存Entry 實體场斑。ENTRY_METADATA 代表
響應(yīng)頭信息漓踢,ENTRY_BODY 代表響應(yīng)體信息。如果緩存存在漏隐,在指定目錄下會有兩個文件
****.0 *****.1 分別存儲某個請求緩存響應(yīng)頭和響應(yīng)體信息。
CacheStrategy 的get 方法:1)若緩存響應(yīng)為空或2)請求是https 但緩存響應(yīng)沒有握手信息奴迅;
3)請求和緩存響應(yīng)都是不可緩存的青责;4)請求是onCache,并且又包含if-Modified-Since 或
If-None-Match 則不使用緩存取具; 再計算請求有效時間是否符合響應(yīng)的過期時間脖隶,若響應(yīng)在有
效范圍內(nèi),則緩存策略使用緩存暇检,否則創(chuàng)建一個新的有條件的請求产阱,返回有條件的緩存策略。
( 2 ) 存儲緩存流程: 從HttpEngine 的readResponse() 發(fā)送請求開始块仆, 判斷
hasBody(userResponse), 如果緩存的話构蹬, maybeCache() 緩存響應(yīng)頭信息,
unzip(cacheWritingResponse(storeRequest, userResponse))緩存響應(yīng)體
3.網(wǎng)絡(luò)請求緩存處理悔据,okhttp 如何處理網(wǎng)絡(luò)緩存的庄敛;
(1)網(wǎng)絡(luò)緩存優(yōu)先考慮強(qiáng)制緩存,再考慮對比緩存
--首先判斷強(qiáng)制緩存中的數(shù)據(jù)的是否在有效期內(nèi)科汗。如果在有效期藻烤,則直接使用緩存。如
果過了有效期头滔,則進(jìn)入對比緩存怖亭。
--在對比緩存過程中,判斷ETag 是否有變動坤检,如果服務(wù)端返回沒有變動兴猩,說明資源未改
變,使用緩存缀蹄。如果有變動峭跳,判斷Last-Modified膘婶。
--判斷Last-Modified,如果服務(wù)端對比資源的上次修改時間沒有變化蛀醉,則使用緩存悬襟,否
則重新請求服務(wù)端的數(shù)據(jù),并作緩存工作
CacheStrategy是其中的緩存策略,Cache類
除了緩存策略類CacheStrategy拯刁,還有一個重要的類Cache沒講脊岳。這里簡單講下這個類就是負(fù)責(zé)存取緩存的。它里面存儲緩存的對象是DiskLruCache垛玻,而DiskLruCache內(nèi)部又是LinkedHashMap<String, Entry>割捅,關(guān)于LinkedHashMap大家有可以查閱一下相關(guān)文章,它底層是一個哈希表和鏈表的組合帚桩,并且由于同時維護(hù)了一個可以給存儲對象排序的雙向鏈表亿驾,因此可以實現(xiàn)Lru算法。
private static boolean validate(Response cached, Response network) {
if (network.code() ==304) {
return true;
? ? }else {
Date lastModified = cached.headers().getDate("Last-Modified");
? ? ? ? if (lastModified !=null) {
Date networkLastModified = network.headers().getDate("Last-Modified");
? ? ? ? ? ? if (networkLastModified !=null && networkLastModified.getTime() < lastModified.getTime()) {
return true;
? ? ? ? ? ? }
}
return false;
? ? }
}
if
(networkRequest ==null && cacheResponse ==null) {
return (new Builder()).request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(EMPTY_BODY).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();
}else if (networkRequest ==null) {
return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();
}else {
分析:
1.cacheResponse也為null說明我們沒有有效的緩存response账嚎,而我們又不會進(jìn)行網(wǎng)絡(luò)請求莫瞬,因此給上層構(gòu)建了一個響應(yīng)碼味504的response
2.如果cacheResponse不為null,說明我們有可用緩存郭蕉,而此次請求又不會再請求網(wǎng)絡(luò)疼邀,因此直接將緩存response返回。
3.重點分析:
1).此時如果服務(wù)器返回的響應(yīng)碼為HTTP_NOT_MODIFIED召锈,也就是我們常見的304旁振,代表服務(wù)器的資源沒有變化,客戶端去取本地緩存即可涨岁,此時服務(wù)器不會返回響應(yīng)體
private static boolean
validate(Response cached, Response network) {
if (network.code() ==304) {
return true;
? ? }else {
Date lastModified = cached.headers().getDate("Last-Modified");
? ? ? ? if (lastModified !=null) {
Date networkLastModified = network.headers().getDate("Last-Modified");
? ? ? ? ? ? if (networkLastModified !=null && networkLastModified.getTime() < lastModified.getTime()) {
return true;
? ? ? ? ? ? }
}
return false;
? ? }
}
2).直接使用networkResponse構(gòu)建response并返回拐袜。此時我們還需要做一件事,就是更新我們的緩存卵惦,將最終response寫入到cache對象中去
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)throws IOException {
if (cacheRequest ==null) {
return response;
? ? }else {
Sink cacheBodyUnbuffered = cacheRequest.body();
? ? ? ? if (cacheBodyUnbuffered ==null) {
return response;
? ? ? ? }else {
final BufferedSource source = response.body().source();
? ? ? ? ? ? final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
? ? ? ? ? ? Source cacheWritingSource =new Source() {
boolean cacheRequestClosed;
? ? ? ? ? ? ? ? public long read(Buffer sink, long byteCount)throws IOException {
long bytesRead;
? ? ? ? ? ? ? ? ? ? try {
bytesRead = source.read(sink, byteCount);
? ? ? ? ? ? ? ? ? ? }catch (IOException var7) {
if (!this.cacheRequestClosed) {
this.cacheRequestClosed =true;
? ? ? ? ? ? ? ? ? ? ? ? ? ? cacheRequest.abort();
? ? ? ? ? ? ? ? ? ? ? ? }
throw var7;
? ? ? ? ? ? ? ? ? ? }
if (bytesRead == -1L) {
if (!this.cacheRequestClosed) {
this.cacheRequestClosed =true;
? ? ? ? ? ? ? ? ? ? ? ? ? ? cacheBody.close();
? ? ? ? ? ? ? ? ? ? ? ? }
return -1L;
? ? ? ? ? ? ? ? ? ? }else {
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
? ? ? ? ? ? ? ? ? ? ? ? cacheBody.emitCompleteSegments();
? ? ? ? ? ? ? ? ? ? ? ? return bytesRead;
? ? ? ? ? ? ? ? ? ? }
}
public Timeout timeout() {
return source.timeout();
? ? ? ? ? ? ? ? }
public void close()throws IOException {
if (!this.cacheRequestClosed && !Util.discard(this, 100, TimeUnit.MILLISECONDS)) {
this.cacheRequestClosed =true;
? ? ? ? ? ? ? ? ? ? ? ? cacheRequest.abort();
? ? ? ? ? ? ? ? ? ? }
source.close();
? ? ? ? ? ? ? ? }
};
? ? ? ? ? ? return response.newBuilder().body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource))).build();
? ? ? ? }
}
}
先了解下HTTP協(xié)議的緩存機(jī)制:
首先緩存分為三種:過期時間緩存阻肿、第一差異緩存和第二差異緩存,而且在優(yōu)先級上沮尿,過期時間緩存 > 第一差異緩存 > 第二差異緩存丛塌。
過期時間緩存,就是通過HTTP響應(yīng)頭部的字段控制:
expires:響應(yīng)字段畜疾,絕對過期時間赴邻,HTTP1.0。
Cache-Control:響應(yīng)字段啡捶,相對過期時間姥敛,HTTP1.1。注意如果值為no-cache瞎暑,表示跳過過期時間緩存邏輯彤敛,值為no-store表示跳過過期時間緩存邏輯和差異緩存邏輯与帆,也就是不使用緩存數(shù)據(jù)。
當(dāng)客戶端請求時墨榄,發(fā)現(xiàn)緩存未過期玄糟,就直接返回緩存數(shù)據(jù)了,不請求網(wǎng)絡(luò)袄秩,否則阵翎,執(zhí)行第一差異緩存邏輯:
If-None-Match:請求字段,值為ETag之剧。
ETag:響應(yīng)字段郭卫,服務(wù)端會根據(jù)內(nèi)容生成唯一的字符串。
如果服務(wù)端發(fā)現(xiàn)If-None-Match的值和當(dāng)前ETag一樣背稼,就說明數(shù)據(jù)內(nèi)容沒有變化贰军,就返回304,否則蟹肘,執(zhí)行第二差異緩存邏輯:
If-Modified-Since:請求字段谓形,客戶端告訴服務(wù)端本地緩存的資源的上次修改時間。
Last-Modified:響應(yīng)字段疆前,服務(wù)端告訴客戶端資源的最后修改時間。
如果服務(wù)端發(fā)現(xiàn)If-Modified-Since的值就是資源的最后修改時間聘萨,就說明數(shù)據(jù)內(nèi)容沒有變化竹椒,就返回304,否則米辐,返回所有資源數(shù)據(jù)給客戶端胸完,響應(yīng)碼為200。
回到OkHttp翘贮,CacheInterceptor攔截器處理的邏輯赊窥,其實就是上面所說的HTTP緩存邏輯,注意到OkHttp提供了一個現(xiàn)成的緩存類Cache狸页,它采用DiskLruCache實現(xiàn)緩存策略锨能,至于緩存的位置和大小,需要你自己指定芍耘。
這里其實會有個問題址遇,上面的緩存都是依賴HTTP協(xié)議本身的緩存機(jī)制的,如果我們請求的服務(wù)器不支持這套緩存機(jī)制斋竞,或者需要實現(xiàn)更靈活的緩存管理倔约,直接使用上面這套緩存機(jī)制就可能不太可行了,這時我們可以自己新增攔截器坝初,自行實現(xiàn)緩存的管理浸剩。
第四攔截器:
ConnectInterceptor負(fù)責(zé)與服務(wù)器建立鏈接:很重要(是最后一個攔截器)
和服務(wù)器通信
我們發(fā)現(xiàn)目前為止我們還沒有進(jìn)行真正的請求钾军。別急,ConnectInterceptor就是一個負(fù)責(zé)建立http連接的攔截器
封裝了socket連接和TLS握手等邏輯
用來打開到服務(wù)端的連接绢要。其實是調(diào)用了 StreamAllocation 的newStream 方法來打開連接的吏恭。建聯(lián)的 TCP 握手,TLS 握手都發(fā)生該階段袖扛。過了這個階段砸泛,和服務(wù)端的 socket 連接打通
public final class RealConnectionextends Listenerimplements Connection {
private final Route route;
? ? private Socket rawSocket;
? ? public Socket socket;
? ? private Handshake handshake;
? ? private Protocol protocol;
? ? public volatile FramedConnection framedConnection;
? ? public int successCount;
? ? public BufferedSource source;
? ? public BufferedSink sink;
? ? public int allocationLimit;
? ? public final List> allocations =new ArrayList();
? ? public boolean noNewStreams;
? ? public long idleAtNanos =9223372036854775807L;
? ? public RealConnection(Route route) {
this.route = route;
? ? }
2)然后在ConnectInterceptor,也就是負(fù)責(zé)建立連接的攔截器中蛆封,首先會找可用連接唇礁,也就是從連接池中去獲取連接,具體的就是會調(diào)用到ConectionPool的get方法惨篱。
需要看下這個源碼:ConnectInterceptor的intercept()方法
publicResponseintercept(Chainchain)throwsIOException{RealInterceptorChainrealChain=(RealInterceptorChain)chain;Requestrequest=realChain.request();StreamAllocationstreamAllocation=realChain.streamAllocation();// We need the network to satisfy this request. Possibly for validating a conditional GET.booleandoExtensiveHealthChecks=!request.method().equals("GET");// 1HttpCodechttpCodec=streamAllocation.newStream(client,chain,doExtensiveHealthChecks);RealConnectionconnection=streamAllocation.connection();returnrealChain.proceed(request,streamAllocation,httpCodec,connection);}
來源:http://www.reibang.com/p/7cdd598d382b
第五攔截器:CallServerInterceptor
主要的工作就是把請求的Request寫入到服務(wù)端盏筐,而后從服務(wù)端讀取Response。
(1)砸讳、寫入請求頭
(2)琢融、寫入請求體
(3)、讀取響應(yīng)頭
(4)簿寂、讀取響應(yīng)體
用來發(fā)起請求并且得到響應(yīng)漾抬。上一個階段已經(jīng)握手成功,HttpStream 流已經(jīng)打開常遂,所以這個階段把 Request 的請求信息傳入流中纳令,并且從流中讀取數(shù)據(jù)封裝成 Response 返回
面試官:自定義過哪些攔截器?
自定義攔截器:通常情況下攔截器用來添加克胳,移除或者轉(zhuǎn)換請求或者響應(yīng)的頭部信息平绩。比如將域名替換為ip地址,將請求頭中添加host屬性漠另,也可以添加我們應(yīng)用中的一些公共參數(shù)捏雌,比如設(shè)備id、版本號等等
okhttp攔截器主要在以下幾種情況使用:
自定義攔截器不一定要繼承基本的5大攔截器笆搓,而是繼承Interceptor
網(wǎng)絡(luò)請求性湿、響應(yīng)日志輸出
在Header中統(tǒng)一添加cookie、token
設(shè)置網(wǎng)絡(luò)緩存
1.添加日志攔截器
可以用系統(tǒng)的砚作,或者通過添加自己寫的攔截器
2.在Header中統(tǒng)一添加cookie窘奏、token ?
public?class?HeaderInterceptor?implements?Interceptor?
面試官:okhttp是如何實現(xiàn)連接池復(fù)用的?
OkHttp的底層是通過Java的Socket發(fā)送HTTP請求與接受響應(yīng)的(這也好理解葫录,HTTP就是基于TCP協(xié)議的)着裹,但是OkHttp實現(xiàn)了連接池的概念,
KeepAlive
當(dāng)然大量的連接每次連接關(guān)閉都要三次握手四次分手的很顯然會造成性能低下,因此http有一種叫做keepalive connections的機(jī)制骇扇,它可以在傳輸數(shù)據(jù)后仍然保持連接摔竿,當(dāng)客戶端需要再次獲取數(shù)據(jù)時,直接使用剛剛空閑下來的連接而不需要再次握手
我們可以Reuqst的header中將Connection設(shè)置為keepalive來復(fù)用連接少孝。
什么時候用到鏈接池復(fù)用继低?
同一端口和同一域名。socket復(fù)用稍走。減少3次握手袁翁,4次揮手。連接池源碼分析婿脸。
即對于同一主機(jī)的多個請求粱胜,其實可以公用一個Socket連接,而不是每次發(fā)送完HTTP請求就關(guān)閉底層的Socket狐树,這樣就實現(xiàn)了連接池的概念焙压。而OkHttp對Socket的讀寫操作使用的OkIo庫進(jìn)行了一層封裝。
2 okhttp連接池復(fù)用是具體的一個連接還是同一個域名下的連接都可以復(fù)用抑钟?
是同一個域名下連接都可以復(fù)用涯曲,服務(wù)器和PC瀏覽器同一個域名下只能建立6個TCP 連接,為了讓同一個網(wǎng)頁中的圖片快速加載在塔,所以要把圖片放到不同的域名下幻件,這樣就可以實現(xiàn)>6個的連接請求。
源碼中同一域名下默認(rèn)是5個TCP接連蛔溃,超過后會等待(這個是分發(fā)器要求的5)不同域名下最多64個請求,但是大部分時候同一個域名比較多
會從很多常用的連接問題中自動恢復(fù)傲武。如果您的服務(wù)器配置了多個IP地址,當(dāng)?shù)谝粋€IP連接失敗的時候城榛,OkHttp會自動嘗試下一個IP,此外OkHttp還處理了代理服務(wù)器問題和SSL握手失敗問題态兴。
如上代碼設(shè)置相當(dāng)于沒有復(fù)用 keep-alive導(dǎo)致每次請求都需要重新進(jìn)行 DNS解析狠持,3次握手4次揮手操作,這樣是非常浪費(fèi)性能的瞻润,源碼中默認(rèn)的是5個空閑TCP接連喘垂,并且活躍時間為5分鐘。
通一個請求地址绍撞?
Okhttp支持5個并發(fā)KeepAlive正勒,默認(rèn)鏈路生命為5分鐘(鏈路空閑后,保持存活的時間)傻铣。
創(chuàng)建連接池實在OkHttpClient初始化的時候章贞,ConnectinoPool通過直接new出的
public ConnectionPool() {
this(5, 5L, TimeUnit.MINUTES);
}
public final class ConnectionPool {
private static final Executor executor;
? ? private final int maxIdleConnections;
? ? private final long keepAliveDurationNs;
? ? private final Runnable cleanupRunnable;
? ? private final Deque connections;
? ? final RouteDatabase routeDatabase;
? ? boolean cleanupRunning;
? ? public ConnectionPool() {
this(5, 5L, TimeUnit.MINUTES);
? ? }
RealConnection get(Address address, StreamAllocation streamAllocation) {
assert Thread.holdsLock(this);
? ? Iterator var3 =this.connections.iterator();
? ? RealConnection connection;
? ? do {
if (!var3.hasNext()) {
return null;
? ? ? ? }
connection = (RealConnection)var3.next();
? ? }while(connection.allocations.size() >= connection.allocationLimit || !address.equals(connection.route().address) || connection.noNewStreams);
? ? streamAllocation.acquire(connection);
? ? return connection;
}
主要的變量有必要說明一下:
什么時候鏈接?什么時候獲确侵蕖鸭限?什么時候添加蜕径?什么時候移除
在第四個鏈接攔截器的時候添加和獲取的,還有鏈接的败京。然后移除通過一個線程一直移除
executor線程池兜喻,類似于CachedThreadPool,需要注意的是這種線程池的工作隊列采用了沒有容量的SynchronousQueue
Deque<RealConnection>赡麦,雙向隊列朴皆,雙端隊列同時具有隊列和棧性質(zhì),經(jīng)常在緩存中被使用泛粹,里面維護(hù)了RealConnection也就是socket物理連接的包裝遂铡。
RouteDatabase,它用來記錄連接失敗的Route的黑名單戚扳,當(dāng)連接失敗的時候就會把失敗的線路加進(jìn)去忧便。
? ? ??ConnectionPool:?連接池
static {
executor =new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));
}
可以看出連接池復(fù)用的核心就是用Deque<RealConnection>來存儲連接
里面的線程池干嘛的?去清理無用鏈接帽借。Executor的線程池是用來清理閑置的連接的
在cleanupRunnable的run方法會不停的調(diào)用cleanup清理并返回下一次清理的時間間隔珠增。然后進(jìn)入wait,等待下一次的清理砍艾。那么cleanup()是怎么計算時間間隔的蒂教?
鏈路的概念
Collection
HttpCodec
StreamAllocation
ConnectionPool
HttpEngine在發(fā)起請求之前,會先調(diào)用nextConnection()來獲取一個Connection對象脆荷,如果可以從ConnectionPool中獲取一個Connection對象凝垛,就不會新建,
如果無法獲取蜓谋,就會調(diào)用createnextConnection()來新建一個Connection對象梦皮,
這就是okhttp多路復(fù)用的核心,不像之前的網(wǎng)絡(luò)框架桃焕,無論有沒有剑肯,都會新建Connection對象。
http://www.reibang.com/p/8928f4b128f1
https://www.cnblogs.com/tony-yang-flutter/p/12383267.html
復(fù)用第一要素:ConnectionPool 要點(對鏈接隊列進(jìn)行添加观堂,移除操作)
ConnectionPool提供對Deque<RealConnection>進(jìn)行操作的方法分別為put让网、get、connectionBecameIdle师痕、evictAll幾個操作溃睹。分別對應(yīng)放入連接、獲取連接胰坟、移除連接因篇、移除所有連接操作。
管理http和http/2的鏈接,以便減少網(wǎng)絡(luò)請求延遲惜犀。同一個address將共享同一個connection铛碑。該類實現(xiàn)了復(fù)用連接的目標(biāo)。
ConnectionPool內(nèi)部以隊列方式存儲連接
連接池最多維持5個連接虽界,且每個鏈接最多活5分鐘
每次添加鏈接的時候回執(zhí)行一次清理任務(wù)汽烦,清理空閑的鏈接(RealConnection)。
連接池的清理和回收:我們在put新連接到隊列的時候會先執(zhí)行清理閑置連接的線程
復(fù)用第二要素:RealConnection 要點
RealConnection是socket物理連接的包裝莉御,
真實連接:public final class RealConnectionextends Listenerimplements Connection {
代表著鏈接socket的鏈路撇吞,如果擁有了一個RealConnection就代表了我們已經(jīng)跟服務(wù)器有了一條通信鏈路
實現(xiàn)了三次握手等操作。
Put方法:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
? ? if (!cleanupRunning) {
cleanupRunning =true;
? ? ? ? executor.execute(cleanupRunnable);
? ? }
connections.add(connection);
}
歷connections緩存列表礁叔,當(dāng)某個連接計數(shù)的次數(shù)小于限制的大小并且request的地址和緩存列表中此連接的地址完全匹配牍颈。則直接復(fù)用緩存列表中的connection作為request的連接。
在RealConnectionPool中維護(hù)了一個線程池琅关,來進(jìn)行回收和復(fù)用煮岁;connections是一個記錄連接的雙端隊列;routeDatabase是記錄路由失敗的線路涣易,cleanupRunnable是用來進(jìn)行自動回收連接的画机。
然后我們重點看一下cleanupRunnable:首先是一個死循環(huán),一直執(zhí)行新症,通過cleanup方法進(jìn)行回收連接步氏,并返回了下次清理的間隔時間(以納米為單位,下次調(diào)用這個方法的時間)徒爹,-1表示不需要再進(jìn)行回收荚醒,則跳出循環(huán),否則一直進(jìn)行回收隆嗅,同時讓連接池RealConnectionPool等待
根據(jù)連接中的引用計數(shù)來計算空閑連接數(shù)和活躍連接數(shù)界阁,然后標(biāo)記出空閑的連接,如果空閑連接keepAlive時間超過5分鐘胖喳,或者空閑連接數(shù)超過5個铺董,則從Deque中移除此連接。接下來根據(jù)空閑連接或者活躍連接來返回下次需要清理的時間數(shù):如果空閑連接大于0則返回此連接即將到期的時間禀晓,如果都是活躍連接并且大于0則返回默認(rèn)的keepAlive時間5分鐘,如果沒有任何連接則跳出循環(huán)并返回-1
Clear方法:
首先會對緩存中的連接進(jìn)行遍歷坝锰,以尋找一個閑置時間最長的連接粹懒,然后根據(jù)該連接的閑置時長和最大允許的連接數(shù)量等參數(shù)來決定是否應(yīng)該清理該連接。同時注意上面的方法的返回值是一個時間顷级,如果閑置時間最長的連接仍然需要一段時間才能被清理的時候凫乖,會返回這段時間的時間差,然后會在這段時間之后再次對連接池進(jìn)行清理。
復(fù)用第三要素:Connection 要點
復(fù)用第四要素:StreamAllocation 要點(計數(shù)對象帽芽。判斷是否是空閑的 )删掀。
如何判斷閑置的連接:主要通過弱引用和計數(shù)的方式
在遍歷緩存列表的過程中
使用連接數(shù)目inUseConnectionCount 和閑置連接數(shù)目idleConnectionCount 的計數(shù)累加值都是通過pruneAndGetAllocationCount()?是否大于0來控制的。
那么很顯然pruneAndGetAllocationCount() 方法就是用來識別對應(yīng)連接是否閑置的导街。>0則不閑置披泪。否則就是閑置的連接。
在RealConnection中保存了StreamAllocationd的一個列表搬瑰,作為連接上流的計數(shù)器款票。如果列表大小為0,表示連接是空閑的泽论,可以回收艾少;否則連接還在用,不能關(guān)閉翼悴。
對StreamAllocation使用了弱引用包裝缚够。只要弱引用還存在,說明連接還在用鹦赎。
總結(jié):清理閑置連接的核心主要是引用計數(shù)器List<Reference<StreamAllocation>>?和?選擇排序的算法以及excutor的清理線程池谍椅。
一個鏈接對應(yīng)多個流,鏈接和流的對應(yīng)關(guān)系通過StreamAllocation來記錄钙姊。
判斷connection.allocations列表中能否有StreamAllocation毯辅,假如沒有就是空閑連接,否則不是煞额。
它里面維護(hù)了List<Reference<StreamAllocation>>的引用思恐。List中StreamAllocation的數(shù)量也就是socket被引用的計數(shù),如果計數(shù)為0的話膊毁,說明此連接沒有被使用就是空閑的胀莹,需要通過下文的算法實現(xiàn)回收;如果計數(shù)不為0婚温,則表示上層代碼仍然引用描焰,就不需要關(guān)閉連接。
在okhttp中栅螟,在高層代碼的調(diào)用中荆秦,使用了類似于引用計數(shù)的方式跟蹤Socket流的調(diào)用,這里的計數(shù)對象是StreamAllocation力图,它被反復(fù)執(zhí)行aquire與release操作步绸,這兩個函數(shù)其實是在改變RealConnection中的List<Reference<StreamAllocation>>?的大小。(StreamAllocation.Java)吃媒。
List中StreamAllocation的數(shù)量也就是socket被引用的計數(shù)
public final class StreamAllocation {
? public void acquire(RealConnection connection) {
? ? connection.allocations.add(new WeakReference<>(this));
? }
? private void release(RealConnection connection) {
? ? for (int i = 0, size = connection.allocations.size(); i < size; i++) {
? ? ? Reference<StreamAllocation> reference = connection.allocations.get(i);
? ? ? if (reference.get() == this) {
? ? ? ? connection.allocations.remove(i);
? ? ? ? return;
? ? ? }
? ? }
? ? throw new IllegalStateException();
? }
復(fù)用第五要素:RouteDatabase瓤介,它用來記錄連接失敗的Route的黑名單吕喘,當(dāng)連接失敗的時候就會把失敗的線路加進(jìn)去
面試官:okhttp是如何保證通信安全的?
面試官:okhttp如何方法https里面怎么處理SSL刑桑?
客戶端默認(rèn)信任全部證書
X509TrustManager
SSLSocketFactory
客戶端默認(rèn)信任全部證書
面試官:OKhttp針對網(wǎng)絡(luò)層有哪些優(yōu)化氯质?
另外一個博客
面試官:okhttp,在訪問一個界面沒有結(jié)束祠斧,關(guān)閉activity闻察。還是會,這個要驗證一下梁肿,那么下一次進(jìn)去重新訪問接口還是--------
網(wǎng)絡(luò)請求如何取消蜓陌?底層是怎么實現(xiàn)的
還是會執(zhí)行
面試官:如何修改服務(wù)器返回的數(shù)據(jù)類型修改
Response的body().string()方法獲取返回回來的json數(shù)據(jù)(也可以是其他類型的數(shù)據(jù)(XML類型) 這個需要和服務(wù)器端商量好)
面試官:delay和delayqueue
面試官:OkHttp中為什么使用構(gòu)建者模式?
使用多個簡單的對象一步一步構(gòu)建成一個復(fù)雜的對象吩蔑;
優(yōu)點:?當(dāng)內(nèi)部數(shù)據(jù)過于復(fù)雜的時候钮热,可以非常方便的構(gòu)建出我們想要的對象,并且不是所有的參數(shù)我們都需要進(jìn)行傳遞烛芬;
缺點:?代碼會有冗余
好封裝
面試官:OkHttp中模板方法設(shè)計模式是怎樣的隧期?
面試官:?okhttp的缺點?
就是責(zé)任鏈模式的缺點:當(dāng)然它也有缺點:因為調(diào)用鏈路長赘娄,而且存在嵌套仆潮,遇到問題排查其它比較麻煩。
面試官:okio是用來做什么的遣臼?
寫流