《億級Android架構》小專欄文章列表:java
《Android 架構之網絡鏈接與加速》github
《Android 架構之秒級移動配置中心》markdown
私覺得,閱讀開源項目是與世界級技術大牛直接對話的最好方式。 這次來分享下 OkHttp 源碼的分析。cookie
在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的雛形,只具有最基本的功能。
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;
...
}
}
複製代碼
Request
對象傳遞給WingjayHttpClient
咱們能夠設計WingjayHttpClient
以下:
class WingjayHttpClient { public Response sendRequest(Request request) { return executeRequest(request); } } 複製代碼
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); 複製代碼
然而,上面的雛形是遠遠不能勝任常規的應用需求的,所以,下面再來對它添加一些經常使用的功能模塊。
通常的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()); } ... } 複製代碼
有時咱們請求的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:服務器端錯誤,服務器未能實現合法的請求
所謂重試,和重定向很是相似,即經過判斷Response
狀態,若是鏈接服務器失敗等,那麼能夠嘗試獲取一個新的路徑進行從新鏈接,大體的實現和重定向很是相似,此不贅述。
這是很是核心的部分。
經過上面的從新組裝request
和重定向機制,咱們能夠感覺的,一個request
從user建立出來後,會通過層層處理後,才真正發出去,而一個response
,也會通過各類處理,最終返回給用戶。
筆者認爲這和網絡協議棧很是類似,用戶在應用層發出簡單的數據,而後通過傳輸層、網絡層等,層層封裝後真正把請求從物理層發出去,當請求結果回來後又層層解析,最終把最直接的結果返回給用戶使用。
最重要的是,每一層都是抽象的,互不相關的!
所以在咱們設計時,也能夠借鑑這個思想,經過設置攔截器Interceptor
,每一個攔截器會作兩件事情:
那麼,咱們能夠爲攔截器定義一個抽象接口,而後去實現具體的攔截器。
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。具體緣由能夠看上面的圖。
這是很是核心的部分。
經過上面的工做,咱們修改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架構》小專欄會基於做者實際工做經驗,結合國內大廠如阿里、騰訊、美團等基礎架構現狀,嘗試談談如何設計一套好的架構來支持業務從0到1,甚至到億,但願與你們多多探討。
本專欄主要內容:
《億級Android架構》小專欄文章列表:
若是有問題歡迎聯繫我,或者關注個人公衆號:wingjay。
謝謝。