帶你學開源項目:OkHttp--自己動手實現okhttp

本文轉載於:帶你學開源項目:OkHttp--自己動手實現okhttp

一、開源項目 OkHttp

在Android、Java開發領域中,相信大家都聽過或者在使用Square家大名鼎鼎的網絡請求庫——OkHttp——https://github.com/square/okhttp ,當前多數著名的開源項目如 FrescoGlide、 Picasso、 Retrofit都在使用OkHttp,這足以說明其質量,而且該項目仍處在不斷維護中

二、問題

在分析okhttp源碼之前,我想先提出一個問題,如果我們自己來設計一個網絡請求庫,這個庫應該長什麼樣子?大致是什麼結構呢?

下面我和大家一起來構建一個網絡請求庫,並在其中融入okhttp中核心的設計思想,希望藉此讓讀者感受並學習到okhttp中的精華之處,而非僅限於瞭解其實現。

筆者相信,如果你能耐心閱讀完本篇,不僅能對http協議有進一步理解,更能夠學習到世界級項目的思維精華,提高自身思維方式。

三、思考

首先,我們假設要構建的的網絡請求庫叫做WingjayHttpClient,那麼,作爲一個網絡請求庫,它最基本功能是什麼呢?

在我看來應該是:接收用戶的請求 -> 發出請求 -> 接收響應結果並返回給用戶。

那麼從使用者角度而言,需要做的事是:

  1. 創建一個Request:在裏面設置好目標URL;請求method如GET/POST等;一些header如Host、User-Agent等;如果你在POST上傳一個表單,那麼還需要body。
  2. 將創建好的Request傳遞給WingjayHttpClient
  3. WingjayHttpClient去執行Request,並把返回結果封裝成一個Response給用戶。而一個Response裏應該包括statusCode如200,一些header如content-type等,可能還有body

到此即爲一次完整請求的雛形。那麼下面我們來具體實現這三步。

四、雛形實現

下面我們先來實現一個httpClient的雛形,只具備最基本的功能。

1. 創建Request

首先,我們要建立一個Request類,利用Request類用戶可以把自己需要的參數傳入進去,基本形式如下:

1
2
3
4
5
6
7
8
9
10
11
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如下:

1
2
3
4
5
class WingjayHttpClient {
	public Response sendRequest(Request request) {
		return executeRequest(request);
	}
}

3. 執行Request,並把返回結果封裝成一個Response返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
	...
}

五、功能擴展

利用上面的雛形,可以得到其使用方法如下:

