淺析okHttp3的網路請求流程
okHttp目前可以稱的上是Android主流網路框架,甚至連谷歌官方也將網路請求的實現替換成okHttp.
網上也有很多人對okHttp的原始碼進行了分析,不過基於每個人的分析思路都不盡相同,讀者看起來的收穫也各不相同,所以我還是整理了下思路,寫了點自己的分析感悟。
本文基於 okhttp3.11.0 版本分析
基本用法
String url = "http://www.baidu.com"; //'1. 生成OkHttpClient例項物件' OkHttpClient okHttpClient = new OkHttpClient(); //'2. 生成Request物件' Request request = new Request.Builder() .url(url) .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"),"test content")) .build(); Call call = okHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { } @Override public void onResponse(@NonNull Call call, @NonNull Response response){ } }); 複製程式碼
整體流程
借用別人的一張流程圖來概括一下okHttp的請求走向原圖出處

okHttp的整體流程大致分為以下幾個階段
-
建立請求物件 (url, method,body)-->request-->Call
-
請求事件佇列,執行緒池分發 enqueue-->Runnable-->ThreadPoolExecutor
-
遞迴
Interceptor
攔截器,傳送請求。 InterceptorChain -
請求回撥,資料解析。 Respose-->(code,message,requestBody)
建立請求物件
其中 Request
維護請求物件的屬性
public final class Request { final HttpUrl url; final String method; final Headers headers; final @Nullable RequestBody body; //請求的標記,在okHttp2.x的時候,okHttpClint提供Cancel(tag)的方法來批量取消請求 //不過在3.x上批量請求的api被刪除了,要取消請求只能在Callback中呼叫 call.cancel() //因此這個tags引數只能由開發者自己編寫函式來實現批量取消請求的操作 final Map<Class<?>, Object> tags; } 複製程式碼
請求響應的包裝介面 Call
public interface Call extends Cloneable { Request request(); Response execute() throws IOException; void enqueue(Callback responseCallback); void cancel(); } 複製程式碼
請求事件佇列,執行緒池分發
Call的實現類 RealCall
和 AsyncCall
@Override public void enqueue(Callback responseCallback) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } captureCallStackTrace(); eventListener.callStart(this); client.dispatcher().enqueue(new AsyncCall(responseCallback)); } //其中AsyncCall是RealCall的一個內部類,繼承自Runnable,這樣就能通過執行緒池來回調AsyncCall的execute函式 final class AsyncCall extends NamedRunnable { @Override protected void execute() { boolean signalledCallback = false; try { //getResponseWithInterceptorChain 攔截鏈的邏輯,也是發起請求的真正入口 Response response = getResponseWithInterceptorChain(); if (retryAndFollowUpInterceptor.isCanceled()) { signalledCallback = true; responseCallback.onFailure(RealCall.this, new IOException("Canceled")); } else { signalledCallback = true; responseCallback.onResponse(RealCall.this, response); } } catch (IOException e) { ... } ... } } 複製程式碼
遞迴Interceptor攔截器,傳送請求
Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. List<Interceptor> interceptors = new ArrayList<>(); //使用者自定義的攔截器(注意addAll 所以可以新增多個自定義的攔截器) interceptors.addAll(client.interceptors()); //重試與重定向攔截器 interceptors.add(retryAndFollowUpInterceptor); //內容攔截器 interceptors.add(new BridgeInterceptor(client.cookieJar())); //快取攔截器 interceptors.add(new CacheInterceptor(client.internalCache())); //網路連線攔截器 interceptors.add(new ConnectInterceptor(client)); if (!forSocket/">WebSocket) { //使用者自定義的網路攔截器 interceptors.addAll(client.networkInterceptors()); } //服務請求的攔截器 interceptors.add(new CallServerInterceptor(forWebSocket)); Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); return chain.proceed(originalRequest); } 複製程式碼
okHttp的核心部分就是這個 Interceptor
攔截鏈,每個 Interceptor
各自負責一部分功能,內部通過遞迴的方式遍歷每一個 Interceptor
攔截器。遞迴邏輯在 RealInterceptorChain
類下
public final class RealInterceptorChain implements Interceptor.Chain { //攔截器遞迴的入口 public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException { ... //攔截器遞迴的核心程式碼,根據interceptors列表執行每一個攔截器的intercept函式 RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); .... return response; } } 複製程式碼
遞迴結束後會獲得請求響應,那麼說明我們的 request 行為就在這個攔截鏈中,接下來我們先看看負責網路請求的那部分攔截器,從類名上就能比較容易的看出 ConnectInterceptor
和 CallServerInterceptor
這兩個攔截器的主要工作。
網路連線攔截器 ConnectInterceptor
public final class ConnectInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); StreamAllocation streamAllocation = realChain.streamAllocation(); // We need the network to satisfy this request. Possibly for validating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection); } } 複製程式碼
其中有幾個物件說明一下
-
**StreamAllocation:**記憶體流的儲存空間,這個物件可以直接從realChain中直接獲取,說明在之前的攔截鏈中就已經賦值過
-
HttpCodec(Encodes HTTP requests and decodes HTTP responses):對請求的編碼以及對響應資料的解碼
-
**realChain.proceed():**通知下一個攔截器執行
接下來看建立HttpCodec物件的newStream函式中做了些什麼
//HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); public HttpCodec newStream( OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) { ... try { //findHealthyConnection內部通過一個死迴圈查詢一個可用的連線,優先使用存在的可用連線,否則就通過//執行緒池來生成,其中多處使用 synchronized關鍵字,防止因為多併發導致問題 RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks); HttpCodec resultCodec = resultConnection.newCodec(client, chain, this); synchronized (connectionPool) { codec = resultCodec; return resultCodec; } } catch (IOException e) { throw new RouteException(e); } } 複製程式碼
沿著程式碼往下走,你會發現實際上負責網路連線功能的類是一個叫 RealConnection
的類,該類中有一個 connect
的函式
RealConnection#connect
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) { ... while (true) { try { if (route.requiresTunnel()) { //這個函式最終還是會走到connectSocket()函式中 connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener); if (rawSocket == null) { // We were unable to connect the tunnel but properly closed down our resources. break; } } else { connectSocket(connectTimeout, readTimeout, call, eventListener); } } ... } //最終呼叫的還是Socket物件來建立網路連線,包括connectTimeout,readTimeout等引數也是這個時候真正設定的。 複製程式碼
網路請求攔截器 CallServerInterceptor
This is the last interceptor in the chain. It makes a network call to the server.
直接看 CallServerInterceptor
的intercept函式
@Override public Response intercept(Chain chain) throws IOException{ //下面的各引數都是之前幾個攔截器所生成的 RealInterceptorChain realChain = (RealInterceptorChain) chain; HttpCodec httpCodec = realChain.httpStream(); StreamAllocation streamAllocation = realChain.streamAllocation(); RealConnection connection = (RealConnection) realChain.connection(); Request request = realChain.request(); //傳送請求頭,也是網路請求的開始 httpCodec.writeRequestHeaders(request); Response.Builder responseBuilder = null; //請求不是get,並且有添加了請求體,寫入請求體資訊 if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) { //如果請求頭中有Expect:100-continue這麼一個屬性 //會先發送一個header部分給伺服器,並詢問伺服器是否支援Expect:100-continue 這麼一個擴充套件域 //okhttp3提供這麼個判斷是為了相容http2的連線複用行為的 if ("100-continue".equalsIgnoreCase(request.header("Expect"))) { //重新整理快取區,可以理解為向服務端寫入資料 httpCodec.flushRequest(); realChain.eventListener().responseHeadersStart(realChain.call()); responseBuilder = httpCodec.readResponseHeaders(true); } //寫入請求body if (responseBuilder == null) { realChain.eventListener().requestBodyStart(realChain.call()); long contentLength = request.body().contentLength(); CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength)); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); realChain.eventListener() .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount); } ... httpCodec.finishRequest(); //響應相關的程式碼 ... } 複製程式碼
寫入請求body的核心程式碼
//將請求體寫入到BufferedSink中,而BufferedSink是另外一個類庫Okio中的類 CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength)); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); //httpCodec.finishRequest 最終會呼叫 sink.flush(),sink是BufferedSink的物件,BufferedSink在底層 //會將其內的資料推給服務端,相當於是一個重新整理緩衝區的功能 httpCodec.finishRequest(); 複製程式碼
響應相關的程式碼
if (responseBuilder == null) { realChain.eventListener().responseHeadersStart(realChain.call()); //讀取響應頭,實際的返回流存放位置在okio庫下的buffer物件中,讀取過程中做了判斷,只有當code==100時才會 //有返回,不然丟擲異常並攔截,所以下面這段程式碼肯定有響應頭返回,不然直到超時也不會回撥 responseBuilder = httpCodec.readResponseHeaders(false); } Response response = responseBuilder .request(request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); int code = response.code(); if (code == 100) { //如果服務端響應碼為100,需要我們再次請求,注意這裡的100是響應碼和之前的100不同 //之前的100是headerLine的標識碼 responseBuilder = httpCodec.readResponseHeaders(false); response = responseBuilder .request(request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); code = response.code(); } if (forWebSocket && code == 101) { //Connection is upgrading, but we need to ensure interceptors see a //non-null response body. response = response.newBuilder() .body(Util.EMPTY_RESPONSE) .build(); } else { //讀取響應body response = response.newBuilder() .body(httpCodec.openResponseBody(response)) .build(); } return response; 複製程式碼
讀取響應body HttpCodec#openResponseBody
public ResponseBody openResponseBody(Response response) throws IOException { ... Source source = newFixedLengthSource(contentLength); return new RealResponseBody(contentType, contentLength, Okio.buffer(source)); ... } //openResponseBody將Socket的輸入流InputStream物件交給OkIo的Source物件,然後封裝成RealResponseBody(該類是ResponseBody的子類)作為Response的body. //具體讀取是在RealResponseBody父類ResponseBody中,其中有個string()函式 //響應主體存放在記憶體中,然後呼叫source.readString來讀取伺服器的資料。需要注意的是該方法最後呼叫closeQuietly來關閉了當前請求的InputStream輸入流,所以string()方法只能呼叫一次,再次呼叫的話會報錯 public final String string() throws IOException { BufferedSource source = source(); try { Charset charset = Util.bomAwareCharset(source, charset()); return source.readString(charset); } finally { Util.closeQuietly(source); } } 複製程式碼
請求回撥,資料解析
拿到請求回撥的Response之後,再回到我們最開始呼叫的程式碼,
String url = "http://www.baidu.com"; //'1. 生成OkHttpClient例項物件' OkHttpClient okHttpClient = new OkHttpClient(); //'2. 生成Request物件' Request request = new Request.Builder() .url(url) .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"),"test content")) .build(); Call call = okHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { } @Override public void onResponse(@NonNull Call call, @NonNull Response response){ Headers responseHeaders = response.headers(); for (int i = 0, size = responseHeaders.size(); i < size; i++) { System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); } System.out.println(response.body().string()); } }); 複製程式碼
我們可以從Response物件中獲取所有我們所需要的資料,包括header,body.至此,okHttp的網路請求的大致流程已經分析完成,至於還有部分沒有講到的攔截器就不再本文綴述了.有興趣的可以看下文末的參考連線或者自行谷歌。
參考文章
ofollow,noindex">Okhttp之CallServerInterceptor簡單分析
Android技能樹 — 網路小結之 OkHttp超超超超超超超詳細解析
OkHttp3.0解析 —— 從原始碼的角度談談發起網路請求時做的操作
擴充套件閱讀
關於Http的請求頭 Expect:100-Continue
Expect請求頭部域,用於指出客戶端要求的特殊伺服器行為。若伺服器不能理解或者滿足 Expect域中的任何期望值,則必須返回417(Expectation Failed)狀態,或者如果請求 有其他問題,返回4xx狀態。 Expect:100-Continue握手的目的,是為了允許客戶端在傳送請求內容之前,判斷源伺服器是否願意接受 請求(基於請求頭部)。 Expect:100-Continue握手需謹慎使用,因為遇到不支援HTTP/1.1協議的伺服器或者代理時會引起問題。 複製程式碼
http2比起http1.x的有點主要體現在以下幾點
- 新的資料格式, http基於檔案協議解析,http2基於二進位制協議解析,
- 連線共享,多路複用(MultiPlexing)
- header壓縮,減小header的體積,使得請求更快
- 壓縮演算法從gzip改成HPACK的演算法,防破解
- 重置連線表現更好,http1.x取消請求的是直接斷開連線,http2則是斷開某個連線的stream流
- 更安全的SSL