帶你學開源專案:OkHttp--自己動手實現OkHttp
一、開源專案 OkHttp
在Android、Java開發領域中,相信大家都聽過或者在使用Square家大名鼎鼎的網路請求庫: OkHttp 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的雛形,只具備最基本的功能。
1. 建立Request
類
首先,我們要建立一個Request
類,利用Request
類使用者可以把自己需要的引數傳入進去,基本形式如下:
1234567891011 | classRequest{Stringurl;Stringmethod;Headers headers;Body requestBody;publicRequest(Stringurl,Stringmethod,@Nullable Headers headers,@Nullable Body body){this.url=url;...}} |
2. 將Request
物件傳遞給WingjayHttpClient
我們可以設計WingjayHttpClient
如下:
12345 | classWingjayHttpClient{publicResponse sendRequest(Request request){returnexecuteRequest(request);}} |
3. 執行Request
,並把返回結果封裝成一個Response
返回
1234567891011121314151617 | classWingjayHttpClient{...privateResponse executeRequest(Request request){//使用socket來進行訪問Socket socket=newSocket(request.getUrl(),80);ResponseData data=socket.connect().getResponseData();returnnewResponse(data);}...}classResponse{intstatusCode;Headers headers;Body responseBody...} |
五、功能擴充套件
利用上面的雛形,可以得到其使用方法如下:
1234 | Request request=newRequest("https://wingjay.com");WingjayHttpClient client=newWingjayHttpClient();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)
方法:
1234567891011121314151617 | [classWingjayHttpClient]publicResponse sendRequest(Request userRequest){Request httpRequest=expandHeaders(userRequest);returnexecuteRequest(httpRequest);}privateRequest 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)
方法:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 | [classWingjayHttpClient]privatebooleanallowRedirect=true;// user can set redirect status when building WingjayHttpClientpublicvoidsetAllowRedirect(booleanallowRedirect){this.allowRedirect=allowRedirect;}publicResponse 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 permanentlycase300:case301:case302:case303:case307:case308:returnhandleRedirect(response);default:returnresponse;}}// the max times of followup requestprivatestaticfinalintMAX_FOLLOW_UPS=20;privateintfollowupCount=0;privateResponse handleRedirect(Response response){// Does the WingjayHttpClient allow redirect?if(!client.allowRedirect()){returnnull;}// Get the redirecting urlStringnextUrl=response.header("Location");// Construct a redirecting requestRequest followup=newRequest(nextUrl);// check the max followupCountif(++followupCount>MAX_FOLLOW_UPS){thrownewException("Too many follow-up requests: "+followUpCount);}// not reach the max followup times, send followup request then.returnsendRequest(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,然後進一步處理。
那麼,我們可以為攔截器定義一個抽象介面,然後去實現具體的攔截器。
123 | interfaceInterceptor{Response intercept(Request request);} |
大家可以看下上面這個攔截器設計是否有問題?
我們想象這個攔截器能夠接收一個request,進行攔截處理,並返回結果。
但實際上,它無法返回結果,而且它在處理request後,並不能繼續向下傳遞,因為它並不知道下一個Interceptor
在哪裡,也就無法繼續向下傳遞。
那麼,如何解決才能把所有Interceptor
串在一起,並能夠依次傳遞下去。
123456789 | publicinterfaceInterceptor{Response intercept(Chain chain);interfaceChain{Request request();Response proceed(Request request);}} |
使用方法如下:假如我們現在有三個Interceptor
需要依次攔截:
123456789 | // Build a full stack of interceptors.List<Interceptor>interceptors=newArrayList<>();interceptors.add(newMyInterceptor1());interceptors.add(newMyInterceptor2());interceptors.add(newMyInterceptor3());Interceptor.Chain chain=newRealInterceptorChain(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
後得到了下面的樣子:
123456789101112131415161718192021222324 | 相關推薦帶你學開源專案:OkHttp--自己動手實現OkHttp一、開源專案 OkHttp 在Android、Java開發領域中,相信大家都聽過或者在使用Square家大名鼎鼎的網路請求庫: OkHttp https://github.com/square/okhttp ,當前多數著名的開源專案如 Fresco、Glide、 Picasso 帶你學python基礎:模塊和包自己實現 書包 view bcb 可選 存在 trac 類定義 執行 一、什麽是模塊 在我們平時的開發過程中,或多或少會用到 Python 的一些內置的功能,或者說,還會用到一些第三方的庫,我們用到的這些 Python 的內置的功能,和一些第三方的庫,就可以說是一些模塊了。 帶你學夠浪:Go語言基礎系列 - 8分鐘學基礎語法> 文章每週持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜尋公眾號「 後端技術學堂 」第一時間閱讀(一般比部落格早更新一到兩篇) 對於一般的語言使用者來說 ,20% 的語言特性就能夠滿足 80% 的使用需求,剩下在使用中掌握。 基於這一理論,Go 基礎系列的文章不會刻意追求面面俱到 帶你學夠浪:Go語言基礎系列-環境配置和 Hello world> 文章每週持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜尋公眾號「 後端技術學堂 」第一時間閱讀(一般比部落格早更新一到兩篇) 前面幾周陸陸續續寫了一些後端技術的文章,包括資料庫、微服務、記憶體管理等等,我比較傾向於成體系的學習,所以資料庫和微服務還有後續系列文章補充。 最近工作上 帶你學夠浪:Go語言基礎系列 - 8分鐘學控制流語句★ 文章每週持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜尋公眾號「 後端技術學堂 」第一時間閱讀(一般比部落格早更新一到兩篇) ” 對於一般的語言使用者來說 ,20% 的語言特性就能夠滿足 80% 的使用需求,剩下在使用中掌握。基於這一理論,Go 基礎系列的文章不會刻意追求面面俱到, 帶你學夠浪:Go語言基礎系列 - 8分鐘學複合型別★ 文章每週持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜尋公眾號「 後端技術學堂 」第一時間閱讀(一般比部落格早更新一到兩篇) ” 對於一般的語言使用者來說 ,20% 的語言特性就能夠滿足 80% 的使用需求,剩下在使用中掌握。基於這一理論,Go 基礎系列的文章不會刻意追求面面俱到, idou老師教你學Istio 16:如何用 Istio 實現微服務間的訪問控制字符 rod 登出 決定 yaml des 有時 tin bbs 摘要 使用 Istio 可以很方便地實現微服務間的訪問控制。本文演示了使用 Denier 適配器實現拒絕訪問,和 Listchecker 適配器實現黑白名單兩種方法。 使用場景 有時需要對微服務間的相互訪問進 idou老師教你學Istio 25:如何用istio實現監控和日誌采集設置 時間信息 Kubernete 標準 每一個 搜索 warning 打印日誌 度量標準 大家都知道istio可以幫助我們實現灰度發布、流量監控、流量治理等功能。每一個功能都幫助我們在不同場景中實現不同的業務。那Istio是如何幫助我們實現監控和日誌采集的呢? 這裏我們依 GitHub 熱點速覽 Vol.34:亞馬遜、微軟開源專案帶你學硬核技術![](https://img2020.cnblogs.com/blog/759200/202008/759200-20200824230332585-1697508771.png) 作者:HelloGitHub-**小魚乾** > 摘要:站在巨人的肩膀上才能看得更遠,本週上榜的 computerv Android JetPack架構篇,一個實戰專案帶你學懂JetPack今日科技快訊 第五屆世界網際網路大會昨日開幕,來自76個國家的1500餘位嘉賓出席大會。騰訊公司董事會主席兼執行長馬化騰在大會開幕式演講中表示,全球產業都在進行數字化,在此期間機遇挑戰並存,產業網際網路機會巨大。 作者簡介 本篇來自 w 清風帶你學-H5+CSS3(六)使用less維護rem佈局專案M-web 掌握less的安裝和編譯 掌握less的基本語法 掌握在專案中使用less 掌握rem適配的原理 掌握rem+媒體查詢適配 掌握rem+flexible適配 課程內容 less 什麼是less 作為 若你要開源自己的程式碼,此文帶你瞭解開源協議作為一個開發者,如果你打算開源自己的程式碼,千萬不要忘記,選擇一種開源許可證(license)。 許多開發者對開源許可證瞭解很少,不清楚有哪些許可證,應該怎麼選擇。本文介紹開源許可證的基本知識,主要參考了 OpenSource.com (1,2)。 一、什麼是開源 idou老師帶教你學Istio 03: istio故障註入功能的介紹和使用清除 net rtu 2.3 所有 review book 負載 images 故障註入測試 故障註入測試顧名思義就是當被測試應用部分組件或功能出現潛在故障時其本身的容錯機制是否正常工作,以達到規避故障保證正常組件或功能的使用。Istio提供了HTTP故障註入功能,在htt 【牛客帶你學程式設計C++方向】專案練習第1期//普通構造 MyString::MyString(const char *str){ if(str == NULL){ m_data = new char[1]; / 講解開源專案:讓你成為靈魂畫手的 JS 引擎:Zdog本文作者:HelloGitHub-kalifun HelloGitHub 的《講解開源專案》系列,專案地址:https://github.com/HelloGitHub-Team/Article 今天給大家推薦一個使用 JavaScript 語言編寫的開源 Web 3D 模型專案 —— Zdog。 一 讓你如紳士般基於描述編寫 Python 命令列工具的開源專案:docopt作者:HelloGitHub-Prodesire HelloGitHub 的《講解開源專案》系列,專案地址:https://github.com/HelloGitHub-Team/Article 一、前言 在本系列前面四篇文章中,我們介紹了 argparse 的方方面面。它無疑是強大的,但使用 讓你如“老”紳士般編寫 Python 命令列工具的開源專案:docopt作者:HelloGitHub-Prodesire HelloGitHub 的《講解開源專案》系列,專案地址:https://github.com/HelloGitHub-Team/Article 一、前言 在第一篇“初探 docopt”的文章中,我們初步掌握了使用 docopt 的三個步驟,瞭 演算法君帶你學演算法(1):最長迴文字串演算法君:小白同學,給你出道演算法題,看你小子演算法能力有沒有長進。 演算法小白:最近一直在研究演算法,刷了很多演算法題,正好活動活動大腦,來來來,趕快出題! 演算法君:聽好了,題目是:求一個字串中最長的迴文字串。 演算法小白:這個演算法好像很簡單,就是有一個概念不太明白,啥叫“迴文字串&r 樹義帶你學 Prometheus(三):Grafana 圖表配置快速入門# 文章首發於【陳樹義】公眾號,點選跳轉到原文:https://mp.weixin.qq.com/s/sA0nYevO8yz6QLRz03qJSw 前面我們使用 Prometheus + Grafana 實現了一個簡單的 CPU 使用率變化圖,但是這個圖還有許多缺陷,例如:左邊欄的數值太小了無法調整,下面的 「補課」進行時:設計模式(6)——郭靖大俠帶你學原型模式![](https://cdn.geekdigging.com/DesignPatterns/java_design_pattern.jpg) ## 1. 前文彙總 [「補課」進行時:設計模式系列](https://www.geekdigging.com/category/%e8%ae%be%e8%ae% |