出處:
http://www.jianshu.com/p/aad5aacd79bf
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進行調試,可以看
綜述完成,如果需要更深入瞭解,可以按照目錄接着看下去