隻有讀過網絡請求架構源碼,你才知道發送一個請求到底有多麼複雜…
适讀人群
- 具有JAVA基礎開發知識
- 接觸過HTTP,HTTPS,HTTP2,TCP,Socket等相關知識
- 簡單了解設計模式
背景
從Google開始摒棄了httpclient替換為了Okhttp後,Android的網絡請求世界 Retrofit2 + RxJava也在大行其道時;OkHttp無疑成為了大家公認好評的新一代網絡請求架構;但是,筆者認為一個架構的優劣不能隻是因為其流行就裁定它一定是目前最适合我們自己的工具;應該一切都有我們自己的判斷,隻有我們自己能夠說出的它的優勢劣勢我們才可以真正的用好這些架構;基于此種目的打算對OKhttp一探究竟…
GET STRATED
首先來快速了解一下OKhttp,它是一個能夠幫助我們在交換資料和媒體時,載入速度更快且節省網絡帶寬的HTTP用戶端;
okhttp支援
同步調用
我們
GET 示例
public class GetExample {
//建構一個 OkHttpClient的執行個體
OkHttpClient client = new OkHttpClient();
String run(String url) throws IOException {
//建構一個 request 網絡請求,傳入定義的url
Request request = new Request.Builder()
.url(url)
.build();
//傳入client,調用excute進行執行擷取到使用者需要的響應
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
}
POST 示例
//定義MediaType
public static final MediaType JSON
= MediaType.get("application/json; charset=utf-8");
//初始化 OKhttpClient
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
//針對requestBody進行初始化
RequestBody body = RequestBody.create(JSON, json);
//傳入對應的url和body,建構
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Call call = client.newCall(request);
try (Response response = call.execute()) {
return response.body().string();
}
}
初識OKhttp
首先,了解了上文的示例,我們對于OKhttp的API調用有了一個初步的認識,接下來簡單介紹一下我們剛才所用到一些類
okhttpClient的說明圖,request說明圖,response說明圖,call說明圖
由此我們便可以初步的了解了OkHttp的簡單調用和與使用者最常打交道的幾個類;接下來我們針對上文的示例代碼做一個初步的流程分析,讓大家有一個簡單對于OKhttp的工作流程有一個大緻的了解

