【精】OkHttp3源碼分析[綜述]

出處:

http://www.jianshu.com/p/aad5aacd79bf

OkHttp系列文章如下

本文主要是綜述與常識介紹


OkHttp是一個高效的Http客戶端,有如下的特點:

  1. 支持HTTP2/SPDY黑科技
  2. socket自動選擇最好路線,並支持自動重連
  3. 擁有自動維護的socket連接池,減少握手次數
  4. 擁有隊列線程池,輕鬆寫併發
  5. 擁有Interceptors輕鬆處理請求與響應(比如透明GZIP壓縮,LOGGING)
  6. 基於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進行緩存等待。

當任務執行完成後,調用finishedpromoteCalls()函數,手動移動緩存區(可以看出這裏是主動清理的,因此不會發生死鎖)


okhttp dispatcher

本部分詳細版在OkHttp3源碼分析[任務隊列]

Socket管理(StreamAllocation)

經過上一步的分配,我們現在需要進行連接了。我們目前有封裝好的Request,而進行HTTP連接需要進行Socket握手,Socket握手的前提是根據域名或代理確定Socket的ip與端口。這個環節主要講了http的握手過程與連接池的管理,分析的對象主要是StreamAllocation

1. 選擇路線與自動重連(RouteSelector)

此步驟用於獲取socket的ip與端口,各位請欣賞源碼next()的迷之縮進與遞歸,代碼進行了如下事情:

如果Proxynull:

  1. 在構造函數中設置代理Proxy.NO_PROXY
  2. 如果緩存中的lastInetSocketAddress爲空,就通過DNS(默認是Dns.SYSTEM,包裝了jdk自帶的lookup函數)查詢,並保存結果,注意結果是數組,即一個域名有多個IP,這就是自動重連的來源
  3. 如果還沒有查詢到就遞歸調用next查詢,直到查到爲止
  4. 一切next都沒有枚舉到,拋出NoSuchElementException,退出(這個幾乎見不到)

如果ProxyHTTP:

  1. 設置socket的ip爲代理地址的ip
  2. 設置socket的端口爲代理地址的端口
  3. 一切next都沒有枚舉到,拋出NoSuchElementException,退出
  1. HTTP代理是不安全的,本文附錄有介紹
  2. HTTP代理會幫你在遠程服務器進行DNS查詢
  3. 至於socket代理這裏就不分析了,它已經不屬於應用層了

2. 連接socket鏈路(RealConnection)

當地址,端口準備好了,就可以進行TCP連接了(也就是我們常說的TCP三次握手),步驟如下:

  1. 如果連接池中已經存在連接,就從中取出(get)RealConnection,如果沒有命中就進入下一步
  2. 根據選擇的路線(Route),調用Platform.get().connectSocket選擇當前平臺Runtime下最好的socket庫進行握手
  3. 將建立成功的RealConnection放入(put)連接池緩存
  4. 如果存在TLS,就根據SSL版本與證書進行安全握手
  5. 構造HttpStream並維護剛剛的socket連接,管道建立完成

關於Platform,DNS,Proxy詳細請看附錄

3. 釋放socket鏈路(release)

如果不再需要(比如通信完成,連接失敗等)此鏈路後,釋放連接(也就是TCP斷開的握手)

  1. 嘗試從緩存的連接池中刪除(remove)
  2. 如果沒有命中緩存,就直接調用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構建了簡陋的請求後,可能需要進行一些修飾,這時需要使用InterceptorsRequest進行進一步的拼裝了。

攔截器是okhttp中強大的流程裝置,它可以用來監控log,修改請求,修改結果,甚至是對用戶透明的GZIP壓縮。類似於腳本語言中的map操作。在okhttp中,內部維護了一個Interceptors的List,通過InterceptorChain進行多次攔截修改操作。


interceptors

請求的代碼如下,詳細代碼在這裏,源代碼中是自增遞歸(recursive)調用Chain.process(),直到interceptors().size()中的攔截器全部調用完。這裏代碼維護性估計看着頭大,大神們以後可能把它改成for等更簡單的循環,主要做了兩件事:

  1. 遞歸調用Interceptors,依次入棧對response進行處理
  2. 當全部遞歸出棧完成後,移交給網絡模塊(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文本

  1. 在okhttp中,通過RequestLineRequstHttpEngineHeader等參數進行序列化操作,也就是拼裝參數爲socketRaw數據。拼裝方法也比較暴力,直接按照RFC協議要求的格式進行concat輸出就實現了
  2. 通過sink寫入write到socket連接。

具體代碼在這裏

1.3. 獲得響應(readResponseHeaders/Body)

此步驟根據獲取到的Socket純文本,解析爲Response對象,我們可以看成是一個反序列化(通過http協議將Raw文本轉成對象)的過程:

攔截器的設計:

  1. 自定義網絡攔截器請求進行遞歸入棧
  2. 自定義網絡攔截器intercept中,調用NetworkInterceptorChain的proceed(request),進行真正的網絡請求(readNetworkResponse)
  3. 接自定義請求遞歸出棧

網絡讀取(readNetworkResponse)分析:

  1. 讀取Raw的第一行,並反序列化StatusLine對象
  2. Transfer-Encoding: chunked的模式傳輸並組裝Body

僞代碼如下:

(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)方法。

參考:DNSPod中HTTP DNS的實現

3. Platform

OkHttp的最底層是Socket,而不是URLConnection,它通過PlatformClass.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進行調試,可以看

  1. 抓包方法
  2. okhttp-logging-interceptor

綜述完成,如果需要更深入瞭解,可以按照目錄接着看下去

Refference

  1. Socket sample in C and Java
  2. https://imququ.com/post/optimize-tls-handshake.html
  3. http://www.williamlong.info/archives/2210.html
  4. http://www.cnblogs.com/zemliu/p/4263048.html
  5. http://www.cnblogs.com/ct2011/p/3997368.html
  6. 架構設計:生產者/消費者模式
發佈了47 篇原創文章 · 獲贊 13 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章