OkHttp3原始碼分析[綜述]
OkHttp系列文章如下
本文主要是綜述與常識介紹
OkHttp是一個高效的Http客戶端,有如下的特點:
- 支援HTTP2/SPDY黑科技
- socket自動選擇最好路線,並支援自動重連
- 擁有自動維護的socket連線池,減少握手次數
- 擁有佇列執行緒池,輕鬆寫併發
- 擁有Interceptors輕鬆處理請求與響應(比如透明GZIP壓縮,LOGGING)
- 基於Headers的快取策略
本文基於okhttp3
原始碼進行分析,邏輯錯誤或者不足請指出!
建議使用Idea作為分析工具。
主要物件
- Connections: 對JDK中的socket進行了引用計數封裝,用來控制socket連線
- Streams: 維護HTTP的流,用來對Requset/Response進行IO操作
- Calls: HTTP請求任務封裝
- StreamAllocation: 用來控制
Connections
/Streams
的資源分配與釋放
工作流程的概述
當我們用OkHttpClient.newCall(request)
進行execute/enenqueue
時,實際是將請求Call
放到了Dispatcher
中,okhttp使用Dispatcher進行執行緒分發,它有兩種方法,一個是普通的同步單執行緒;另一種是使用了佇列進行併發任務的分發(Dispatch)與回撥,我們下面主要分析第二種,也就是佇列這種情況,這也是okhttp能夠競爭過其它庫的核心功能之一
1. Dispatcher的結構
Dispatcher維護瞭如下變數,用於控制併發的請求
- maxRequests = 64: 最大併發請求數為64
- maxRequestsPerHost = 5: 每個主機最大請求數為5
- Dispatcher: 分發者,也就是生產者(預設在主執行緒)
- AsyncCall: 佇列中需要處理的Runnable(包裝了非同步回撥介面)
- ExecutorService:消費者池(也就是執行緒池)
- Deque<readyAsyncCalls>:快取(用陣列實現,可自動擴容,無大小限制)
- Deque<runningAsyncCalls>:正在執行的任務,僅僅是用來引用正在執行的任務以判斷併發量,注意它並不是消費者快取
根據生產者消費者模型的模型理論,當入隊(enqueue)請求時,如果滿足(runningRequests<64
&& runningRequestsPerHost<5)
,那麼就直接把AsyncCall
直接加到runningCalls
的佇列中,並線上程池中執行。如果消費者快取滿了,就放入readyAsyncCalls
進行快取等待。
當任務執行完成後,呼叫finished的promoteCalls()
函式,手動移動快取區(可以看出這裡是主動清理的,因此不會發生死鎖)
本部分詳細版在OkHttp3原始碼分析[任務佇列]
Socket管理(StreamAllocation)
經過上一步的分配,我們現在需要進行連線了。我們目前有封裝好的Request,而進行HTTP連線需要進行Socket握手,Socket握手的前提是根據域名或代理確定Socket的ip與埠。這個環節主要講了http的握手過程與連線池的管理,分析的物件主要是StreamAllocation
1. 選擇路線與自動重連(RouteSelector)
此步驟用於獲取socket的ip與埠,各位請欣賞原始碼中next()
的迷之縮排與遞迴,程式碼進行了如下事情:
如果Proxy
為null
:
- 在建構函式中設定代理為
Proxy.NO_PROXY
- 如果快取中的
lastInetSocketAddress
為空,就通過DNS(預設是Dns.SYSTEM
,包裝了jdk自帶的lookup函式)查詢,並儲存結果,注意結果是陣列,即一個域名有多個IP,這就是自動重連的來源 - 如果還沒有查詢到就遞迴呼叫next查詢,直到查到為止
- 一切next都沒有列舉到,丟擲
NoSuchElementException
,退出(這個幾乎見不到)
如果Proxy
為HTTP
:
- 設定socket的ip為代理地址的ip
- 設定socket的埠為代理地址的埠
- 一切next都沒有列舉到,丟擲
NoSuchElementException
,退出
- HTTP代理是不安全的,本文附錄有介紹
- HTTP代理會幫你在遠端伺服器進行DNS查詢
- 至於socket代理這裡就不分析了,它已經不屬於應用層了
2. 連線socket鏈路(RealConnection)
當地址,埠準備好了,就可以進行TCP連線了(也就是我們常說的TCP三次握手),步驟如下:
- 如果連線池中已經存在連線,就從中取出(get)RealConnection,如果沒有命中就進入下一步
- 根據選擇的路線(Route),呼叫
Platform.get().connectSocket
選擇當前平臺Runtime下最好的socket庫進行握手 - 將建立成功的
RealConnection
放入(put)連線池快取 - 如果存在TLS,就根據SSL版本與證照進行安全握手
- 構造HttpStream並維護剛剛的socket連線,管道建立完成
關於
Platform
,DNS
,Proxy
詳細請看附錄
3. 釋放socket鏈路(release)
如果不再需要(比如通訊完成,連線失敗等)此鏈路後,釋放連線(也就是TCP斷開的握手)
- 嘗試從快取的連線池中刪除(remove)
- 如果沒有命中快取,就直接呼叫jdk的socket關閉
本部分詳細版見: OkHttp3原始碼分析[複用連線池]
HTTP請求序列化/反序列化
本段主要分析從拼裝HTTP套接字到讀取的步驟,用垠神的話說,就是實現了一個Parser。分析的物件是HttpStream
介面,在HTTP/1.1下是Http1xStream
實現的。
1. 獲得HTTP流(httpStream)
以下為無快取,無多次302跳轉,網路良好,HTTP/1.1下的GET
訪問例項分析。
我們已經在上文的RealConnection
通過connectSocket()
構造HttpStream
物件並建立套接字連線(完成三次握手)
httpStream = connect();
在connect()
有非常重要的一步,它通過okio庫與遠端socket建立了I/O連線,為了更好的理解,我們可以把它看成管道
//source 用於獲取response
source = Okio.buffer(Okio.source(rawSocket));
//sink 用於write buffer 到server
sink = Okio.buffer(Okio.sink(rawSocket));
Okhttp的I/O使用的是Okio庫,它是java中最好用的I/O API,本人曾經寫NFC對這個用的就非常順手。
Buffer
: Buffer是可變位元組,類似於byte[]
,相當於傳輸介質source
: source是okio庫中的輸入元件,類似於inputstream,經常在下載中用到。它的重要方法是read(Buffer sink, long byteCount)
,從流中讀取資料。Sink
: sink是okio庫中的io輸出元件,類似於outputstream,經常用於寫到file/Socket,它的最重要方法是void write(Buffer source, long byteCount)
,寫資料到Buffer
中如果把連線看成管道,
->
為管道的方向,如下圖,這裡借鑑了go語言的描述
Sink -> Socket/File Source <- Socket/File
2. 拼裝Raw請求與Headers(writeRequestHeaders)
我們通過Request.Builder
構建了簡陋的請求後,可能需要進行一些修飾,這時需要使用Interceptors
對Request
進行進一步的拼裝了。
攔截器是okhttp中強大的流程裝置,它可以用來監控log,修改請求,修改結果,甚至是對使用者透明的GZIP壓縮。類似於指令碼語言中的map操作。在okhttp中,內部維護了一個Interceptors
的List,通過InterceptorChain
進行多次攔截修改操作。
請求的程式碼如下,詳細程式碼在這裡,原始碼中是自增遞迴(recursive)呼叫Chain.process()
,直到interceptors().size()
中的攔截器全部呼叫完。這裡程式碼維護性估計看著頭大,大神們以後可能把它改成for等更簡單的迴圈,主要做了兩件事:
- 遞迴呼叫Interceptors,依次入棧對response進行處理
- 當全部遞迴出棧完成後,移交給網路模組(getResponse)
if (index < client.interceptors().size()) {
Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
Interceptor interceptor = client.interceptors().get(index);
//遞迴呼叫Chain.process()
Response interceptedResponse = interceptor.intercept(chain);
if (interceptedResponse == null) {
throw new NullPointerException("application interceptor " + interceptor
+ " returned null");
}
return interceptedResponse;
}
// No more interceptors. Do HTTP.
return getResponse(request, forWebSocket);
}
接下來是正式的網路請求getResponse()
,此步驟通過http協議規範
將物件中的資料資訊
序列化為Raw文字
:
- 在okhttp中,通過
RequestLine
,Requst
,HttpEngine
,Header
等引數進行序列化操作,也就是拼裝引數為socketRaw資料。拼裝方法也比較暴力,直接按照RFC協議要求的格式進行concat輸出就實現了 - 通過sink寫入
write
到socket連線。
具體程式碼在這裡。
1.3. 獲得響應(readResponseHeaders/Body)
此步驟根據獲取到的Socket純文字
,解析為Response物件
,我們可以看成是一個反序列化(通過http協議將Raw文字轉成物件)的過程:
攔截器的設計:
自定義網路攔截器
請求進行遞迴入棧- 在
自定義網路攔截器
的intercept
中,呼叫NetworkInterceptorChain
的proceed(request),進行真正的網路請求(readNetworkResponse) - 接自定義請求遞迴出棧
網路讀取(readNetworkResponse)分析:
虛擬碼如下:
(RawData <- RemoteChannel(www.xx.com, 80))//讀取遠端的Raw
map(func NetworkInterceptorChains())//預處理
//這裡的source引用了HttpEngine,並重寫了read方法
.map(func getTransferStream(){})
//根據source拼裝body物件
.map(func RealResponseBody(){})
接下來進行釋放socket連線,上文已經介紹過了。現在我們就獲得到response
物件,可以進行進一步的Gson等操作了。
附錄
以下為一些計算機常識
1. Proxy
代理,也就是有個中間伺服器幫助你訪問不存在的網站,okhttp中使用jdk自帶的代理
You ---- Proxy ----- Server
HTTP代理的本質是改Header資訊,當你訪問HTTP/HTTPS服務時,本質是明文向跳板傳送如下raw,遠端伺服器幫你完成dns與請求操作,比如HTTPS請求原始碼就詳細的解釋了傳送的內容是非加密的,下面是我實際抓包的內容
//HTTP 請求
GET HTTP://www.qq.com HTTP/1.1
//HTTPS 請求
CONNECT github.com:443 HTTP/1.1
上面的抓包過程,廉價的民用上網行為管理交換機就可以把你記錄的一清二楚,所以慎用HTTP代理或者儘量使用HTTPS代理,它是“不安全”的。
2. DNS
DNS也就是域名到ip的對映(mapping
)操作,使用者向DNS伺服器的53埠傳送udp包後,會返回域名對應的地址,當然傳送udp的細節對使用者是透明的,使用者直接呼叫jdk就可以了。我們先試下Unix下的查詢
$ host baidu.com
baidu.com has address 111.13.101.208
baidu.com has address 123.125.114.144
.....
在OkHttp中,提供了DNS介面,預設是使用Dns.SYSTEM
,它包裝了java原生socket包中的InetAddress.getAllByName(hostname)
方法。
3. Platform
OkHttp的最底層是Socket,而不是URLConnection,它通過Platform
的Class.forName()
反射獲得當前Runtime使用的socket庫,呼叫棧如下(瞭解即可)
okhttp//實現HTTP協議
framwork//JRE,實現JDK中Socket封裝
jvm//JDK的實現,本質對libc標準庫的native封裝
bionic//android下的libc標準庫
systemcall//使用者態切換入核心
kernel//實現下協議棧(L4,L3)與網路驅動(一般是L2,L1)
如果你想用藍芽硬體中Socket的進行HTTP協議開發,嘗試重寫這個類。
另外,再說一句廢話,自從Android4.4以來,URLConnection在fram的實現也是使用了okhttp
OkHttp支援非常多平臺下的Socket庫實現,包括
Android, JettyBootPlatform
等都是支援的,具體的平臺支援可以看這裡
4. 如何除錯HTTP傳送的內容
如果需要對OkHttp進行除錯,可以看
綜述完成,如果需要更深入瞭解,可以按照目錄接著看下去
Refference
相關文章
- OkHttp3原始碼分析[DiskLruCache]HTTP原始碼
- okhttp3 攔截器原始碼分析HTTP原始碼
- OkHttp3原始碼分析[快取策略]HTTP原始碼快取
- Netty原始碼解析4-Handler綜述Netty原始碼
- OkHttp3原始碼分析[任務佇列]HTTP原始碼佇列
- 【OkHttp3原始碼分析】(一)Request的executeHTTP原始碼
- 【OkHttp3原始碼分析】(二)Request的enqueueHTTP原始碼ENQ
- OkHttp3原始碼分析[複用連線池]HTTP原始碼
- NGFS:金融機構環境風險分析綜述
- Spring綜述Spring
- API安全綜述API
- butterknife原始碼簡單分析&原理簡述原始碼
- 超解析度分析(一)--傳統方案綜述
- Spark RPC框架原始碼分析(一)簡述SparkRPC框架原始碼
- 視覺SLAM綜述視覺SLAM
- Spring Bean 綜述SpringBean
- JavaScript模板引擎綜述JavaScript
- Java集合框架綜述Java框架
- pl/sql reference綜述SQL
- (譯)haslayout 綜述(一)
- 超解析度分析(二)--深度學習方案綜述深度學習
- Lotus Domino/Notes Toolkits綜述(七) 分析比較 (轉)
- OkHttp3原始碼解析(一)之請求流程HTTP原始碼
- 李航「機器學習」最全綜述機器學習
- PostgreSQL掃描方法綜述SQL
- 目標檢測綜述
- 損失函式綜述函式
- 對話系統綜述
- Image Caption任務綜述APT
- 網路廣告研究綜述
- RocketMQ綜述(未完成)MQ
- 評價物件抽取綜述物件
- SQL效能調優綜述SQL
- Java 執行緒綜述Java執行緒
- 敏捷開發方法綜述敏捷
- 【JUC】JUC鎖框架綜述框架
- GNOME 技術綜述(轉)
- 文章綜述:基於結構法分析的故障檢測