[搞定開源] 第一篇 okhttp 3.10原理
okhttp是Android攻城獅必須掌握的網路庫,很多其他開源庫用到也是它,第一篇介紹okhttp原理最合適不過。
review keyword:請求分發、責任鏈
okhttp的使用
作為網路庫,okhttp的基本功能就是發出request請求,得到response響應。OkHttpClient的構造方法,三種形式,使用構造者模式,裡面有幾十個引數,重要引數後面逐漸會講到。
OkHttpClient okHttpClient1 = new OkHttpClient();
OkHttpClient okHttpClient2 = new OkHttpClient.Builder().build();
OkHttpClient okHttpClient3 = okHttpClient2.newBuilder().build();
同步請求和非同步請求的例子:
private static void sync(OkHttpClient client, String url) {
Request request = new Request.Builder().url(url).build();
Response response = null;
try {
response = client.newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
}
try {
System.out.print(response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
private static void async(OkHttpClient client, String url) {
Request request = new Request.Builder().url(url).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
System.out.println("onFailure");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println("onResponse:" + response.body().string());
}
});
}
使用很簡單不多說,速度進入原始碼,先放張okhttp結構圖,涉及到的重要類都在上面了。
請求封裝Call
請求request需要封裝到Call物件,呼叫的是OkHttpClinet的newCall方法。Call定義為介面,具體實現類區分同步和非同步:
- 同步請求:RealCall
- 非同步請求:AsyncCall
非同步請求需要線上程池裡執行,所以AsyncCall繼承了Runnable。Call提供了cancel方法,所以網路請求是可以中止的。
封裝request到Call後,就交由Dispatcher執行請求分發。
請求分發Dispatcher
同步請求直接執行,非同步請求提交到執行緒池裡執行,Dispatcher裡執行緒池的構建引數如下:
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;
}
- 執行緒數量範圍:0到無窮大;
- 空閒執行緒的回收時間是60秒;
- 阻塞佇列是SynchronousQueue,不存放元素;
- 自定義執行緒工廠:名稱直接硬編碼,非守護。
沒有新鮮的,執行緒池引數應該要爛熟於胸。
Dispatcher裡有三個Deque,存放什麼名稱寫得很清楚。
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
- readyAsyncCalls:待執行的非同步請求
- runningAsyncCalls:正在執行的非同步請求
- runningSyncCalls:正在執行的同步請求
同步請求呼叫execute方法,非同步請求呼叫enqueue方法,本質就是將call放入對應的佇列。
同步請求
@Override
public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
RealCall的execute方法,裡面是一些狀態判斷和監聽,最重要的是呼叫Dispatcher的executed:
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
很簡單地,將Call加入runningSyncCalls。每個OkHttpClient物件只建立一個Dispatcher,所以操作佇列時,需要同步。
具體的執行過程呼叫getResponseWithInterceptorChain,後文很快會說。當Call執行完成得到response時,在finally裡呼叫Dispatcher的finished。
非同步請求
@Override
public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
非同步請求的enqueue,呼叫Dispatcher同名的enqueue,這時候傳入的Call為AsyncCall。
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
到達Dispatcher進行分發,enqueue也是操作Call入隊。請求的數量有限制(maxRequests=64 && maxRequestsPerHost=5),範圍內加入runningAsyncCalls並提交執行緒池執行;否則加入readyAsyncCalls等待。
@Override
protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
//省略callback處理部分
} catch (IOException e) {
//
} finally {
client.dispatcher().finished(this);
}
}
具體執行非同步請求在execute方法,callback裡的onResponse和onFailure很簡單,程式碼略掉。execute核心同樣呼叫getResponseWithInterceptorChain得到response,最後也是呼叫Dispatcher的finished。
請求完成後
請求執行完成的善後是Dispatcher.finished,功能是將完成的Call移出佇列。
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();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
如果是非同步請求,需要增加呼叫promoteCalls,看看readyAsyncCalls裡的Call能不能放入runningAsyncCalls參與執行。這裡用synchronized鎖住Dispatcher,和“生產者-消費者”有點像。(回憶wait/notify的寫法)
看到個擴充套件點,當Dispatcher裡沒有Call可執行時,可以設定一個idleCallback跑一些東西。
攔截器Interceptor
重頭戲是方法getResponseWithInterceptorChain。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
getResponseWithInterceptorChain首先組建攔截器列表,包括okhttp自帶的攔截器還有使用者自定義的攔截器。這是責任鏈設計模式,每個request都需要按序通過攔截器,最終發出到伺服器得到response,再反序依次通過攔截器。
- RetryAndFollowUpInterceptor
- BridgeInterceptor
- CacheInterceptor
- ConnectInterceptor
- CallServerInterceptor
okhttp的核心實現就是這幾個攔截器,後面會逐個分析它們的功能。
補充說明自定義攔截器,有兩種選擇,application interceptors和network interceptors,區別在wiki寫得很清楚,圖直接搬過來。
okhttp支援WebSocket,在程式碼裡看到如果是WebSocket,則不支援network interceptors。
WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——允許伺服器主動傳送資訊給客戶端。
定義完攔截器後,由RealInterceptorChain將攔截器串聯,呼叫proceed方法,傳入originalRequest。
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
//...
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
//...
return response;
}
proceed的核心是建立下一個RealInterceptorChain,並傳入index+1,表示獲取下一個攔截器,然後執行當前攔截器的intercept,最後返回response。
不得不說,攔截器的設計非常美,每一層都各司其職,互不相干,但又配合著處理request和response,最終完成http整個流程。
連線和流
RealInterceptorChain的proceed有四個入參:
Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,RealConnection connection
RealConnection封裝了Socket,是請求傳送的通道;HttpCodec描述了request和response的輸入輸出,用的是okio;StreamAllocation是管理連線和流的橋樑。為了複用連線,okhttp使用ConnectionPool對連線進行管理。
對上面幾個類的介紹,另外放在okhttp 3.10連線複用原理。
RetryAndFollowUpInterceptor
真的開始跟著請求過攔截器了。RetryAndFollowUpInterceptor是請求通過的第一個自帶攔截器,負責處理請求失敗的重試和伺服器的重定向。
intercept的程式碼比較長,我們分開幾部分來看。
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
第一步就是建立StreamAllocation。
while(true){
//1、請求是否取消
//2、request交給下一個攔截器,得到response
//3、是否重定向
}
然後進入無限迴圈,邏輯很明確,檢查response是否是重定向,是的話一直迴圈請求新url。當然,重定向次數有限制,最大到MAX_FOLLOW_UPS=20。
mark1
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
首先判斷canceled狀態,哪裡改變狀態的呢?回看RealCall的cancel。
@Override
public void cancel() {
retryAndFollowUpInterceptor.cancel();
}
只有一句話,執行retryAndFollowUpInterceptor的cancel。
public void cancel() {
canceled = true;
StreamAllocation streamAllocation = this.streamAllocation;
if (streamAllocation != null) streamAllocation.cancel();
}
設定當前canceled=true,停止retryAndFollowUpInterceptor的無限迴圈,同時呼叫StreamAllocation的cancel,裡面繼續呼叫連線和流的cancel,將能停的東西都停了。
mark2
Response response;
boolean releaseConnection = true;
try {
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// We’re throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
這部分將請求傳遞到下一個攔截器,並捕獲處理各種網路請求異常。失敗的原因很多,裡面的異常處理呼叫recover方法判斷是否能夠重試。recover裡檢查配置、協議、exception型別。只要能重試,會保留連線。
mark3
//...
Request followUp = followUpRequest(response, streamAllocation.route());
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
//...
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(followUp.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
priorResponse = response;
這部分最重要的是呼叫followUpRequest檢查是否重定向,followUpRequest裡是swtich-case一系列http狀態碼,對我們學習各個狀態碼的處理是不可多得的資料。
最後還需要判斷重定向的目標是否sameConnection,不是的話需要重新建立StreamAllocation。
BridgeInterceptor
使用request時,使用者一般只會傳入method和url。http header才不止這麼少引數,填充預設引數、處理cookie、gzip等是BridgeInterceptor的工作。
CacheInterceptor
okhttp對http的快取策略全部在CacheInterceptor中完成,另見okhttp 3.10快取原理。request到達CacheInterceptor時,如果快取命中,直接返回快取response。當下層response返回到CacheInterceptor時,它可以將結果快取起來。
ConnectInterceptor
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
ConnectInterceptor的程式碼很短,核心功能是獲取能用的流和連線,也就是HttpCodec和RealConnection,streamAllocation.newStream對應這兩步:
public HttpCodec newStream(...) {
//呼叫findHealthyConnection得到RealConnection
//呼叫RealConnection.newCodec得到HttpCodec
}
private RealConnection findHealthyConnection(...) throws IOException {
while (true) {
//呼叫findConnection獲取RealConnection
}
}
findHealthyConnection,顧名思義找到一個能用的連線,裡面是個無限迴圈呼叫findConnection,直到獲得RealConnection。
連線可能是複用舊有的,也可能是新建立的,findConnection方法比較長,分析下來就是對應這兩步:
- 從ConnectionPool中取出連線複用;
- 建立新連線,放回ConnectionPool管理。
連線過程是tcp握手、ssl握手等協議過程,不細說。
得到連線後,就可以建立流。在RealCollection中,newCodec在當前連線上建立一個流,http1使用了okio的source和sink讀寫資料。
private BufferedSource source;
private BufferedSink sink;
public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
StreamAllocation streamAllocation) throws SocketException {
if (http2Connection != null) {
return new Http2Codec(client, chain, streamAllocation, http2Connection);
} else {
socket.setSoTimeout(chain.readTimeoutMillis());
source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
return new Http1Codec(client, streamAllocation, source, sink);
}
}
至此,請求傳送需要的連線和流準備完畢。
CallServerInterceptor
最後執行的攔截器CallServerInterceptor,核心是通過okio在socket上寫入request,讀取response。
後記
okhttp是大師的作品,贊。
我和大師的距離就差那麼一點,就像考北大差那麼一點分(100分),哭笑。
相關文章
- OkHttp 開源庫使用與原始碼解析HTTP原始碼
- OKHttp網路請求原理流程解析HTTP
- OkHttp原理解析1(框架流程篇)HTTP框架
- 抽絲剝繭okhttp(五)Interceptors原理HTTP
- 常用輪子之Okhttp基本使用及原理HTTP
- 3.10
- 軟體安裝程式第一篇(原理)
- 開源公開課丨 ChengYing 安裝原理剖析
- 開源框架TLog核心原理架構解析框架架構
- rk3568硬體開發筆記(第一篇) 電源設計筆記
- 徹底理解OkHttp - OkHttp 原始碼解析及OkHttp的設計思想HTTP原始碼
- 開源網格VPN meshboi及其背後原理
- 最通俗易懂搞定HashMap的底層原理HashMap
- Python 3.10 新增功能Python
- Handtrack.js 開源:3行JS程式碼搞定手部動作跟蹤JS
- 寫簡歷沒模板?別怕,這些開源專案幫你搞定!
- OkHttp解析HTTP
- QGIS3.10配置python外掛開發環境S3Python開發環境
- 《流程引擎原理與實踐》開源電子書
- 人人都能搞定的大模型原理 - 神經網路大模型神經網路
- Android 網路優化,使用 HTTPDNS 優化 DNS,從原理到 OkHttp 整合Android優化httpdDNS
- okhttp與nettyHTTPNetty
- OkHttp Interceptors(四)HTTP
- 手撕OkHttpHTTP
- OkHttp的概述HTTP
- 我沒有前端經驗,但1天就搞定了開源專案主頁前端
- 開源|ns4_chatbot通訊元件的工作原理元件
- 一文搞定HashMap的實現原理和麵試HashMap
- OkHttp 知識梳理(4) - OkHttp 之快取原始碼解析HTTP快取原始碼
- 開源實時資料處理系統Pulsar:一套搞定Kafka+Flink+DBKafka
- 利用Conda嚐鮮Python 3.10Python
- MVP基本原理,以及幾款開源專案分析MVP
- 搞定 Redis 資料儲存原理,別隻會 set、get 了Redis
- OkHttp簡單剖析HTTP
- OkHttp原始碼分析HTTP原始碼
- OKHttp 官方文件【一】HTTP
- OKHttp 官方文件【二】HTTP
- 搜程式碼費時又費力?這裡有一個開源神器幫你快速搞定!