OkHttp3原始碼分析[綜述]

yangxi_001發表於2017-06-19

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. 架構設計:生產者/消費者模式

相關文章