1
2
3
4
Request request = new Request("https://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)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[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:http://www.publicobject.com/helloworld.txt ,通過訪問並抓包,可以看到如下信息:

因此,我們在接收到Response後要根據status_code是否爲重定向,如果是,則要從Response Header裏解析出新的URL-Location並自動請求新URL。那麼,我們可以繼續改寫sendRequest(request)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[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,每個攔截器會做兩件事情:

  1. 接收上一層攔截器封裝後的request,然後自身對這個request進行處理,例如添加一些header,處理後向下傳遞;
  2. 接收下一層攔截器傳遞回來的response,然後自身對response進行處理,例如判斷返回的statusCode,然後進一步處理。

那麼,我們可以爲攔截器定義一個抽象接口,然後去實現具體的攔截器。

1
2
3
interface Interceptor {
	Response intercept(Request request);
}

我們想象這個攔截器能夠接收一個request,進行攔截處理,並返回結果。

但實際上,它無法返回結果,而且它在處理request後,並不能繼續向下傳遞,因爲它並不知道下一個Interceptor在哪裏,也就無法繼續向下傳遞。

那麼,如何解決才能把所有Interceptor串在一起,並能夠依次傳遞下去。

1
2
3
4
5
6
7
8
9
public interface Interceptor {
  Response intercept(Chain chain);

  interface Chain {
    Request request();

    Response proceed(Request request);
  }
}

使用方法如下:假如我們現在有三個Interceptor需要依次攔截:

1
2
3
4
5
6
7
8
9
// 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是用來擴展requestheader的攔截器。這兩個攔截器存在於okhttp裏,實際上在okhttp裏還有好幾個攔截器,這裏暫時不做深入分析。


  1. CacheInterceptor
    這是用來攔截請求並提供緩存的,當request進入這一層,它會自動去檢查緩存,如果有,就直接返回緩存結果;否則的話纔將request繼續向下傳遞。而且,當下層把response返回到這一層,它會根據需求進行緩存處理;

  2. ConnectInterceptor
    這一層是用來與目標服務器建立連接

  3. CallServerInterceptor
    這一層位於最底層,直接向服務器發出請求,並接收服務器返回的response,並向上層層傳遞。

上面幾個都是okhttp自帶的,也就是說需要在WingjayHttpClient自己實現的。除了這幾個功能性的攔截器,我們還要支持用戶自定義攔截器,主要有以下兩種(見圖中非虛線框藍色字部分):

  1. interceptors
    這裏的攔截器是攔截用戶最原始的request。

  2. NetworkInterceptor
    這是最底層的request攔截器。

如何區分這兩個呢?舉個例子,我創建兩個LoggingInterceptor,分別放在interceptors層和NetworkInterceptor層,然後訪問一個會重定向的URL_1,當訪問完URL_1後會再去訪問重定向後的新地址URL_2。對於這個過程,interceptors層的攔截器只會攔截到URL_1的request,而在NetworkInterceptor層的攔截器則會同時攔截到URL_1URL_2兩個request。具體原因可以看上面的圖。

5. 同步、異步 Request池管理機制

這是非常核心的部分。

通過上面的工作,我們修改WingjayHttpClient後得到了下面的樣子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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協議相關的內容,還應該要設置其處理方式同步異步。那這些信息應該存在哪裏呢?兩種選擇:

  1. 直接放入Request
    從理論上來講是可以的,但是卻違背了初衷。我們最開始是希望用Request來構造符合http協議的一個請求,裏面應該包含的是請求目標網址URL,請求端口,請求方法等等信息,而http協議是不關心這個request是同步還是異步之類的信息

  2. 創建一個類,專門來管理Request的狀態
    這是更爲合適的,我們可以更好的拆分職責。

因此,這裏選擇創建兩個類SyncCallAsyncCall,用來區分同步異步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
	}
}

基於上面兩個類,我們的使用場景如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WingjayHttpClient client = new WingjayHttpClient();
// Sync
Request syncRequest = new Request("https://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);,而現在變成了

1
2
3
response = client.sendSyncCall(syncCall);

client.equeueAsyncCall(asyncCall);

那麼,我們也需要對WingjayHttpClient進行改造,基本思路是在內部添加請求池來對所有request進行管理。那麼這個請求池我們怎麼來設計呢?有兩個方法:

  1. 直接在WingjayHttpClient內部創建幾個容器
    同樣,從理論上而言是可行的。當用戶把(a)syncCall傳給client後,client自動把call存入對應的容器進行管理。

  2. 創建一個獨立的類進行管理
    顯然這樣可以更好的分配職責。我們把WingjayHttpClient的職責定義爲,接收一個call,內部進行處理後返回結果。這就是WingjayHttpClient的任務,那麼具體如何去管理這些request的執行順序和生命週期,自然不需要由它來管。

因此,我們創建一個新的類:Dispatcher,這個類的作用是:

  1. 存儲外界不斷傳入的SyncCallAsyncCall,如果用戶想取消則可以遍歷所有的call進行cancel操作;
  2. 對於SyncCall,由於它是即時運行的,因此Dispatcher只需要在SyncCall運行前存儲進來,在運行結束後移除即可;
  3. 對於AsyncCallDispatcher首先啓動一個ExecutorService,不斷取出AsyncCall去進行執行,然後,我們設置最多執行的request數量爲64,如果已經有64個request在執行中,那麼就將這個asyncCall存入等待區。

根據設計可以得到Dispatcher構造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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以實現

1
2
3
response = client.sendSyncCall(syncCall);

client.equeueAsyncCall(asyncCall);

這兩個方法了。具體實現如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[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
	}

基於以上,我們能夠很好的處理同步異步兩種請求,使用場景如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WingjayHttpClient client = new WingjayHttpClient();
// Sync
Request syncRequest = new Request("https://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的整體結構和核心機制都有了較爲詳細的瞭解。

如果有問題歡迎聯繫我。

謝謝!

wingjay



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章