《億級Android架構》小專欄文章列表:
正文
私以為,閱讀開源專案是與世界級技術大牛直接對話的最好方式。 此次來分享下 OkHttp 原始碼的分析。
一、開源專案 OkHttp
在Android、Java開發領域中,相信大家都聽過或者在使用Square家大名鼎鼎的網路請求庫:OkHttp ,當前多數著名的開源專案如 Fresco、Glide、 Picasso、 Retrofit都在使用OkHttp,這足以說明其質量,而且該專案仍處在不斷維護中。
二、問題
在分析okhttp原始碼之前,我想先提出一個問題,如果我們自己來設計一個網路請求庫,這個庫應該長什麼樣子?大致是什麼結構呢?
下面我和大家一起來構建一個網路請求庫,並在其中融入okhttp中核心的設計思想,希望藉此讓讀者感受並學習到okhttp中的精華之處,而非僅限於瞭解其實現。
筆者相信,如果你能耐心閱讀完本篇,不僅能對http協議有進一步理解,更能夠學習到世界級專案的思維精華,提高自身思維方式。
三、思考
首先,我們假設要構建的的網路請求庫叫做WingjayHttpClient
,那麼,作為一個網路請求庫,它最基本功能是什麼呢?
在我看來應該是:接收使用者的請求 -> 發出請求 -> 接收響應結果並返回給使用者。
那麼從使用者角度而言,需要做的事是:
- 建立一個
Request
:在裡面設定好目標URL;請求method如GET/POST等;一些header如Host、User-Agent等;如果你在POST上傳一個表單,那麼還需要body。 - 將建立好的
Request
傳遞給WingjayHttpClient
。 WingjayHttpClient
去執行Request
,並把返回結果封裝成一個Response
給使用者。而一個Response
裡應該包括statusCode如200,一些header如content-type等,可能還有body
到此即為一次完整請求的雛形。那麼下面我們來具體實現這三步。
四、雛形實現
下面我們先來實現一個httpClient的雛形,只具備最基本的功能。
1. 建立Request
類
首先,我們要建立一個Request
類,利用Request
類使用者可以把自己需要的引數傳入進去,基本形式如下:
class Request {
String url;
String method;
Headers headers;
Body requestBody;
public Request(String url, String method, @Nullable Headers headers, @Nullable Body body) {
this.url = url;
...
}
}
複製程式碼
2. 將Request
物件傳遞給WingjayHttpClient
我們可以設計WingjayHttpClient
如下:
class WingjayHttpClient {
public Response sendRequest(Request request) {
return executeRequest(request);
}
}
複製程式碼
3. 執行Request
,並把返回結果封裝成一個Response
返回
class WingjayHttpClient {
...
private Response executeRequest(Request request) {
//使用socket來進行訪問
Socket socket = new Socket(request.getUrl(), 80);
ResponseData data = socket.connect().getResponseData();
return new Response(data);
}
...
}
class Response {
int statusCode;
Headers headers;
Body responseBody
...
}
複製程式碼
五、功能擴充套件
利用上面的雛形,可以得到其使用方法如下:
Request request = new Request("http://wingjay.com");
WingjayHttpClient client = new WingjayHttpClient();
Response response = client.sendRequest(request);
handle(response);
複製程式碼
然而,上面的雛形是遠遠不能勝任常規的應用需求的,因此,下面再來對它新增一些常用的功能模組。
1. 重新把簡陋的user Request組裝成一個規範的http request
一般的request中,往往使用者只會指定一個URL和method,這個簡單的user request是不足以成為一個http request,我們還需要為它新增一些header,如Content-Length, Transfer-Encoding, User-Agent, Host, Connection, 和 Content-Type,如果這個request使用了cookie,那我們還要將cookie新增到這個request中。
我們可以擴充套件上面的sendRequest(request)
方法:
[class WingjayHttpClient]
public Response sendRequest(Request userRequest) {
Request httpRequest = expandHeaders(userRequest);
return executeRequest(httpRequest);
}
private Request expandHeaders(Request userRequest) {
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
...
}
複製程式碼
2. 支援自動重定向
有時我們請求的URL已經被移走了,此時server會返回301狀態碼和一個重定向的新URL,此時我們要能夠支援自動訪問新URL而不是向使用者報錯。
對於重定向這裡有一個測試性URL:www.publicobject.com/helloworld.… ,通過訪問並抓包,可以看到如下資訊:
因此,我們在接收到Response後要根據status_code是否為重定向,如果是,則要從Response Header裡解析出新的URL-Location
並自動請求新URL。那麼,我們可以繼續改寫sendRequest(request)
方法:
[class WingjayHttpClient]
private boolean allowRedirect = true;
// user can set redirect status when building WingjayHttpClient
public void setAllowRedirect(boolean allowRedirect) {
this.allowRedirect = allowRedirect;
}
public Response sendRequest(Request userRequest) {
Request httpRequest = expandHeaders(userRequest);
Response response = executeRequest(httpRequest);
switch (response.statusCode()) {
// 300: multi choice; 301: moven permanently;
// 302: moved temporarily; 303: see other;
// 307: redirect temporarily; 308: redirect permanently
case 300:
case 301:
case 302:
case 303:
case 307:
case 308:
return handleRedirect(response);
default:
return response;
}
}
// the max times of followup request
private static final int MAX_FOLLOW_UPS = 20;
private int followupCount = 0;
private Response handleRedirect(Response response) {
// Does the WingjayHttpClient allow redirect?
if (!client.allowRedirect()) {
return null;
}
// Get the redirecting url
String nextUrl = response.header("Location");
// Construct a redirecting request
Request followup = new Request(nextUrl);
// check the max followupCount
if (++followupCount > MAX_FOLLOW_UPS) {
throw new Exception("Too many follow-up requests: " + followUpCount);
}
// not reach the max followup times, send followup request then.
return sendRequest(followup);
}
複製程式碼
利用上面的程式碼,我們通過獲取原始userRequest
的返回結果,判斷結果是否為重定向,並做出自動followup處理。
一些常用的狀態碼 100~199:指示資訊,表示請求已接收,繼續處理 200~299:請求成功,表示請求已被成功接收、理解、接受 300~399:重定向,要完成請求必須進行更進一步的操作 400~499:客戶端錯誤,請求有語法錯誤或請求無法實現 500~599:伺服器端錯誤,伺服器未能實現合法的請求
3. 支援重試機制
所謂重試,和重定向非常類似,即通過判斷Response
狀態,如果連線伺服器失敗等,那麼可以嘗試獲取一個新的路徑進行重新連線,大致的實現和重定向非常類似,此不贅述。
4. Request & Response 攔截機制
這是非常核心的部分。
通過上面的重新組裝request
和重定向機制,我們可以感受的,一個request
從user建立出來後,會經過層層處理後,才真正發出去,而一個response
,也會經過各種處理,最終返回給使用者。
筆者認為這和網路協議棧非常相似,使用者在應用層發出簡單的資料,然後經過傳輸層、網路層等,層層封裝後真正把請求從物理層發出去,當請求結果回來後又層層解析,最終把最直接的結果返回給使用者使用。
最重要的是,每一層都是抽象的,互不相關的!
因此在我們設計時,也可以借鑑這個思想,通過設定攔截器Interceptor
,每個攔截器會做兩件事情:
- 接收上一層攔截器封裝後的request,然後自身對這個request進行處理,例如新增一些header,處理後向下傳遞;
- 接收下一層攔截器傳遞回來的response,然後自身對response進行處理,例如判斷返回的statusCode,然後進一步處理。
那麼,我們可以為攔截器定義一個抽象介面,然後去實現具體的攔截器。
interface Interceptor {
Response intercept(Request request);
}
複製程式碼
大家可以看下上面這個攔截器設計是否有問題?
我們想象這個攔截器能夠接收一個request,進行攔截處理,並返回結果。
但實際上,它無法返回結果,而且它在處理request後,並不能繼續向下傳遞,因為它並不知道下一個Interceptor
在哪裡,也就無法繼續向下傳遞。
那麼,如何解決才能把所有Interceptor
串在一起,並能夠依次傳遞下去。
public interface Interceptor {
Response intercept(Chain chain);
interface Chain {
Request request();
Response proceed(Request request);
}
}
複製程式碼
使用方法如下:假如我們現在有三個Interceptor
需要依次攔截:
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(new MyInterceptor1());
interceptors.add(new MyInterceptor2());
interceptors.add(new MyInterceptor3());
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, 0, originalRequest);
chain.proceed(originalRequest);
複製程式碼
裡面的RealInterceptorChain
的基本思想是:我們把所有interceptors
傳進去,然後chain
去依次把request
傳入到每一個interceptors
進行攔截即可。
通過下面的示意圖可以明確看出攔截流程:
其中,RetryAndFollowupInterceptor
是用來做自動重試和自動重定向的攔截器;BridgeInterceptor
是用來擴充套件request
的header
的攔截器。這兩個攔截器存在於okhttp
裡,實際上在okhttp
裡還有好幾個攔截器,這裡暫時不做深入分析。
-
CacheInterceptor
這是用來攔截請求並提供快取的,當request進入這一層,它會自動去檢查快取,如果有,就直接返回快取結果;否則的話才將request繼續向下傳遞。而且,當下層把response返回到這一層,它會根據需求進行快取處理; -
ConnectInterceptor
這一層是用來與目標伺服器建立連線 -
CallServerInterceptor
這一層位於最底層,直接向伺服器發出請求,並接收伺服器返回的response,並向上層層傳遞。
上面幾個都是okhttp自帶的,也就是說需要在WingjayHttpClient
自己實現的。除了這幾個功能性的攔截器,我們還要支援使用者自定義攔截器
,主要有以下兩種(見圖中非虛線框藍色字部分):
-
interceptors
這裡的攔截器是攔截使用者最原始的request。 -
NetworkInterceptor
這是最底層的request攔截器。
如何區分這兩個呢?舉個例子,我建立兩個LoggingInterceptor
,分別放在interceptors
層和NetworkInterceptor
層,然後訪問一個會重定向的URL_1
,當訪問完URL_1
後會再去訪問重定向後的新地址URL_2
。對於這個過程,interceptors
層的攔截器只會攔截到URL_1
的request,而在NetworkInterceptor
層的攔截器則會同時攔截到URL_1
和URL_2
兩個request。具體原因可以看上面的圖。
5. 同步、非同步 Request池管理機制
這是非常核心的部分。
通過上面的工作,我們修改WingjayHttpClient
後得到了下面的樣子:
class WingjayHttpClient {
public Response sendRequest(Request userRequest) {
Request httpRequest = expandHeaders(userRequest);
Response response = executeRequest(httpRequest);
switch (response.statusCode()) {
// 300: multi choice; 301: moven permanently;
// 302: moved temporarily; 303: see other;
// 307: redirect temporarily; 308: redirect permanently
case 300:
case 301:
case 302:
case 303:
case 307:
case 308:
return handleRedirect(response);
default:
return response;
}
}
private Request expandHeaders(Request userRequest) {...}
private Response executeRequest(Request httpRequest) {...}
private Response handleRedirect(Response response) {...}
}
複製程式碼
也就是說,WingjayHttpClient
現在能夠同步
地處理單個Request
了。
然而,在實際應用中,一個WingjayHttpClient
可能會被用於同時處理幾十個使用者request,而且這些request裡還分成了同步
和非同步
兩種不同的請求方式,所以我們顯然不能簡單把一個request直接塞給WingjayHttpClient
。
我們知道,一個request除了上面定義的http協議相關的內容,還應該要設定其處理方式同步
和非同步
。那這些資訊應該存在哪裡呢?兩種選擇:
-
直接放入
Request
從理論上來講是可以的,但是卻違背了初衷。我們最開始是希望用Request
來構造符合http協議的一個請求,裡面應該包含的是請求目標網址URL,請求埠,請求方法等等資訊,而http協議是不關心這個request是同步還是非同步之類的資訊 -
建立一個類,專門來管理
Request
的狀態 這是更為合適的,我們可以更好的拆分職責。
因此,這裡選擇建立兩個類SyncCall
和AsyncCall
,用來區分同步
和非同步
。
class SyncCall {
private Request userRequest;
public SyncCall(Request userRequest) {
this.userRequest = userRequest;
}
}
class AsyncCall {
private Request userRequest;
private Callback callback;
public AsyncCall(Request userRequest, Callback callback) {
this.userRequest = userRequest;
this.callback = callback;
}
interface Callback {
void onFailure(Call call, IOException e);
void onResponse(Call call, Response response) throws IOException;
}
}
複製程式碼
基於上面兩個類,我們的使用場景如下:
WingjayHttpClient client = new WingjayHttpClient();
// Sync
Request syncRequest = new Request("http://wingjay.com");
SyncCall syncCall = new SyncCall(request);
Response response = client.sendSyncCall(syncCall);
handle(response);
// Async
AsyncCall asyncCall = new AsyncCall(request, new CallBack() {
@Override
public void onFailure(Call call, IOException e) {}
@Override
public void onResponse(Call call, Response response) throws IOException {
handle(response);
}
});
client.equeueAsyncCall(asyncCall);
複製程式碼
從上面的程式碼可以看到,WingjayHttpClient
的職責發生了變化:以前是response = client.sendRequest(request);,而現在變成了
response = client.sendSyncCall(syncCall);
client.equeueAsyncCall(asyncCall);
複製程式碼
那麼,我們也需要對WingjayHttpClient
進行改造,基本思路是在內部新增請求池
來對所有request進行管理。那麼這個請求池
我們怎麼來設計呢?有兩個方法:
-
直接在
WingjayHttpClient
內部建立幾個容器 同樣,從理論上而言是可行的。當使用者把(a)syncCall傳給client後,client自動把call存入對應的容器進行管理。 -
建立一個獨立的類進行管理 顯然這樣可以更好的分配職責。我們把
WingjayHttpClient
的職責定義為,接收一個call,內部進行處理後返回結果。這就是WingjayHttpClient
的任務,那麼具體如何去管理這些request的執行順序和生命週期,自然不需要由它來管。
因此,我們建立一個新的類:Dispatcher
,這個類的作用是:
- 儲存外界不斷傳入的
SyncCall
和AsyncCall
,如果使用者想取消則可以遍歷所有的call進行cancel操作; - 對於
SyncCall
,由於它是即時執行的,因此Dispatcher
只需要在SyncCall
執行前儲存進來,在執行結束後移除即可; - 對於
AsyncCall
,Dispatcher
首先啟動一個ExecutorService,不斷取出AsyncCall
去進行執行,然後,我們設定最多執行的request數量為64,如果已經有64個request在執行中,那麼就將這個asyncCall存入等待區。
根據設計可以得到Dispatcher
構造:
class Dispatcher {
// sync call
private final Deque<SyncCall> runningSyncCalls = new ArrayDeque<>();
// async call
private int maxRequests = 64;
private final Deque<AsyncCall> waitingAsyncCalls = new ArrayDeque<>();
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
private ExecutorService executorService;
// begin execute Sync call
public void startSyncCall(SyncCall syncCall) {
runningSyncCalls.add(syncCall);
}
// finish Sync call
public void finishSyncCall(SyncCall syncCall) {
runningSyncCalls.remove(syncCall);
}
// enqueue a new AsyncCall
public void enqueue(AsyncCall asyncCall) {
if (runningAsyncCalls.size() < 64) {
// run directly
runningAsyncCalls.add(asyncCall);
executorService.execute(asyncCall);
} else {
readyAsyncCalls.add(asyncCall);
}
}
// finish a AsyncCall
public void finishAsyncCall(AsyncCall asyncCall) {
runningAsyncCalls.remove(asyncCall);
}
}
複製程式碼
有了這個Dispatcher
,那我們就可以去修改WingjayHttpClient
以實現
response = client.sendSyncCall(syncCall);
client.equeueAsyncCall(asyncCall);
複製程式碼
這兩個方法了。具體實現如下
[class WingjayHttpClient]
private Dispatcher dispatcher;
public Response sendSyncCall(SyncCall syncCall) {
try {
// store syncCall into dispatcher;
dispatcher.startSyncCall(syncCall);
// execute
return sendRequest(syncCall.getRequest());
} finally {
// remove syncCall from dispatcher
dispatcher.finishSyncCall(syncCall);
}
}
public void equeueAsyncCall(AsyncCall asyncCall) {
// store asyncCall into dispatcher;
dispatcher.enqueue(asyncCall);
// it will be removed when this asyncCall be executed
}
複製程式碼
基於以上,我們能夠很好的處理同步
和非同步
兩種請求,使用場景如下:
WingjayHttpClient client = new WingjayHttpClient();
// Sync
Request syncRequest = new Request("http://wingjay.com");
SyncCall syncCall = new SyncCall(request);
Response response = client.sendSyncCall(syncCall);
handle(response);
// Async
AsyncCall asyncCall = new AsyncCall(request, new CallBack() {
@Override
public void onFailure(Call call, IOException e) {}
@Override
public void onResponse(Call call, Response response) throws IOException {
handle(response);
}
});
client.equeueAsyncCall(asyncCall);
複製程式碼
六、總結
到此,我們基本把okhttp
裡核心的機制都講解了一遍,相信讀者對於okhttp的整體結構和核心機制都有了較為詳細的瞭解。
《億級Android架構》小專欄介紹
業務的快速增長離不開穩定可靠的架構。《億級Android架構》小專欄會基於作者實際工作經驗,結合國內大廠如阿里、騰訊、美團等基礎架構現狀,嘗試談談如何設計一套好的架構來支援業務從0到1,甚至到億,希望與大家多多探討。
本專欄主要內容:
- 當前大廠有哪些Android架構;
- 這些架構能解決什麼問題;
- 這些架構的原理是什麼;
- 學習這些架構對我們自身的意義。
《億級Android架構》小專欄文章列表:
如果有問題歡迎聯絡我,或者關注我的公眾號:wingjay。
謝謝。