是以,通過此圖我們可以大緻的了解到關于OKhttp最粗略的一個流程,在call中有一個execute的方法;而其中最核心的就是這個方法 ;所有的網絡請求的整個處理核心就在這個部分;是以接下來我們的重心就是這裡
Interceptor
在分析前我們需要了解一下OKhttp在架構中引入的一個概念 interceptor (攔截器),它的作用機制非常強大,可以監控,重寫,重複調用;借用官網的一個示例簡單了解一下用法
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
//擷取攔截器鍊中的request
Request request = chain.request();
//記錄 request相關資訊
long t1 = System.nanoTime();
logger.info(String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
logger.info(String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
此為一個日志攔截器的定義,而這個攔截器最關鍵的部分就是 **chain.proceed(request)**這個就是我們的http互動産生對應目前的request的response的部分;這些攔截器會被串聯起來依照順序依次調用,此處引用下官方文檔說明:
A call to chain.proceed(request) is a critical part of each interceptor’s implementation. This simple-looking method is where all the HTTP work happens, producing a response to satisfy the request.
對chain.proceed(請求)的調用是每個攔截器實作的關鍵部分。這個看起來很簡單的方法是所有HTTP工作發生的地方,它生成一個響應來滿足請求。
Interceptors can be chained. Suppose you have both a compressing interceptor and a checksumming interceptor: you’ll need to decide whether data is compressed and then checksummed, or checksummed and then compressed. OkHttp uses lists to track interceptors, and interceptors are called in order.
攔截器可以連結。假設您同時擁有壓縮攔截器和校驗和攔截器:您需要确定資料是否已壓縮,然後進行校驗和,校驗和再壓縮。 OkHttp使用清單來跟蹤攔截器,并按順序調用攔截器。
然後,大概了解一下兩個概念,就是 OKhttp提供了兩種攔截器 :
- 應用級别攔截器:隻會調用一次,擷取到最終的response結果
- 網絡級别攔截器:可以感覺到網絡的請求的重定向,以及重試會被執行多次
引用兩個代碼示例,讓我們簡單感受一下兩者的差別,具體示例代碼詳細分析可以檢視 interceptors
應用級别攔截器
OkHttpClient client = new OkHttpClient.Builder()
//添加了上文的日志攔截器 (應用級别攔截器)
.addInterceptor(new LoggingInterceptor())
.build();
//添加url以及header
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
結果展示:
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
從結果可以發現,我們的是得到一個最終的結果
網絡級别攔截器
OkHttpClient client = new OkHttpClient.Builder()
//添加了上文的日志攔截器 (網絡級别攔截器)
.addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
結果展示:
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt
INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
從此結果可以看到,我們每次網絡發生比如上文的重定向的情況,都可以清楚的感覺到 ;初始的一次請求的是http://www.publicobject.com/helloworld.txt,另外一次重定向到了 https://publicobject.com/helloworld.txt.
核心主流程分析——攔截器組合使用
上文我們知道了攔截器這個概念,實際上OKhttp不僅提供使用者可以編輯的添加的Interceptor,實際上整個的OKhttp的網絡請求整體都是通過一個個的攔截器分層完成的,那麼接下來我們把上面的示意圖再次拓展更詳細些
那麼來根據此示意圖可以看的出,其實我們的網絡請求的過程被分解成了一個個的步驟分别分工在每一個類裡面去執行,每一個類隻負責自己部分工作内容,也就是我們常說的責任鍊模式(chain-of-responsibility-pattern)并且還在責任鍊的兩個位置提供添加使用者的自定義攔截器由此友善針對request和response進行各種自定義的處理,接下來我們來看一下源碼部分的實作:
首先是使用者調用部分:
使用者定義了request,傳入了定義好的一個client;并且調用了execute方法
//定義client
OkHttpClient client = new OkHttpClient.Builder()
//添加使用者的攔截器
.addInterceptor(new LoggingInterceptor())
.addNetworkInterceptor(new LoggingInterceptor())
.build();
//開始執行網絡請求
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
調用了此時execute最核心的代碼:
@Override public Response execute() throws IOException {
synchronized (this) {
//暫時忽略部分
//執行響應鍊
return getResponseWithInterceptorChain();
//暫時忽略部分
}
攔截器鍊的調用:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//重試之前添加的攔截器,由于 重試層将網絡請求全部處理完畢後,才傳回;是以是無感覺的,使用者的攔截器隻會得到一個結果
interceptors.addAll(client.interceptors());
//重試重定向攔截器
interceptors.add(new RetryAndFollowUpInterceptor(client));
//給request添加網絡請求必須的header,處理request的response的壓縮和解壓縮
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//處理網路的緩存的部分
interceptors.add(new CacheInterceptor(client.internalCache()));
//建立或者從連接配接池中找到合适的安全的連接配接
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
//網絡層的攔截器,由于在重試層之下;是以,每次的重試重定向都會有感覺,并且是以此時使用者添加的 網絡級别的 攔截器 就會調用多次
interceptors.addAll(client.networkInterceptors());
}
//執行實際的io流,request和resopnse的接收
interceptors.add(new CallServerInterceptor(forWebSocket));
//定義 串聯攔截的鍊對象,将攔截器串聯起來
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
//忽略
try {
//依次執行攔截器
Response response = chain.proceed(originalRequest);
//忽略
return response;
} catch (IOException e) {
//忽略
} finally {
//忽略
}
}
然後,需要補充說明一點的是:
上面在**getResponseWithInterceptorChain()**調用中
interceptors.addAll(client.interceptors());
以及
interceptors.addAll(client.networkInterceptors());
都是在上述代碼:
OkHttpClient client = new OkHttpClient.Builder()
//添加使用者的攔截器
.addInterceptor(new LoggingInterceptor())
.addNetworkInterceptor(new LoggingInterceptor())
.build();
由使用者添加進來的
被okHttpClient緩存到了自己的成員變量中
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
final List<Interceptor> interceptors;
final List<Interceptor> networkInterceptors;
}
實戰篇
學習源碼如果隻是将我們看到的源碼進行翻譯,其實就失去了學習的意義;因為在我們實際生活用不到遲早還是會忘記和沒有學習一樣;是以,筆者為大家提供幾種我在公司中針對攔截器的實際應用方式,讓大家感受一下OKHttp通過攔截器的方式給大家提供便利性的作用
招式一: 記錄網絡資訊
背景
筆者的公司在開發過程中經常和背景對接網絡接口時,經常會有這樣的對話(PS:筆者負責的app對接的接口有對接加密):
筆者: XXX接口邏輯有問題
背景: 麻煩把明文的封包和加密的封包都發一下,調試一下
筆者: xxx接口的request 是.... ,response是.......
方案
首先,根據上面的場景,看得出我們一般都會有需要時刻擷取到每一條請求的明文和密文的情況;是以,一般我們想到的最直接的辦法就是解決以上情況的方式就是将request和response的内容都打在控制台上,友善可以随時複制;那麼這種場景很明顯就可以用Interceptor來解決問題,那麼這個解決思路如下:
1.根據目前場景,我們要首先有一個對請求響應處理加解密的 application級别的interceptor(ps:加解密隻需要進行一次,故不選擇網絡級别攔截器)
2.根據我們上文我們可以知道,攔截器鍊上依此按照每一個攔截器添加的順序依次執行的,故我們的思路就是在加密攔截器前後各添加一個記錄日志的攔截器,進而得到兩種請求
代碼如下:
OkHttpClient client = new OkHttpClient.Builder()
//添加了明文的日志攔截器
.addInterceptor(new ClearTextLoggingInterceptor())
//添加了加解密
.addInterceptor(new CodecInterceptor())
//添加了密文的日志攔截器
.addInterceptor(new EncrypteLoggingInterceptor())
.build();
為了友善了解可以看一下示意圖
由此我們可以感覺到這種責任鍊的模式給我們提供的對于 request和response的豐富的客戶化拓展性,并且我們可以通過這樣的方式将我們想要定制化的效果也分工在每一個類中,然後按照順序依次執行;這樣有以下的好處:
- 當客戶化的功能被拆分在每一個攔截器後,友善使用者在後期維護中針對功能的增加或者減少
- 提供了每一個類的清晰的可讀性,不用把大量的客戶化代碼都放置到一個類
招式二 使用者請求偷天換日李代桃僵
背景
在筆者在公司以下一系列問題:
- 公司由于是to B的業務較多;業務邏輯非常複雜,為我們的app制造資料成了一個成本非常高的問題
- 我們的背景人員資源不足導緻我們并行開發對接時,接口給到的時間也經常比較晚,造成前端開發人員時間前松後緊
- 筆者同時還在負責着一部分的app的自動化測試,同樣的資料問題限制着UI自動化的進行,緻使腳本運作前會需要做大量準備工作,準備好app需要消費的資料
而業界比較公認的一種解決這類問題的方案就是 MOCK資料
但是,當嘗試解決此問題時,又有了一隻新的攔路虎産生,那就是app的網絡請求是加密的,并且由于曆史原因各個app的加密方案還不一樣,這兩者造成的傷害就十分巨大了;因為它會産生這樣的以下問題:
- 如果加密方案是一緻的,用一個比較簡單粗暴的方式就是将我們的加解密方案,在我們自己搭建的mock平台客戶化後移植進去,且針對不同項目還要加解密的key配置不一樣的
- app要做到多套baseUrl的自由切換,實作mock與真實的請求的互動,又或者可以在wifi中搭建代理伺服器幫助切換真實和MOCK的資料,但是兩者都具有高昂的成本
方案
是以,基于以上的背景,我們考慮在服務端去解決這樣的問題,雖然實作了可以做到用戶端無感覺,且代碼沒有入侵性;但是實際上确實付出的代價十分高昂,故我們設計這樣的方案:
首先,我們知道
實際是被這樣的一層一層的攔截器處理過後,才傳回給使用者的response
interceptors.addAll(client.interceptors());
interceptors.add(new RetryAndFollowUpInterceptor(client));
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
interceptors.addAll(client.networkInterceptors());
interceptors.add(new CallServerInterceptor(forWebSocket));
而在攔截器中
class SomeInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
//調用此行代碼去鍊式調用下一個攔截器
Response response = chain.proceed(request);
return response;
}
是以,我們可以得到一個結論就是,每一個攔截器可以決定我的是否要鍊式調用下一層的攔截器,同時也可以決定我這一層的攔截器response資料來自哪裡,是以我們有了以下的方案:
從圖上可以看到使用者的request經過了一些正常的攔截器後,在我們的加解密攔截器前添加了一個mock攔截器,在判斷了使用者的請求是否需要走mock後,決定使用者的response是從真實網絡請求中擷取,還是從mock server的平台擷取,而這個思路充分展現了 責任鍊模式中,由責任鍊的每一環節決定配置設定的任務是否是由自己處理還是分發給下一層處理,而對調用者來說這些都是透明毫無感覺的!
轉換成代碼
class MockInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
//判斷是否需要mock
if(isNeedMock(request)){
//讀取原始request建構對應的MOCK server的request
Request mockRequest = buildMockRequest(request);
//通路mockServer
return MockServer(mockRequest);
}
//調用此行代碼去鍊式調用下一個攔截器
Response response = chain.proceed(request);
return response;
}
private boolean isNeedMock(){
/*此方法主要是用來控制哪些request走假資料,哪些request走真實資料,友善開發自己切換;讀者可以根據自己需求自行設計,筆者此處僅提供一個思路*/
}
private Request buildMockRequest( Request request){
/*該方法的主要作用在于我們要把真實的request替換成通路我們自行搭建的mock server平台的請求,一般是替換掉請求的baseUrl部分(即ip位址部分);其他的替換根據讀者選用的mock平台自行決定
*/
}
/**
*這一部分的就非常簡單了,發送一個網絡請求給 mockServer把mockServer的response傳回使用者
**/
private Response MockServer(Request request) throw Exeption{
return client.newCall(request).execute());
}
此種方案的優劣如下
優勢:
- MOCK SERVER 平台根據項目情況自行選擇,由于不需要關心app的本身加密解密,那麼基本很多MOCK平台的初始功能就能滿足需要,節省了大量了閱讀開源mock平台的時間,以及在平台對接各個app加解密的時間
- 真實的接口和mock接口切換靠app端自行代碼中随意切換,具體政策由移動端開發人員自己設計;對于大部分移動端同學來說在自己代碼中嵌入一些内容,相比搭建代理伺服器在并且在伺服器的工程寫背景代碼耗費的時間成本,精力成本遠遠小的多
- 基于okhttp的攔截器設計,友善随時随時添加移除;對于調用者卻毫無感覺!這意味着一旦産生問題,或者需要有所改動,我們所有關注點隻需要集中在mock攔截器上,代碼維護成本十分低廉!
劣勢:
- 需要在業務代碼中嵌入一些友善開發者使用的工具有一定的侵入性和危險性;這部分如果我們有一個合理的開關,可以控制危險到一個可控範圍
- 跳過了不同app的加密部分的問題,但是每一個要用mockServer的app都需要做相類似的定制化操作;不過,如果經過良好的設計,這部分完全可以考慮提取成一個類庫友善其他app內建,這樣便可以大大降低風險
當然還要搭建一個合适的mock平台,筆者選用的是 EASY-MOCK,感興趣的同學可以自行了解下~
至此,OKhttp最主要的流程就是此部分的内容,欲知後事如何且聽下回分解
NEW FEATURE
- 責任鍊RealInterceptorChain如何實作對于攔截器的串聯
- RetryAndFollowUpInterceptor重試重定向的分析
- BridgeInterceptor的分析
- CacheInterceptor緩存機制分析
- ConnectInterceptor連接配接複用相關分析
- CallServerInterceptor實際IO操作分析
- OKhttp同步與異步calls的調用管理分析