帶你學開源專案:OkHttp-- 自己動手實現 okhttp
一、開源專案 OkHttp
在 Android、Java 開發領域中,相信大家都聽過或者在使用 Square 家大名鼎鼎的網路請求庫——OkHttp—— ofollow,noindex">https://github.com/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 類使用者可以把自己需要的引數傳入進去,基本形式如下:
image.png
-
將 Request 物件傳遞給 WingjayHttpClient
我們可以設計 WingjayHttpClient 如下:
image.png
-
執行 Request,並把返回結果封裝成一個Response 返回
image.png
五、功能擴充套件
利用上面的雛形,可以得到其使用方法如下:

image.png
三、思考然而,上面的雛形是遠遠不能勝任常規的應用需求的,因此,下面再來對它新增一些常用的功能模組。
-
重新把簡陋的 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) 方法:

image.png
2. 支援自動重定向
有時我們請求的 URL 已經被移走了,此時 server 會返回 301 狀態碼和一個重定向的新 URL,此時我們要能夠支援自動訪問新 URL 而不是向用戶報錯。
對於重定向這裡有一個測試性 URL: http://www.publicobject.com/helloworld.txt ,通過訪問並抓包,可以看到如下資訊:

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

image.png
userRequest
的返回結果,判斷結果是否為重定向,並做出自動 followup 處理。
一些常用的狀態碼
100~199:指示資訊,表示請求已接收,繼續處理
200~299:請求成功,表示請求已被成功接收、理解、接受
300~399:重定向,要完成請求必須進行更進一步的操作
400~499:客戶端錯誤,請求有語法錯誤或請求無法實現
500~599:伺服器端錯誤,伺服器未能實現合法的請求
-
支援重試機制
所謂重試,和重定向非常類似,即通過判斷 Response 狀態,如果連線伺服器失敗等,那麼可以嘗試獲取一個新的路徑進行重新連線,大致的實現和重定向非常類似,此不贅述。
-
Request & Response 攔截機制
這是非常核心的部分。
通過上面的重新組裝 request 和重定向機制,我們可以感受的,一個 request 從 user 創建出來後,會經過層層處理後,才真正發出去,而一個response,也會經過各種處理,最終返回給使用者。
筆者認為這和網路協議棧非常相似,使用者在應用層發出簡單的資料,然後經過傳輸層、網路層等,層層封裝後真正把請求從物理層發出去,當請求結果回來後又層層解析,最終把最直接的結果返回給使用者使用。
最重要的是,每一層都是抽象的,互不相關的!
因此在我們設計時,也可以借鑑這個思想,通過設定 攔截器 Interceptor,每個攔截器會做兩件事情:
1 .接收上一層攔截器封裝後的 request,然後自身對這個 request 進行處理,例如新增一些 header,處理後向下傳遞;
2 .接收下一層攔截器傳遞回來的 response,然後自身對 response 進行處理,例如判斷返回的 statusCode,然後進一步處理。
那麼,我們可以為攔截器定義一個抽象介面,然後去實現具體的攔截器。

image.png
大家可以看下上面這個攔截器設計是否有問題?

image
我們想象這個攔截器能夠接收一個 request,進行攔截處理,並返回結果。
但實際上,它無法返回結果,而且它在處理 request 後,並不能繼續向下傳遞,因為它並不知道下一個 Interceptor
在哪裡,也就無法繼續向下傳遞。
那麼,如何解決才能把所有 Interceptor
串在一起,並能夠依次傳遞下去。

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

image.png
RealInterceptorChain
的基本思想是:我們把所有
interceptors
傳進去,然後
chain
去依次把
request
傳入到每一個
interceptors
進行攔截即可。
通過下面的示意圖可以明確看出攔截流程:

image
其中, RetryAndFollowupInterceptor
是用來做自動重試和自動重定向的攔截器; BridgeInterceptor
是用來擴充套件 request
的 header
的攔截器。這兩個攔截器存在於 okhttp
裡,實際上在 okhttp
裡還有好幾個攔截器,這裡暫時不做深入分析。

image
1 .CacheInterceptor
這是用來攔截請求並提供快取的,當 request 進入這一層,它會自動去檢查快取,如果有,就直接返回快取結果;否則的話才將 request 繼續向下傳遞。而且,當下層把 response 返回到這一層,它會根據需求進行快取處理;
2 .ConnectInterceptor
這一層是用來與目標伺服器建立連線
3 .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 後得到了下面的樣子:

image.png
也就是說,WingjayHttpClient現在能夠 同步 地處理單個 Request 了。
然而,在實際應用中,一個 WingjayHttpClient 可能會被用於同時處理幾十個使用者 request,而且這些 request 裡還分成了 同步 和非同步 兩種不同的請求方式,所以我們顯然不能簡單把一個 request 直接塞給WingjayHttpClient。
我們知道,一個 request 除了上面定義的 http 協議相關的內容,還應該要設定其處理方式 同步 和非同步。那這些資訊應該存在哪裡呢?兩種選擇:
直接放入 Request
從理論上來講是可以的,但是卻違背了初衷。我們最開始是希望用 Request 來構造符合 http 協議的一個請求,裡面應該包含的是請求目標網址 URL,請求埠,請求方法等等資訊,而 http 協議是不關心這個 request 是同步還是非同步之類的資訊
建立一個類,專門來管理 Request 的狀態
這是更為合適的,我們可以更好的拆分職責。
因此,這裡選擇建立兩個類 SyncCall 和AsyncCall,用來區分 同步 和非同步。

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

image.png
從上面的程式碼可以看到,WingjayHttpClient的職責發生了變化:以前是response = client.sendRequest(request);,而現在變成了

image.png
那麼,我們也需要對 WingjayHttpClient 進行改造,基本思路是在內部新增 請求池 來對所有 request 進行管理。那麼這個 請求池 我們怎麼來設計呢?有兩個方法:
1 .直接在 WingjayHttpClient 內部建立幾個容器
同樣,從理論上而言是可行的。當用戶把(a)syncCall 傳給 client 後,client 自動把 call 存入對應的容器進行管理。
2 .建立一個獨立的類進行管理
顯然這樣可以更好的分配職責。我們把 WingjayHttpClient 的職責定義為,接收一個 call,內部進行處理後返回結果。這就是 WingjayHttpClient 的任務,那麼具體如何去管理這些 request 的執行順序和生命週期,自然不需要由它來管。
因此,我們建立一個新的類:Dispatcher,這個類的作用是:
1 .儲存外界不斷傳入的 SyncCall 和AsyncCall,如果使用者想取消則可以遍歷所有的 call 進行 cancel 操作;
2 .對於 SyncCall,由於它是即時執行的,因此Dispatcher 只需要在 SyncCall 執行前儲存進來,在執行結束後移除即可;
3 .對於 AsyncCall,Dispatcher 首先啟動一個 ExecutorService,不斷取出 AsyncCall 去進行執行,然後,我們設定最多執行的 request 數量為 64,如果已經有 64 個 request 在執行中,那麼就將這個 asyncCall 存入等待區。
根據設計可以得到 Dispatcher 構造:

image.png
有了這個 Dispatcher,那我們就可以去修改WingjayHttpClient 以實現

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

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

image.png
六、總結
到此,我們基本把 okhttp 裡核心的機制都講解了一遍,相信讀者對於 okhttp 的整體結構和核心機制都有了較為詳細的瞭解。
如果有問題歡迎聯絡我。
謝謝!