目錄:
1. okhttp做了哪些優化
2. okhttp的線程池邏輯
3. okhttp的5大攔截器
4. okhttp的責任鏈模式
5. 攔截器之連接攔截器
5. okhttp的鏈接池復用
6. okhttp的緩存機制
7. OKHttp框架中用到了哪些設計模式?
3.14.x版本及以前的版本,采用Java語言編寫,4.0.0以后采用kotlin語言
1. okhttp做了哪些優化
1.1 為什么用Okhttp,而不選擇其它網絡框架?
關于HTTP2的優點,主要有:
1). 支持HTTP2/SPDY,允許所有同一個主機地址的請求共享同一個Socket連接
(SPDY是Google開發的基于TCP的傳輸層協議,用以最小化網絡延遲,提升網絡速度,優化用戶的網絡使用體驗)
在早期的版本中,OkHttp支持Http1.0,1.1,SPDY協議,但是Http2協議的問世,導致OkHttp也做出了改變,OkHttp鼓勵開發者使用HTTP2,不再對SPDY協議給予支持。另外,新版本的OkHttp還有一個新的 亮點就是支持WebScoket,這樣我 們就可以非常方便的建立長連接了
- .多路復用:就是針對同個域名的請求,都可以在同一條連接中并行進行,而且頭部和數據都進行了二進制封裝。
連接池減少請求延時(socket自動選擇最好路線,并支持自動重連,擁有自動維護的socket連接池,減少握手次數,減少了請求延遲,共享Socket,減少對服務器的請求次數。
.透明的GZIP壓縮減少響應數據的大小(基于Headers的緩存策略減少重復的網絡請求), 減少響應數據的大小{{{擁有Interceptors輕松處理請求與響應(自動處理GZip壓縮)}}},使用Gzip來壓縮request和response, 減少傳輸數據量, 從而減少流量消耗.重試及重定向
.頭部壓縮:HTTP1.x每次請求都會攜帶完整的頭部字段,所以可能會出現重復傳輸,因此HTTP2采用HPACK對其進行壓縮優化,可以節省不少的傳輸流量。
.二進制分幀:傳輸都是基于字節流進行的,而不是文本,二進制分幀層處于應用層和傳輸層之間。
簡單來說就是:
OkHttp是一個非常優秀的網絡請求框架,已被谷歌加入到Android的源碼中。
- 支持http2,對一臺機器的所有請求共享同一個socket 。2是否支持呢????不是1.2
- 內置連接池,支持連接復用,減少延遲
- 支持透明的gzip壓縮響應體
- 通過緩存避免重復的請求
- 請求失敗時自動重試主機的其他ip,自動重定向
- 很多設計模式,好用的API。鏈式調用
面試官:okhttp支持的協議是什么?
支持 http2.0、websocket,spdy 等協議
面試官:android支持http2.0,Okhttp如何開啟的Http2.0?
https://blog.csdn.net/weixin_42522669/article/details/117576189
為什么只要后端將接口升級到Http2.0的支持之后,客戶端就能自動的把所有的請求切換到Http2.0上呢?
Http2.0的前置條件是實現了https。
有非常好的握手過程
自動的一個過程:ALPN協議,是TLS的擴展,瀏覽器是基于ALPN協議來判斷服務器是否支持HTTP2協議。
當后端支持的協議內包含Http2.0時,則就會把請求升級到Http2.0階段。
2. 看過OkHttp的源碼嗎,簡單說一下
2.1 三條主線:
第一。分發器(處理高并發請求)
- 1).用法解析 同步和異步 怎么實現的
- 2).okhttp線程池工作原理
- 3).同步隊列里面,運行隊列和準備怎么工作的
- 4).如何實現并發的?并發控制
- 第二。攔截器(每個攔截器的作用)
- 1).責任鏈模式
- 2).攔截器 緩存機制 緩存基于DiskLruCache
- 3).自定義攔截器
- 4).多域名如何封裝?測試和正式如何封裝
- 第三。網絡攔截器ConnectionIntercepter原理
- 1).攔截器 socker連接池復用機制詳解
- 2) .在無網的時候直接使用緩存,這該怎么做呢?很簡單,我們只要自定義一個攔截器,在我們的請求頭中判斷沒有網絡可用時,緩存策略為強制使用緩存。
- 3). 緩存機制是怎么樣的?網絡請求緩存處理,okhttp如何處理網絡緩存的?
2. 2 怎么設計一個自己的網絡訪問框架,為什么這么設計?
同上:并發,處理請求,處理響應,復用
先參考現有的框架,找一個比較合適的框架作為啟動點,比如說,基于上面講到的okhttp的優點,選擇okhttp的源碼進行閱讀,并且將主線的流程抽取出,為什么這么做,因為okhttp里面雖然涉及到了很多的內容,但是我們用到的內容并不是特別多;保證先能運行起來一個基本的框架;
考慮拓展,有了基本框架之后,我會按照我目前在項目中遇到的一些需求或者網路方面的問題,看看能不能基于我這個框架進行優化,比如服務器它設置的緩存策略,
我應該如何去編寫客戶端的緩存策略去對應服務器的,還比如說,可能剛剛去建立基本的框架時,不會考慮HTTPS的問題,那么也會基于后來都要求https,進行拓展;
3. okhttp的線程池邏輯
3.1 問題: okhttp如何處理并發的?
同步和異步 同步1個隊列,異步2個隊列
3隊列 64鏈接 5 host
異步請求處理:隊列是否滿,否則假如到準備隊列。然后線程池執行,通過5大攔截器返回響應,處理完成,移除 。然后判斷準備隊列大小。添加到正在執行的隊列里面
面試官:為什么既要隊列又要線程池?下載文件為啥不用?
隊列:64總量,最多存放 。線程池:控制同時并非的個數,處理完從隊列取
下載文件:只用了隊列+協程。處理了一個用下一個
其中Dispatcher這個類,有一個線程池,用于執行異步的請求.并且內部還維護了3個雙向任務隊列,3個就是上面說的
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
/** Executes calls. Created lazily. */
private @Nullable ExecutorService executorService;
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public synchronized ExecutorService executorService() {
if (executorService == null) {
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() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
2個隊列:重點一:
同一個服務器地址不超過5個
如果這個AsyncCall請求符合條件(判斷實際的運行請求數是否小于允許的最大的請求數量(64) 并且共享主機的正在運行的調用的數量小于同時最大的相同Host的請求數(5)) 才會添加到執行異步請求隊列,然后通過線程池進行異步請求否則就把這個AsyncCall請求添加到就緒(等待)異步請求隊列當中
如果都符合就把請求添加到正在執行的異步請求隊列當中,然后通過線程池去執行這個請求call,否則的話在就緒(等待)異步請求隊列當中添加
SynchronousQueue:經典的生產者-消費者模式
不像ArrayBlockingQueue、LinkedBlockingDeque之類的阻塞隊列依賴AQS實現并發操作,SynchronousQueue直接使用CAS實現線程的安全訪問
面試官:為什么要異步用兩個隊列呢?
因為Dispatcher默認支持最大的并發請求是64個,單個Host最多執行5個并發請求,
Call會先被放入到readyAsyncCall中,當出現空閑的線程時,再將readyAsyncCall中的線程移入到runningAsynCalls中,執行請求。先看Dispatcher的流程
面試官:大于64鏈接之后,準備都隊列是如何處理的?準備隊列是如何加入到運行隊列里面?
通過結束后再來一次,處理完成一個請求之后,會在finlly調用,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.
}
通過攔截器鏈得到Response,然后通過重定向攔截器判斷是否取消,取消調用callBack的失敗方法,沒有取消就直接返回結果
最后無論是否取消,都會調用dispatcher的finish方法,后面會講到
同步請求總結:在哪個線程回調,就在哪個線程處理。
面試官:什么場景用同步請求?
舉例:分片上傳
3.2 面試官:okhttp線程池工作原理是怎樣的?
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;
}
在Okhttp中,構建了一個核心為[0, Integer.MAX_VALUE]的線程池,它不保留任何最小線程數,隨時創建更多的線程數,當線程空閑時只能活60秒
線程池execute,其實就是要執行線程的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);
}
}
同步和異步總結:
對于同步和異步請求,唯一的區別就是異步請求會放在線程池中去執行,而同步請求則會在當前線程中執行,注意:同步請求會阻塞當前線程。
同步請求可以得到respones,異步請求通過回調得到。最終都是經過攔截器
4. okhttp的5大攔截器
面試官:okhttp的攔截器是在哪個類添加的?
RealCall,它返回相應Respond
面試官:響應是如何得到
通過攔截器
4.1. 面試官:okhttp的攔截器是怎么理解的?
1.攔截器主要處理2個東西,request和respond.可以看看源碼的攔截器,攔截器主要用來觀察,修改以及可能短路的請求輸出和響應的回來。
先介紹一個比較重要的類:RealInterceptorChain,直譯就是攔截器鏈類
Response result = getResponseWithInterceptorChain();
沒錯,在RealCall類的 getResponseWithInterceptorChain();方法中我們就用到了這個RealInterceptorChain類。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}
4.2. 自定義攔截器
但是看上面這段源碼發現好像并不是只有這五種對吧!有兩句代碼,分別是addAll方法,添加了兩個集合,集合存儲的是啥?
這里其實是自定義攔截器,可以看到,自定義攔截器添加的順序分別又有兩種,根據順序分別叫做:Application Interceptors和Network Interceptors
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));
面試官:自定義過哪些攔截器?
自定義攔截器:通常情況下攔截器用來添加,移除或者轉換請求或者響應的頭部信息。比如將域名替換為ip地址,將請求頭中添加host屬性,也可以添加我們應用中的一些公共參數,比如設備id、版本號等等
okhttp攔截器主要在以下幾種情況使用:
自定義攔截器不一定要繼承基本的5大攔截器,而是繼承Interceptor
網絡請求、響應日志輸出
在Header中統一添加cookie、token
設置網絡緩存
1.添加日志攔截器
可以用系統的,或者通過添加自己寫的攔截器
2.在Header中統一添加cookie、token
public class HeaderInterceptor implements Interceptor
面試官:我想要打印請求的一些信息以及返回的一些信息怎么處理 ?
答:這個我們可以利用上面所說的攔截器來實現,通過OkHttp的建造者模式構建OkHttpClient時的一個addInterceptor方法添加一個自定義攔截器,實現Interceptor的intercept方法,利用Chain對象可以獲取到Request信息以及Response信息。
4.3 5大攔截器的分別介紹
4.3.1 RetryAndFollowUpInterceptor(重試,重定向攔截器,code:301,302)
他處于責任鏈的頂端,負責網絡請求的開始工作,也負責收尾的工作。
常用的重定向方式有:301 redirect、302 redirect與meta fresh。 用來實現連接失敗的重試和重定向
RetryAndFollowUpInterceptor攔截設置最大重定向次數為20次;不同情況不一樣
/**
* How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
* curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
*/
private static final int MAX_FOLLOW_UPS = 20;
一開始,創建了StreamAllocation對象,他封裝了網絡請求相關的信息:連接池,地址信息,網絡請求,事件回調,負責網絡連接的連接、關閉,釋放等操作
followUpCount是用來記錄我們發起網絡請求的次數的,為什么我們發起一個網絡請求,可能okhttp會發起多次呢?
例如https的證書驗證,我們需要經過:發起 -> 驗證 -> 響應,三個步驟需要發起至少兩次的請求,或者我們的網絡請求被重定向,在我們第一次請求得到了新的地址后,再向新的地址發起網絡請求。(發現遷移到新地址,訪問新的服務器地址!)
面試官:重試機制是怎么樣的?
里面有個while(true)循環。通過continius和return退出循環
在網絡請求中,不同的異常,重試的次數也不同,okhttp捕獲了兩種異常:RouteException和IOException。
RouteException:所有網絡連接失敗的異常,包括IOException中的連接失敗異常;
IOException:除去連接異常的其他的IO異常。
這個時候我們需要判斷是否需要重試:
其中的路由地址我們先忽略,這個之后我們還會討論。假定沒有其他路由地址的情況下:
1)、連接失敗,并不會重試;
2)、如果連接成功,因為特定的IO異常(例如認證失敗),也不會重試
其實這兩種情況是可以理解的,如果連接異常,例如無網絡狀態,重試也只是毫秒級的任務,不會有特別明顯的效果,如果是網絡很慢,到了超時時間,應該讓用戶及時了解失敗的原因,如果一味重試,用戶就會等待多倍的超時時間,用戶體驗并不好。認證失敗的情況就更不用多說了。
3). 默認重連3次?
不是,通過一系列的判斷,recover ()方法.達到一定條件
如果我們非要重試多次怎么辦?
自定義Interceptor,增加計數器,重試到你滿意就可以了: 通過 recover 方法檢測該 RouteException 是否能重新連接;
總結:只有在特定情況下,okhttp才會重試
4.3.2 BridgeInterceptor(橋接攔截器)
用來修改請求和響應的 header 信息,Zip壓縮配置
負責設置編碼方式,添加頭部,Keep-Alive 連接以及應用層和網絡層請求和響應類型之間的相互轉換
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder()
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
}
4.3.3 CacheInterceptor(緩存攔截器)
http的緩存機制:根據 緩存是否過期、過期后是否有修改(1.緩存,2.etag 3. lastModify)
用來實現響應緩存。比如獲取到的 Response 帶有 Date,Expires,Last-Modified,Etag 等 header,表示該 Response 可以緩存一定的時間,下次請求就可以不需要發往服務端,直接拿緩存的。一些名詞如下:
1).Etag是 Entity tag的縮寫,可以理解為“被請求變量的實體值” ,它是關于緩存的一個字段,每次請求都會存在的一個標識符,將文本哈希編碼來標識當前文本的狀態
2).Last-Modified字段是否存在,這個字段表示響應中資源最后一次修改的時間
3). 304: Not Modified 客戶端有緩沖的文檔并發出了一個條件性的請求(一般是提供If-Modified-Since頭表示客戶只想比指定日期更新的文檔)。服務器告訴客戶,原來緩沖的文檔還可以繼續使用。
面試官:okhttp的緩存是怎么樣的?網絡請求緩存處理,okhttp 如何處理網絡緩存的?
在緩存攔截器里面。
總結: 沒有緩存 ------> 直接請求服務器
eTag------>Last-Modified沒過期,返回304--用本地
eTag------>Last-Modified過期-------->請求服務器
沒有eTag-------------------------------------->請求服務器
第一: 網絡緩存優先考慮強制緩存,再考慮對比緩存
<1>--有緩存的情況下,首先判斷強制緩存中的數據的是否在有效期內。如果在有效期,則直接使用緩存。如果過了有效期,則進入對比緩存。(Date)
還需要強調一點,雖然緩存已經過期了,但是并非緩存與服務器的內容不同,比如服務端的數據并未做出任何更改,說明此時緩存的依舊是最新數據!所以還需要更詳細的判斷再來決定是否需要請求服務器更新數據,所以,避免了不必要的請求,這種緩存機制很大程度上減輕了服務器的壓力!
第二:對比緩存
<2>----在對比緩存過程中,判斷ETag 是否有變動如果服務端返回沒有變動,說明資源未改變,使用緩存。如果有變動,判斷Last-Modified。(Etag)
判斷Last-Modified,如果服務端對比資源的上次修改時間沒有變化,則使用緩存,否則重新請求服務端的數據,并作緩存工作(Last-modified)
結果處理緩存:最后如果服務器返回304,我們要直接使用緩存(304)HTTP_NO_MODIFIY
public static final int HTTP_NOT_MODIFIED = 304;
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 {
重點分析:面試官:okhttp是如何處理304的?
1).此時如果服務器返回的響應碼為HTTP_NOT_MODIFIED,也就是我們常見的304,代表服務器的資源沒有變化,客戶端去取本地緩存即可,此時服務器不會返回響應體
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構建response并返回。此時我們還需要做一件事,就是更新我們的緩存,將最終response寫入到cache對象中去.。通過okio。而OkHttp對Socket的讀寫操作使用的OkIo庫進行了一層封裝。
面試官 :okhttp為什么會使用okio而不是用普通io?
OkIO增強了流于流之間的互動,使得當數據從一個緩沖區移動到另一個緩沖區時,可以不經過copy能達到。
1. 在 Buffer 之間傳輸數據,建立在 Java IO, Java NIO 和 Socket 之上的,補充了java.io和java.nio的不足
2. 速度快
okio采用了segment機制進行內存共享,極大減少copy操作帶來的時間消耗,加快了讀寫速度
okio引入ByteString使其在byte[]與String之間轉換速度非常快(ByteString內部以兩種變量記錄了同個數據byte[] data; transient String utf8;),空間換時間
3 內存消耗小
segmentPool:通過一個鏈表環加上一個緩沖池來管理,okio的segement機制進行內存復用,上傳大文件時完全不用考慮OOM
雖然okio在byteString采用空間換時間,但是對內存也做極致優化,總體還是極大提高了性能
4. 穩定
okio提供了超時機制,不僅在IO操作上加上超時的判定,包括close,flush之類的方法中都有超時機制
總結:
內部所有的操作都要經過buffer緩沖區處理,而緩沖區內部管理細粒度更加細小的Segment,是通過一個鏈表環加上一個緩沖池來管理,這樣就能更大限度的使用內存,同時避免了過多的緩存對象生成。
http://www.lxweimin.com/p/5061860545ef(牛逼)
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
throws IOException {
// Some apps return a null body; for compatibility we treat that like a null cache request.
if (cacheRequest == null) return response;
Sink cacheBodyUnbuffered = cacheRequest.body();
if (cacheBodyUnbuffered == null) return response;
final BufferedSource source = response.body().source();
final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
Source cacheWritingSource = new Source() {
boolean cacheRequestClosed;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead;
try {
bytesRead = source.read(sink, byteCount);
} catch (IOException e) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheRequest.abort(); // Failed to write a complete cache response.
}
throw e;
}
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}
};
CacheStrategy是其中的緩存策略,CacheControl:CacheStrategy是里面的一個類
Cache類:里面包含了DiskLruCache(文件化的LRU 緩存類)。主要用于添加,移除,更新。類似conectionPool連接池!
回到OkHttp,CacheInterceptor攔截器處理的邏輯,其實就是上面所說的HTTP緩存邏輯,注意到OkHttp提供了一個現成的緩存類Cache,它采用DiskLruCache實現緩存策略,至于緩存的位置和大小,需要你自己指定。
自己新增攔截器,自行實現緩存的管理。
解決辦法:添加攔截器:
/**
* 有網時候的緩存
*/
final Interceptor NetCacheInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
int onlineCacheTime = 30;//在線的時候的緩存過期時間,如果想要不緩存,直接時間設置為0
return response.newBuilder()
.header("Cache-Control", "public, max-age="+onlineCacheTime)
.removeHeader("Pragma")
.build();
}
};
/**
* 沒有網時候的緩存
*/
final Interceptor OfflineCacheInterceptor = new Interceptor() {
@Override
4.3.4.ConnectInterceptor負責與服務器建立鏈接:很重要和服務器通信
我們發現目前為止我們還沒有進行真正的請求。別急,ConnectInterceptor就是一個負責建立http連接的攔截器
這里主要就是負責建立連接了,會建立TCP連接或者TLS連接,以及負責編碼解碼的HttpCodec。
1). 用來打開到服務端的連接。其實是調用了 StreamAllocation 的newStream 方法來打開連接的。建聯的 TCP 握手,TLS 握手都發生該階段。過了這個階段,和服務端的 socket 連接打通
2). 在ConnectInterceptor,也就是負責建立連接的攔截器中,首先會找可用連接,也就是從連接池中去獲取連接,具體的就是會調用到ConectionPool的get方法。
需要看下這個源碼:ConnectInterceptor的intercept()方法
4.3.5 CallServerInterceptor是攔截器鏈中最后一個攔截器,負責將網絡請求提交給服務器
它與服務器進行數據交換:主要的工作就是把請求的Request寫入到服務端,而后從服務端讀取Response。
(1)、寫入請求頭
(2)、寫入請求體
(3)、讀取響應頭
(4)、讀取響應體
用來發起請求并且得到響應。上一個階段已經握手成功,HttpStream 流已經打開,所以這個階段把 Request 的請求信息傳入流中,并且從流中讀取數據封裝成 Response 返回
面試官:okhttp是如何保證通信安全的?
面試官:看下okhttp如何封裝https
面試官:okhttp如何方法https里面怎么處理SSL?
RealConnection里面,connectTls方法:
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
// block for session establishment
SSLSession sslSocketSession = sslSocket.getSession();
Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
List<Certificate> peerCertificates = unverifiedHandshake.peerCertificates();
if (!peerCertificates.isEmpty()) {
X509Certificate cert = (X509Certificate) peerCertificates.get(0);
throw new SSLPeerUnverifiedException(
"Hostname " + address.url().host() + " not verified:"
+ "\n certificate: " + CertificatePinner.pin(cert)
+ "\n DN: " + cert.getSubjectDN().getName()
+ "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
} else {
throw new SSLPeerUnverifiedException(
"Hostname " + address.url().host() + " not verified (no certificates)");
}
}
// Check that the certificate pinner is satisfied by the certificates presented.
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
source = Okio.buffer(Okio.source(socket));
sink = Okio.buffer(Okio.sink(socket));
handshake = unverifiedHandshake;
protocol = maybeProtocol != null
? Protocol.get(maybeProtocol)
: Protocol.HTTP_1_1;
success = true;
} catch (AssertionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}
public final class Address {
final HttpUrl url;
final Dns dns;
final SocketFactory socketFactory;
final Authenticator proxyAuthenticator;
final List<Protocol> protocols;
final List<ConnectionSpec> connectionSpecs;
final ProxySelector proxySelector;
final @Nullable Proxy proxy;
final @Nullable SSLSocketFactory sslSocketFactory;
final @Nullable HostnameVerifier hostnameVerifier;
final @Nullable CertificatePinner certificatePinner;
簡單的說Okhttp就是抽象了下所有Tls,SSLSocket相關的代碼,然后通過一個Platform,根據當前使用環境的不同,去反射調用不同的實現類,然后這個抽象的類去調用Platform的實現類代碼,做到多平臺的兼容。
其中Tls當生成好SSLSocket之后,就會開始進行client say hello 和server say hello的操作了,這部分完全和https定義的一模一樣。Handshake則會把服務端支持的Tls版本,加密方式等都帶回來,然后會把這個沒有驗證過的HandShake用X509Certificate去驗證證書的有效性。然后會通過Platform去從SSLSocket去獲取ALPN的協議支持信息,當后端支持的協議內包含Http2.0時,則就會把請求升級到Http2.0階段。
1).客戶端默認信任全部證書
自定義X509TrustManager的形式實現來規避所有的證書檢測
然后將其放入okhttp的sslSocketFactory中。
private OkHttpClient getHttpsClient() {
OkHttpClient.Builder okhttpClient = new OkHttpClient().newBuilder();
//信任所有服務器地址
okhttpClient.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//設置為true
return true;
}
});
//創建管理器
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[] {};
}
} };
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
//為OkHttpClient設置sslSocketFactory
okhttpClient.sslSocketFactory(sslContext.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
return okhttpClient.build();
}
總結:在okhttpClient設置SSLsocketFactory。
在RealConnection的時候獲取SSLsocketFactory.然后進行TLS的訪問
會從很多常用的連接問題中自動恢復。如果您的服務器配置了多個IP地址,當第一個IP連接失敗的時候,OkHttp會自動嘗試下一個IP,此外OkHttp還處理了代理服務器問題和SSL握手失敗問題。
2).Okhttp驗證本地證書(certificate pinning),cer 和 pem 格式都可以
首先將下載的證書srca.cer放到工程的assets文件夾下。
然后讀證書得到流
最后設置sslSocketFactory
OkHttpClient httpClient = new OkHttpClient().newBuilder()
.sslSocketFactory(getSLLContext().getSocketFactory())
.build();
private SSLContext getSLLContext() {
SSLContext sslContext = null;
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
InputStream certificate = mContext.getAssets().open("gdroot-g2.crt");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
String certificateAlias = Integer.toString(0);
keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
sslContext = SSLContext.getInstance("TLS");
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
} catch (CertificateException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return sslContext;
}
5. okhttp的責任鏈模式
5.1 .責任鏈模式:(遞歸調用)循環調用,通過不停的調用自己。然后最后退出
面試官:責任鏈模式是怎么樣的?
責任鏈模式的類似情況如下:
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// 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 intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
每個攔截器負責一個特殊的職責.最后那個攔截器負責請求服務器,然后服務器返回了數據再根據這個攔截器的順序逆序返回回去,最終就得到了網絡數據.
責任鏈模式特點:一個對象持有下個對象的引用
請求:通過5個攔截器,把一個一個請求拼接起來
結果:最后一個先響應,先進后出的原理。
舉一個列子:出現彈框,然后一個個關閉。
最后通過 攔截器得到響應的結果respond
可以看到這里的攔截鏈使用的非常巧妙,有點像棧的數據結構。依次將各個攔截器的方法入棧,最后得到response,再依次彈棧。如果是我來寫的話,可能就直接一個for循環依次調用每個攔截器的攔截方法。但是這樣的話我還得再來一遍反循環,再來依次處理加工response。很明顯這里棧的結構更符合我們的業務場景。
面試官:為什么用責任鏈?
OkHttp的這種攔截器鏈采用的是責任鏈模式,這樣的好處是將請求的發送和處理分開,并且可以動態添加中間的處理方實現對請求的處理、短路等操作
不知你有沒有發現,這一過程 和 公司工作生產流程 很像:
老板接到一筆訂單,要求10天內生產100臺電腦。
總經理拿到任務后,修改了任務和時間:8天內生產110臺,這是基于 生產合格率 以及進行重工、檢驗、包裝、運輸的時間上的考量,既要保質保量,也要按時交貨。
任務接著到了部門經理,部門經理先去確認了倉庫中是否有足夠存貨,如果有就直接使用存貨來交貨,這樣不存在任何交貨風險(質量、時間);如果沒有存貨,那么就去要求生產線生產。
生產線按時按量生產完以后,會把生產情況 上報給部門經理,部門經理把結果總結成excel呈現給總經理,總經理則會把整個生產流程結果及各部門的配合情況,總結成PPT報告給老板。
6. 攔截器之連接攔截器(最重要的一個攔截器 )
7. okhttp的鏈接池復用
面試官:一個 TCP 連接可以對應幾個 HTTP 請求?
一個tcp連接的存活時間是大于Http請求的,所以一個Tcp可以對應多個Http請求。就是所謂的多路復用
面試官:為什么一個引用有多個鏈接?
因為同一個域名可以有多個鏈接。
復用發生在哪個階段?或者哪個攔截器的時候?
第四個,連接攔截器。
7.1 面試官:多路復用和連接池多路復用是一樣的么?
一個是socket復用,一個是nio復用
7. 2 面試官:okhttp是如何實現連接池復用的?
Socket:
OkHttp的底層是通過Java的Socket發送HTTP請求與接受響應的(這也好理解,HTTP就是基于TCP協議的),但是OkHttp實現了連接池的概念,
KeepAlive
當然大量的連接每次連接關閉都要三次握手四次分手的很顯然會造成性能低下,因此http有一種叫做keepalive connections的機制,
它可以在傳輸數據后仍然保持連接,當客戶端需要再次獲取數據時,直接使用剛剛空閑下來的連接而不需要再次握手
沒有復用 keep-alive會導致每次請求都需要重新進行 DNS解析,3次握手4次揮手操作,這樣是非常浪費性能的,
默認的是5個空閑TCP接連,并且活躍時間為5分鐘。Okhttp支持5個并發KeepAlive,默認鏈路生命為5分鐘(鏈路空閑后,保持存活的時間)。
面試官:OkHttp中ConnectionPool的實現原理。怎么判斷connection可復用?
即對于同一主機的多個請求,其實可以公用一個Socket連接,而不是每次發送完HTTP請求就關閉底層的Socket,這樣就實現了連接池的概念。
即連接池,用于管理http1.1/http2.0連接重用,以減少網絡延遲。相同Address的http請求可以共享一個連接,ConnectionPool就是實現了連接的復用。
總結來說: 同一端口和同一域名。socket復用。減少3次握手,4次揮手。連接池源碼分析。
連接攔截器使用了5種方法查找連接
- 首先會嘗試使用 已給請求分配的連接。(已分配連接的情況例如重定向時的再次請求,說明上次已經有了連接)
- 若沒有 已分配的可用連接,就嘗試從連接池中 匹配獲取。因為此時沒有路由信息,所以匹配條件:
address
一致——host
、port
、代理等一致,且匹配的連接可以接受新的請求。 -
若從連接池沒有獲取到,則傳入****
routes
****再次嘗試獲取(路由信息),這主要是針對****Http2.0
****的一個操作,Http2.0
可以復用square.com
與square.ca
的連接 - 若第二次也沒有獲取到,就創建
RealConnection
實例,進行TCP + TLS
握手,與服務端建立連接。 - 此時為了確保
Http2.0
連接的多路復用性,會第三次從連接池匹配。因為新建立的連接的握手過程是非線程安全的,所以此時可能連接池新存入了相同的連接。 - 第三次若匹配到,就使用已有連接,釋放剛剛新建的連接;若未匹配到,則把新連接存入連接池并返回。
優化思想:
是同一個域名下連接都可以復用,服務器和PC瀏覽器同一個域名下只能建立5個TCP 連接,為了讓同一個網頁中的圖片快速加載,所以要把圖片放到不同的域名下,這樣就可以實現>5個的連接請求。
源碼中同一域名下默認是5個TCP接連,超過后會等待(這個是分發器要求的5)
不同域名下最多64個請求,但是大部分時候同一個域名比較多
請求隊列RealConnection
private final Deque<RealConnection> connections = new ArrayDeque<>();
Deque<RealConnection>,雙向隊列,雙端隊列同時具有隊列和棧性質,雙端隊列中的元素可以從兩端彈出,插入和刪除操作限定在隊列的兩邊進行
普通隊列是限制級的一端進,另一端出的FIFO形式,棧是一端進出的LIFO形式,而雙端隊列就沒有這樣的限制級,也就是我們可以在隊列兩端進行插入或者刪除操作
數據結構:數組實現
它經常在緩存中被使用,里面維護了RealConnection也就是socket物理連接的包裝。
面試官:為什么是arrayDeque,這個隊列,有什么好處
說到這LinkedList表示不服,LinkedList同樣也實現了Deque接口,內部是用鏈表實現的雙端隊列,那為什么不用LinkedList呢?
實際上這與readyAsyncCalls向runningAsyncCalls轉換有關,當執行完一個請求或調用enqueue方法入隊新的請求時,會對readyAsyncCalls進行一次遍歷,將那些符合條件的等待請求轉移到runningAsyncCalls隊列中并交給線程池執行。盡管二者都能完成這項任務,
原因:但是由于鏈表的數據結構致使元素離散的分布在內存的各個位置,CPU緩存無法帶來太多的便利,
另外在垃圾回收時,使用數組結構的效率要優于鏈表。
作為隊列使用時由于 ArrayDeque 性能比 LinkedList 更快【速度會更快】
問題: 為什么 ArrayDeque 性能比 LinkedList 更快呢?
因為 ArrayDeque 是用循環數組來實現的,LinkedList 是用鏈表實現的,增刪改查的操作都比鏈表高(刪除操作比鏈表高嗎?如果是刪除數組中間的某個元素,不是的;如果是當成棧或隊列使用的場景下,是的。)。
具體的流程:
ConnectionPool封裝了一個RealConnectionPool。里面是主要實現,ConnectionPool只是入口而已
ConnectionPool提供對Deque<RealConnection>進行操作的方法分別為put、get、connectionBecameIdle、evictAll幾個操作。分別對應放入連接、獲取連接、移除連接、移除所有連接操作。
1). ConnectionPool內部以隊列方式存儲連接
2). 連接池最多維持5個連接,且每個鏈接最多活5分鐘
3). 每次添加鏈接的時候回執行一次清理任務,清理空閑的鏈接(RealConnection)。
4). 在ConnectionPool中維護了一個線程池,來進行回收和復用;connections是一個記錄連接的雙端隊列;routeDatabase是記錄路由失敗的線路,cleanupRunnable是用來進行自動回收連接的。
5). ConnectionPool: 連接池,類似于CachedThreadPool,需要注意的是這種線程池的工作隊列采用了沒有容量的SynchronousQueue
static {
executor = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));
}
7. 3 面試官:okhttp是如何實現清理無效的連接?
源碼在ConnectionPool:
# RealConnectionPool
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce(): Long = cleanup(System.nanoTime())
}
long cleanup(long now) {
int inUseConnectionCount = 0;//正在使用的連接數
int idleConnectionCount = 0;//空閑連接數
RealConnection longestIdleConnection = null;//空閑時間最長的連接
long longestIdleDurationNs = Long.MIN_VALUE;//最長的空閑時間
//遍歷連接:找到待清理的連接, 找到下一次要清理的時間(還未到最大空閑時間)
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//若連接正在使用,continue,正在使用連接數+1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//空閑連接數+1
idleConnectionCount++;
// 賦值最長的空閑時間和對應連接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//若最長的空閑時間大于5分鐘 或 空閑數 大于5,就移除并關閉這個連接
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// else,就返回 還剩多久到達5分鐘,然后wait這個時間再來清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//連接沒有空閑的,就5分鐘后再嘗試清理.
return keepAliveDurationNs;
} else {
// 沒有連接,不清理
cleanupRunning = false;
return -1;
}
}
//關閉移除的連接
closeQuietly(longestIdleConnection.socket());
//關閉移除后 立刻 進行下一次的 嘗試清理
return 0;
}
- 1). 在將連接加入連接池時就會啟動定時任務 (定時)* 2). 有空閑連接的話,如果最長的空閑時間大于5分鐘 或 空閑數 大于5,就移除關閉這個最長空閑連接;如果 空閑數 不大于5 且 最長的空閑時間不大于5分鐘,就返回到5分鐘的剩余時間,然后等待這個時間再來清理。* 3). 沒有空閑連接就等5分鐘后再嘗試清理。* 沒有連接不清理。
總結:清理閑置連接的核心主要是引用計數器List<Reference<StreamAllocation>> 和 選擇排序的算法以及excutor的清理線程池。
復用第一要素:ConnectionPool 要點(對鏈接隊列進行添加,移除操作)
復用第二要素:RealConnection 要點
復用第三要素:StreamAllocation 要點(計數對象 )
復用第四要素:Connection 要點, 里面包含:路由,連接,握手,協議
復用第五要素:RouteDatabase
8. okhttp的緩存機制
9. OKHttp框架中用到了哪些設計模式?
構建者模式:OkHttpClient與Request的構建都用到了構建者模式
外觀模式: OkHttp使用了外觀模式,將整個系統的復雜性給隱藏起來,將子系統接口通過一個客戶端OkHttpClient統一暴露出來。
責任鏈模式: OKHttp的核心就是責任鏈模式,通過5個默認攔截器構成的責任鏈完成請求的配置
享元模式: 享元模式的核心即池中復用,OKHttp復用TCP連接時用到了連接池,同時在異步請求中也用到了線程池
面試官:OkHttp中為什么使用構建者模式?
使用多個簡單的對象一步一步構建成一個復雜的對象;
優點: 當內部數據過于復雜的時候,可以非常方便的構建出我們想要的對象,并且不是所有的參數我們都需要進行傳遞;
缺點: 代碼會有冗余
好封裝
面試官:OkHttp中模板方法設計模式是怎樣的?
10. OKHttp框架中的相關問題
面試官:okhttp,在訪問一個界面沒有結束,關閉activity。還是會,這個要驗證一下,那么下一次進去重新訪問接口還是--------
網絡請求如何取消?底層是怎么實現的
還是會執行
面試官:多域名如何封裝?測試和正式如何封裝?
面試官:你是怎么封裝okhttp的?
https://blog.csdn.net/weimingjue/article/details/88528373
1).證書
2). 請求頭,公共參數
3).請求體,傳入的參數
4).錯誤碼的封裝
5).請求日志監控---攔截器
6).線程切換,響應之后的回調
7). 數據解析