初識OkHttp(一)

OKHttp

Android爲我們提供了兩種HTTP交互的方式:HttpURLConnection 和 Apache HTTP Client,雖然兩者都支持HTTPS,流的上傳和下載,配置超時,IPv6和連接池,已足夠滿足我們各種HTTP請求的需求。但更高效的使用HTTP可以讓您的應用運行更快、更節省流量。而OkHttp庫就是爲此而生。

OkHttp是一個高效的HTTP庫:

  • 支持 SPDY ,共享同一個Socket來處理同一個服務器的所有請求
  • 如果SPDY不可用,則通過連接池來減少請求延時
  • 無縫的支持GZIP來減少數據流量
  • 緩存響應數據來減少重複的網絡請求

會從很多常用的連接問題中自動恢復。如果您的服務器配置了多個IP地址,當第一個IP連接失敗的時候,OkHttp會自動嘗試下一個IP。OkHttp還處理了代理服務器問題和SSL握手失敗問題。

使用 OkHttp 無需重寫您程序中的網絡代碼。OkHttp實現了幾乎和java.net.HttpURLConnection一樣的API。如果您用了 Apache HttpClient,則OkHttp也提供了一個對應的okhttp-apache 模塊。

OKHttp源碼位置https://github.com/square/okhttp

使用

簡單使用代碼

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://api.github.com/repos/square/okhttp/issues")
        .header("User-Agent", "OkHttp Headers.java")
        .addHeader("Accept", "application/json; q=0.5")
        .addHeader("Accept", "application/vnd.github.v3+json")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println("Server: " + response.header("Server"));
    System.out.println("Date: " + response.header("Date"));
    System.out.println("Vary: " + response.headers("Vary"));
}

在這裏使用不做詳細介紹,推薦一篇關於OKHttp的詳細使用教程,下面轉入源碼的分析。

總體設計 OKHttp總體設計圖

上面是OKHttp總體設計圖,主要是通過Diapatcher不斷從RequestQueue中取出請求(Call),根據是否已緩存調用Cache或Network這兩類數據獲取接口之一,從內存緩存或是服務器取得請求的數據。該引擎有同步和異步請求,同步請求通過Call.execute()直接返回當前的Response,而異步請求會把當前的請求Call.enqueue添加(AsyncCall)到請求隊列中,並通過回調(Callback)的方式來獲取最後結果。

接下來會介紹一些比較重要的類,另外一些基礎IO方面的內容主要來之iohttp這個包。這些類的解釋大部分來至文檔介紹本身,所以在此不會翻譯成中文,本人覺得英語原文更能準確表達它自身的作用。

OKHttp中重要的類

1.Route.java
The concrete route used by a connection to reach an abstract origin server.
When creating a connection the client has many options:

譯:用來創建一個到達抽象源服務器的連接的具體路由方式,當創建一個連接的時候,客戶端有很多選擇:

  • HTTP proxy: a proxy server may be explicitly configured for the client. Otherwise the {@linkplain java.net.ProxySelector proxy selector} is used. It may return multiple proxies to attempt.
  • IP address: whether connecting directly to an origin server or a proxy, opening a socket requires an IP address. The DNS server may return multiple IP addresses to attempt.
  • TLS configuration: which cipher suites and TLS versions to attempt with the HTTPS connection.
  • HTTP代理:可以爲客戶端顯示的配置代理服務器,否則將會使用{@linkplain java.net.ProxySelector proxy selector},它可能返回多個代理選項進行嘗試。
  • IP地址:無論是直接連接到原始服務器還是代理服務器,打開套接字都需要一個IP地址。 DNS服務器可能會返回多個IP地址嘗試。
  • TLS配置:密碼套件和TLS版本嘗試使用HTTPS連接。

Each route is a specific selection of these options.
其實就是對地址的一個封裝類,但是很重要。

2.Platform.java
Access to platform-specific features.

  • Server name indication (SNI): Supported on Android 2.3+.
  • Session Tickets: Supported on Android 2.3+.
  • Android Traffic Stats (Socket Tagging): Supported on Android 4.0+.
  • ALPN (Application Layer Protocol Negotiation): Supported on Android 5.0+. The APIs were present in Android 4.4, but that implementation was unstable.

Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
這個類主要是做平臺適應性,針對Android2.3到5.0後的網絡請求的適配支持。同時,在這個類中能看到針對不同平臺,通過java反射不同的class是不一樣的。

3.Connnection.java
The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be used for multiple HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy.

譯:HTTP,HTTPS或HTTPS + SPDY連接的套接字和流。 可用於多個HTTP請求/響應交換。 連接可能直接到源服務器或通過代理。

Typically instances of this class are created, connected and exercised automatically by the HTTP client. Applications may use this class to monitor HTTP connections as members of a ConnectionPool.

譯:通常該類的實例由HTTP客戶端自動創建,連接和運行。 應用程序可以使用此類來監視作爲ConnectionPool成員的HTTP連接。

Do not confuse this class with the misnamed HttpURLConnection, which isn’t so much a connection as a single request/response exchange.

譯:不要將這個類與HttpURLConnection混淆,這不是一個單一的請求/響應交換的連接。

Modern TLS
There are tradeoffs when selecting which options to include when negotiating a secure connection to a remote host. Newer TLS options are quite useful:

譯:現代TLS,當選擇在協商與遠程主機的安全連接時要包括哪些選項時,有權衡。 較新的TLS選項非常有用:

  • Server Name Indication (SNI) enables one IP address to negotiate secure connections for multiple domain names.
  • SNI有效:一個IP地址用來協調多個域名的安全連接
  • Application Layer Protocol Negotiation (ALPN) enables the HTTPS port (443) to be used for different HTTP and SPDY protocols.
  • ALPN有效:443HTTPS端口可用於不同的HTTP和SPDY協議

Unfortunately, older HTTPS servers refuse to connect when such options are presented. Rather than avoiding these options entirely, this class allows a connection to be attempted with modern options and then retried without them should the attempt fail.

譯:不幸的是,當提供這些選項時,較舊的HTTPS服務器拒絕連接。 爲了不完全避免這些選項,這個類允許使用現代選項嘗試連接,並且具有失敗重試機制。

4.ConnnectionPool.java
Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP requests that share the same Address may share a Connection. This class implements the policy of which connections to keep open for future use.
The system-wide default uses system properties for tuning parameters:

譯:管理HTTP和SPDY連接的重用,以減少網絡延遲。 共享相同地址的HTTP請求可能共享一個連接。 該類實現了保持開放以供將來使用的連接的策略。系統默認使用系統屬性調整參數:

  • http.keepAlive true if HTTP and SPDY connections should be pooled at all. Default is true.
  • 如果Http和SPDY連接應該連接(彙集)在一起,則http.keepAlive屬性應爲true,默認爲true
  • http.maxConnections maximum number of idle connections to each to keep in the pool. Default is 5.
  • http.maxConnections 是連接池中保留的最大空閒連接數,默認爲5
  • http.keepAliveDuration Time in milliseconds to keep the connection alive in the pool before closing it. Default is 5 minutes. This property isn’t used by HttpURLConnection.
  • http.keepAliveDuration在關閉它之前保持連接在池中的時間(以毫秒爲單位)。 默認爲5分鐘。 HttpURLConnection不使用此屬性。

The default instance doesn’t adjust its configuration as system properties are changed. This assumes that the applications that set these parameters do so before making HTTP connections, and that this class is initialized lazily.

默認實例不會調整隨着系統屬性的更改而更改。 這假設在進行HTTP連接之前設置這些參數的應用程序是這樣做的,並且該類被懶惰地初始化。

5.Request.java
An HTTP request. Instances of this class are immutable if their body is null or itself immutable.(Builder模式)

譯:HTTP請求,如果它們的主體是空的或者它本身是不可變的,這個類的實例是不可變的(Builder模式)

6.Response.java
An HTTP response. Instances of this class are not immutable: the response body is a one-shot value that may be consumed only once. All other properties are immutable.

譯:HTTP響應。 這個類的實例不是不可變的:響應體是一個可以只用一次**的一次性值。 所有其他屬性都是不可變的。

7.Call.java
A call is a request that has been prepared for execution. A call can be canceled. As this object represents a single request/response pair (stream), it cannot be executed twice.

譯:一個Call就是一個已經準備執行的請求,它可以被取消,這個對象表示單獨的請求或者響應對,不能被執行兩次以上。

8.Dispatcher.java
Policy on when async requests are executed.

Each dispatcher uses an ExecutorService to run calls internally. If you supply your own executor, it should be able to run configured maximum number of calls concurrently.

譯:執行異步請求時的策略,每個調度程序使用ExecutorService來內部運行Call,如果你自定義自己的執行器,它應該能夠保證同時運行已配置的最大併發Call數。

9.HttpEngine.java
Handles a single HTTP request/response pair. Each HTTP engine follows this
lifecycle:

譯:處理單個Http請求/響應對,每一個Http引擎都遵循這個生命週期:

  • It is created.
  • 創建
  • The HTTP request message is sent with sendRequest(). Once the request is sent it is an error to modify the request headers. After sendRequest() has been called the request body can be written to if it exists.
  • 使用sendRequest發送一個Http請求信息,一旦這個請求被髮送,修改這個請求的header將會是一個error,在sendRequest被調用之後,如果請求體存在則可以重新寫入。
  • The HTTP response message is read with readResponse(). After the response has been read the response headers and body can be read. All responses have a response body input stream, though in some instances this stream is empty.
  • 使用readResponse 讀取HTTP響應消息。 響應已被讀取後,可以讀取響應標題和正文。 所有響應都有響應體輸入流,但在某些情況下,此流爲空。

The request and response may be served by the HTTP response cache, by the network, or by both in the event of a conditional GET.

譯:請求和響應可以由http響應緩存,或者網絡,或者兩者都有,在Get情況下提供服務。

10.Internal.java
Escalate internal APIs in {@code com.squareup.okhttp} so they can be used from OkHttp’s implementation packages. The only implementation of this interface is in {@link com.squareup.okhttp.OkHttpClient}.

譯:在{@code com.squareup.okhttp}中升級內部API,以便它們可以從OkHttp的實現包中使用。 此接口的唯一實現是在{@link com.squareup.okhttp.OkHttpClient}中。

11.Cache.java
Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and bandwidth.

緩存HTTP和HTTPS對文件系統的響應,從而可以重用,從而節省時間和帶寬。

Cache Optimization
To measure cache effectiveness, this class tracks three statistics:

譯:爲了衡量緩存的有效性,該類跟蹤三個統計信息:

  • Request Count: the number of HTTP requests issued since this cache was created.
  • 請求計數:創建此緩存後發出的Http請求數
  • Network Count: the number of those requests that required network use.
  • 網絡計數:需要網絡使用的請求數
  • Hit Count: the number of those requests whose responses were served by the cache.
  • 命中計數:由緩存提供響應的請求數

Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of the response, the client will issue a conditional GET. The server will then send either the updated response if it has changed, or a short ‘not modified’ response if the client’s copy is still valid. Such responses increment both the network count and hit count.
The best way to improve the cache hit rate is by configuring the web server to return cacheable responses. Although this client honors all HTTP/1.1 (RFC 2068) cache headers, it doesn’t cache partial responses.

譯:有時,請求將導致條件緩存命中。 如果緩存包含響應的陳舊副本,則客戶端將發出條件GET。 如果客戶端的副本仍然有效,服務器將發送更新的響應(如果已更改)或簡短的“未修改”響應。 此類響應會增加網絡計數和命中次數。
提高緩存命中率的最佳方法是配置Web服務器以返回可緩存的響應。 雖然這個客戶端尊重所有[HTTP / 1.1 \(RFC 2068 \)](http://www.ietf.org/rfc/rfc2616.txt)緩存頭,但它不緩存部分響應。

Force a Network Response
In some situations, such as after a user clicks a ‘refresh’ button, it may be necessary to skip the cache, and fetch data directly from the server. To force a full refresh, add the {@code no-cache} directive:

譯:在某些情況下,例如在用戶單擊“刷新”按鈕之後,可能需要跳過緩存並直接從服務器獲取數據。 要強制完全刷新,請添加{@code no-cache}指令:

connection.addRequestProperty("Cache-Control", "no-cache")

If it is only necessary to force a cached response to be validated by the server, use the more efficient {@code max-age=0} instead:

譯:如果僅需要強制緩存的響應由服務器驗證,則使用更有效的{@code max-age = 0}:

connection.addRequestProperty("Cache-Control", "max-age=0");

Force a Cache Response
Sometimes you’ll want to show resources if they are available immediately, but not otherwise. This can be used so your application can show something while waiting for the latest data to be downloaded. To restrict a request to locally-cached resources, add the {@code only-if-cached} directive:

譯:有時,如果可以的話,你想立即顯示資源。 這也可以,但是你的應用程序必須在等待最新的數據下載時顯示一些東西。 要限制對本地緩存資源的請求,請添加{@code only-if-cached}指令:

try {
    connection.addRequestProperty("Cache-Control", "only-if-cached");
    InputStream cached = connection.getInputStream();
    // the resource was cached! show it
    } catch (FileNotFoundException e) {
    // the resource was not cached
    }

This technique works even better in situations where a stale response is better than no response. To permit stale cached responses, use the {@code max-stale} directive with the maximum staleness in seconds:

譯:這種技術在陳舊的反應比沒有反應更好的情況下運行得更好。 要允許陳舊的緩存響應,請使用{@code max-stale}指令,單位爲秒:

int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);

12.OkHttpClient.java
Configures and creates HTTP connections. Most applications can use a single OkHttpClient for all of their HTTP requests - benefiting from a shared response cache, thread pool, connection re-use, etc.

譯:配置和創建HTTP連接。 大多數應用程序可以使用單個OkHttpClient來處理所有的HTTP請求 - 受益於共享響應緩存,線程池,連接重用等。

Instances of OkHttpClient are intended to be fully configured before they’re shared - once shared they should be treated as immutable and can safely be used to concurrently open new connections. If required, threads can call clone to make a shallow copy of the OkHttpClient that can be safely modified with further configuration changes.

譯:OkHttpClient的實例旨在在共享之前完全配置 - 一旦共享,它們應被視爲不可變的,並且可以安全地用於同時打開新的連接。 如果需要,線程可以調用* clone *來創建OkHttpClient的淺層副本,可以通過進一步的配置更改安全地進行修改。

請求流程圖

下面是關於OKHttp的請求流程圖
OKHttp的請求流程圖

詳細類關係圖

由於整個設計類圖比較大,所以本人將從核心入口client、cache、interceptor、網絡配置、連接池、平臺適配性…這些方面來逐一進行分析源代碼的設計。
下面是核心入口OkHttpClient的類設計圖
OkHttpClient的類設計圖
從OkHttpClient類的整體設計來看,它採用門面模式來。client知曉子模塊的所有配置以及提供需要的參數。client會將所有從客戶端發來的請求委派到相應的子系統去。
在該系統中,有多個子系統、類或者類的集合。例如上面的cache、連接以及連接池相關類的集合、網絡配置相關類集合等等。每個子系統都可以被客戶端直接調用,或者被門面角色調用。子系統並不知道門面的存在,對於子系統而言,門面僅僅是另外一個客戶端而已。同時,OkHttpClient可以看作是整個框架的上下文。
通過類圖,其實很明顯反應了該框架的幾大核心子系統;路由、連接協議、攔截器、代理、安全性認證、連接池以及網絡適配。從client大大降低了開發者使用難度。同時非常明瞭的展示了該框架在所有需要的配置以及獲取結果的方式。

在接下來的幾個Section中將會結合子模塊核心類的設計,從該框架的整體特性上來分析這些模塊是如何實現各自功能。以及各個模塊之間是如何相互配合來完成客戶端各種複雜請求。

同步與異步的實現

在發起請求時,整個框架主要通過Call來封裝每一次的請求。同時Call持有OkHttpClient和一份HttpEngine。而每一次的同步或者異步請求都會有Dispatcher的參與,不同的是:

  • 同步
    Dispatcher會在同步執行任務隊列中記錄當前被執行過得任務Call,同時在當前線程中去執行Call的getResponseWithInterceptorChain()方法,直接獲取當前的返回數據Response;
  • 異步
    首先來說一下Dispatcher,Dispatcher內部實現了懶加載無邊界限制的線程池方式,同時該線程池採用了SynchronousQueue這種阻塞隊列。SynchronousQueue每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作。因此此隊列內部其 實沒有任何一個元素,或者說容量是0,嚴格說並不是一種容器。由於隊列沒有容量,因此不能調用peek操作,因爲只有移除元素時纔有元素。顯然這是一種快速傳遞元素的方式,也就是說在這種情況下元素總是以最快的方式從插入者(生產者)傳遞給移除者(消費者),這在多任務隊列中是最快處理任務的方式。對於高頻繁請求的場景,無疑是最適合的。
    異步執行是通過Call.enqueue(Callback responseCallback)來執行,在Dispatcher中添加一個封裝了Callback的Call的匿名內部類Runnable來執行當前的Call。這裏一定要注意的地方這個AsyncCall是Call的匿名內部類。AsyncCall的execute方法仍然會回調到Call的getResponseWithInterceptorChain方法來完成請求,同時將返回數據或者狀態通過Callback來完成。

接下來繼續講講Call的getResponseWithInterceptorChain()方法,這裏邊重點說一下攔截器鏈條的實現以及作用。

攔截器有什麼作用

先來看看Interceptor本身的文檔解釋:觀察,修改以及可能短路的請求輸出和響應請求的回來。通常情況下攔截器用來添加,移除或者轉換請求或者回應的頭部信息。
攔截器接口中有intercept(Chain chain)方法,同時返回Response。所謂攔截器更像是AOP(面向切面編程)設計的一種實現。下面來看一個okhttp源碼中的一個引導例子來說明攔截器的作用。

public final class LoggingInterceptors {
  private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName());
  private final OkHttpClient client = new OkHttpClient();

  public LoggingInterceptors() {
    client.networkInterceptors().add(new Interceptor() {
      @Override public Response intercept(Chain chain) throws IOException {
        long t1 = System.nanoTime();
        Request request = chain.request();
        logger.info(String.format("Sending request %s on %s%n%s",
            request.url(), chain.connection(), request.headers()));
        Response response = chain.proceed(request);

        long t2 = System.nanoTime();
        logger.info(String.format("Received response for %s in %.1fms%n%s",
            request.url(), (t2 - t1) / 1e6d, response.headers()));
        return response;
      }
    });
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    Response response = client.newCall(request).execute();
    response.body().close();
  }

  public static void main(String... args) throws Exception {
    new LoggingInterceptors().run();
  }
}

返回信息

三月 19, 2015 2:11:29 下午 com.squareup.okhttp.recipes.LoggingInterceptors$1 intercept
信息: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA protocol=http/1.1}
Host: publicobject.com 
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: 

三月 19, 2015 2:11:30 下午 com.squareup.okhttp.recipes.LoggingInterceptors$1 intercept
信息: Received response for https://publicobject.com/helloworld.txt in 275.9ms
Server: nginx/1.4.6 (Ubuntu)
Date: Thu, 19 Mar 2015 06:08:50 GMT
Content-Type: text/plain
Content-Length: 1759
Last-Modified: Tue, 27 May 2014 02:35:47 GMT
Connection: keep-alive
ETag: "5383fa03-6df"
Accept-Ranges: bytes
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1426745489953
OkHttp-Received-Millis: 1426745490198

從這裏的執行來看,攔截器主要是針對Request和Response的切面處理。
那再來看看源碼到底在什麼位置做的這個處理呢?爲了更加直觀的反應執行流程,本人截圖了一下執行堆棧
OKHttp總體設計圖
另外如果還有同學對Interceptor比較敢興趣的可以去源碼的simples模塊看看GzipRequestInterceptor.java針對HTTP request body的一個zip壓縮。

在這裏再多說一下關於Call這個類的作用,在Call中持有一個HttpEngine。每一個不同的Call都有自己獨立的HttpEngine。在HttpEngine中主要是各種鏈路和地址的選擇,還有一個Transport比較重要

緩存策略

在OkHttpClient內部暴露了有Cache和InternalCache。而InternalCache不應該手動去創建,所以作爲開發使用者來說,一般用法如下:

public final class CacheResponse {
  private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName());
  private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    logger.info(String.format("Cache file path %s",cacheDirectory.getAbsoluteFile()));
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    client = new OkHttpClient();
    client.setCache(cache);
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    Response response1 = client.newCall(request).execute();
    if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

    String response1Body = response1.body().string();
    System.out.println("Response 1 response:          " + response1);
    System.out.println("Response 1 cache response:    " + response1.cacheResponse());
    System.out.println("Response 1 network response:  " + response1.networkResponse());

    Response response2 = client.newCall(request).execute();
    if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

    String response2Body = response2.body().string();
    System.out.println("Response 2 response:          " + response2);
    System.out.println("Response 2 cache response:    " + response2.cacheResponse());
    System.out.println("Response 2 network response:  " + response2.networkResponse());

    System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
  }

  public static void main(String... args) throws Exception {
    new CacheResponse(new File("CacheResponse.tmp")).run();
  }
}

返回信息

信息: Cache file path D:\work\workspaces\workspaces_intellij\workspace_opensource\okhttp\CacheResponse.tmp
Response 1 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 cache response:    null
Response 1 network response:  Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 cache response:    Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 network response:  null
Response 2 equals Response 1? true

Process finished with exit code 0

上邊這一段代碼同樣來之於simple代碼CacheResponse.java,反饋回來的數據重點看一下緩存日誌。第一次是來至網絡數據,第二次來至緩存。
那在這一節重點說一下整個框架的緩存策略如何實現的。

在這裏繼續使用上一節中講到的運行堆棧圖。從Call.getResponse(Request request, boolean forWebSocket)執行Engine.sendRequest()和Engine.readResponse()來詳細說明一下。

sendRequest()
此方法是對可能的Response資源進行一個預判,如果需要就會開啓一個socket來獲取資源。如果請求存在那麼就會爲當前request添加請求頭部並且準備開始寫入request body。

public void sendRequest() throws IOException {
        if (cacheStrategy != null) {
            return; // Already sent.
        }
        if (transport != null) {
            throw new IllegalStateException();
        }

        //填充默認的請求頭部和事務。
        Request request = networkRequest(userRequest);

        //下面一行很重要,這個方法會去獲取client中的Cache。同時Cache在初始化的時候會去讀取緩存目錄中關於曾經請求過的所有信息。
        InternalCache responseCache = Internal.instance.internalCache(client);
        Response cacheCandidate = responseCache != null? responseCache.get(request): null;

        long now = System.currentTimeMillis();
        //緩存策略中的各種配置的封裝
        cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
        networkRequest = cacheStrategy.networkRequest;
        cacheResponse = cacheStrategy.cacheResponse;

        if (responseCache != null) {
            //記錄當前請求是來至網絡還是命中了緩存
            responseCache.trackResponse(cacheStrategy);
        }

        if (cacheCandidate != null && cacheResponse == null) {
            closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
        }

        if (networkRequest != null) {
            // Open a connection unless we inherited one from a redirect.
            if (connection == null) {
                //連接到服務器、重定向服務器或者通過一個代理Connect to the origin server either directly or via a proxy.
                connect();
            }
            //通過Connection創建一個SpdyTransport或者HttpTransport
            transport = Internal.instance.newTransport(connection, this);
            ...
        } else {
            ...
        }
    }

readResponse()
此方法發起刷新請求頭部和請求體,解析HTTP迴應頭部,並且如果HTTP迴應體存在的話就開始讀取當前迴應頭。在這裏有發起返回存入緩存系統,也有返回和緩存系統進行一個對比的過程。

public void readResponse() throws IOException {
        ...
        Response networkResponse;

        if (forWebSocket) {
            ...
        } else if (!callerWritesRequestBody) {
            // 這裏主要是看當前的請求body,其實真正請求是在這裏發生的。
            // 在readNetworkResponse()方法中執行transport.finishRequest()
            // 這裏可以看一下該方法內部會調用到HttpConnection.flush()方法
            networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);
        } else {
            ...
        }
        //對Response頭部事務存入事務管理中
        receiveHeaders(networkResponse.headers());

        // If we have a cache response too, then we're doing a conditional get.
        if (cacheResponse != null) {
            //檢查緩存是否可用,如果可用。那麼就用當前緩存的Response,關閉網絡連接,釋放連接。
            if (validate(cacheResponse, networkResponse)) {
                userResponse = cacheResponse.newBuilder()
                        .request(userRequest)
                        .priorResponse(stripBody(priorResponse))
                        .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                        .cacheResponse(stripBody(cacheResponse))
                        .networkResponse(stripBody(networkResponse))
                        .build();
                networkResponse.body().close();
                releaseConnection();

                // Update the cache after combining headers but before stripping the
                // Content-Encoding header (as performed by initContentStream()).
                // 更新緩存以及緩存命中情況
                InternalCache responseCache = Internal.instance.internalCache(client);
                responseCache.trackConditionalCacheHit();
                responseCache.update(cacheResponse, stripBody(userResponse));
                // unzip解壓縮response
                userResponse = unzip(userResponse);
                return;
            } else {
                closeQuietly(cacheResponse.body());
            }
        }

        userResponse = networkResponse.newBuilder()
                .request(userRequest)
                .priorResponse(stripBody(priorResponse))
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();

        //發起緩存的地方
        if (hasBody(userResponse)) {
            maybeCache();
            userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
        }
    }

HTTP連接的實現方式(說說連接池)

外部網絡請求的入口都是通過Transport接口來完成。該類採用了橋接模式將HttpEngine和HttpConnection來連接起來。因爲HttpEngine只是一個邏輯處理器,同時它也充當了請求配置的提供引擎,而HttpConnection是對底層處理Connection的封裝。

OK現在重點轉移到HttpConnection(一個用於發送HTTP/1.1信息的socket連接)這裏。主要有如下的生命週期:

1、發送請求頭;
2、打開一個sink(io中有固定長度的或者塊結構chunked方式的)去寫入請求body;
3、寫入並且關閉sink;
4、讀取Response頭部;
5、打開一個source(對應到第2步的sink方式)去讀取Response的body;
6、讀取並關閉source;

下邊看一張關於連接執行的時序圖:
OKHttp連接執行時序圖
這張圖畫得比較簡單,詳細的過程以及連接池的使用下面大致說明一下:

1、連接池是暴露在client下的,它貫穿了Transport、HttpEngine、Connection、HttpConnection和SpdyConnection;在這裏目前默認討論HttpConnection;
2、ConnectionPool有兩個構建參數是maxIdleConnections(最大空閒連接數)和keepAliveDurationNs(存活時間),另外連接池默認的線程池採用了Single的模式(源碼解釋是:一個用於清理過期的多個連接的後臺線程,最多一個單線程去運行每一個連接池);
3、發起請求是在Connection.connect()這裏,實際執行是在HttpConnection.flush()這裏進行一個刷入。這裏重點應該關注一下sink和source,他們創建的默認方式都是依託於同一個socket:
this.source = Okio.buffer(Okio.source(socket));
this.sink = Okio.buffer(Okio.sink(socket));
如果再進一步看一下io的源碼就能看到:
Source source = source((InputStream)socket.getInputStream(), (Timeout)timeout);
Sink sink = sink((OutputStream)socket.getOutputStream(), (Timeout)timeout);
這下我想大家都應該明白這裏到底是真麼回事兒了吧?
相關的sink和source還有相應的細分,如果有興趣的朋友可以繼續深入看一下,這裏就不再深入了。不然真的說不完了。。。

其實連接池這裏還是有很多值得細看的地方,由於時間有限,到這裏已經花了很多時間搞這事兒了。。。

重連機制

這裏重點說說連接鏈路的相關事情。說說自動重連到底是如何實現的。
照樣先來看看下面的這個自動重連機制的實現方式時序圖
OKHttp重連執行時序圖

同時回到Call.getResponse()方法說起

Response getResponse(Request request, boolean forWebSocket) throws IOException {
    ...
    while (true) { // 自動重連機制的循環處理
      if (canceled) {
        engine.releaseConnection();
        return null;
      }

      try {
        engine.sendRequest();
        engine.readResponse();
      } catch (IOException e) {
        //如果上一次連接異常,那麼當前連接進行一個恢復。
        HttpEngine retryEngine = engine.recover(e, null);
        if (retryEngine != null) {
          engine = retryEngine;
          continue;//如果恢復成功,那麼繼續重新請求
        }

        // Give up; recovery is not possible.如果不行,那麼就中斷了
        throw e;
      }

      Response response = engine.getResponse();
      Request followUp = engine.followUpRequest();
      ...
    }
  }

相信這一段代碼能讓同學們清晰的看到自動重連機制的實現方式,那麼我們來看看詳細的步驟:

1、HttpEngine.recover()的實現方式是通過檢測RouteSelector是否還有更多的routes可以嘗試連接,同時會去檢查是否可以恢復等等的一系列判斷。如果可以會爲重新連接重新創建一份新的HttpEngine,同時把相應的鏈路信息傳遞過去;
2、當恢復後的HttpEngine不爲空,那麼替換當前Call中的當前HttpEngine,執行while的continue,發起下一次的請求;
3、再重點強調一點HttpEngine.sendRequest()。這裏之前分析過會觸發connect()方法,在該方法中會通過RouteSelector.next()再去找當前適合的Route。多說一點,next()方法會傳遞到nextInetSocketAddress()方法,而此處一段重要的執行代碼就是network.resolveInetAddresses(socketHost)。這個地方最重要的是在Network這個接口中有一個對該接口的DEFAULT的實現域,而該方法通過工具類InetAddress.getAllByName(host)來完成對數組類的地址解析。
所以,多地址可以採用[“[http://aaaaa”,”https://bbbbbb”]的方式來配置。](http://aaaaa”%2C”https//bbbbbb”]的方式來配置。)

Gzip的使用方式

在源碼引導RequestBodyCompression.java中我們可以看到gzip的使用身影。通過攔截器對Request 的body進行gzip的壓縮,來減少流量的傳輸。
Gzip實現的方式主要是通過GzipSink對普通sink的封裝壓縮。在這個地方就不再貼相關代碼的實現。有興趣同學對照源碼看一下就ok。

強大的Interceptor設計應該也算是這個框架的一個亮點。

安全性

連接安全性主要是在HttpEngine.connect()方法。上一節講到地址相關的選擇,在HttpEngine中有一個靜態方法createAddress(client, networkRequest),在這裏通過獲取到OkHttpClient中關於SSLSocketFactory、HostnameVerifier和CertificatePinner的配置信息。而這些信息大部分採用默認情況。這些信息都會在後面的重連中作爲對比參考項。

同時在Connection.upgradeToTls()方法中,有對SSLSocket、SSLSocketFactory的創建活動。這些創建都會被記錄到ConnectionSpec中,當發起ConnectionSpec.apply()會發起一些列的配置以及驗證。

建議有興趣的同學先了解java的SSLSocket相關的開發再來了解本框架中的安全性,會更能理解一些。

平臺適應性

講了很多,終於來到了平臺適應性了。Platform是整個平臺適應的核心類。同時它封裝了針對不同平臺的三個平臺類Android和JdkWithJettyBootPlatform。
代碼實現在Platform.findPlatform中

private static Platform findPlatform() {
    // Attempt to find Android 2.3+ APIs.
    try {
      try {
        Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
      } catch (ClassNotFoundException e) {
        // Older platform before being unbundled.
        Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
      }

      OptionalMethod<Socket> setUseSessionTickets
          = new OptionalMethod<>(null, "setUseSessionTickets", boolean.class);
      OptionalMethod<Socket> setHostname
          = new OptionalMethod<>(null, "setHostname", String.class);
      Method trafficStatsTagSocket = null;
      Method trafficStatsUntagSocket = null;
      OptionalMethod<Socket> getAlpnSelectedProtocol = null;
      OptionalMethod<Socket> setAlpnProtocols = null;

      // Attempt to find Android 4.0+ APIs.
      try {
      //流浪統計類
        Class<?> trafficStats = Class.forName("android.net.TrafficStats");
        trafficStatsTagSocket = trafficStats.getMethod("tagSocket", Socket.class);
        trafficStatsUntagSocket = trafficStats.getMethod("untagSocket", Socket.class);

        // Attempt to find Android 5.0+ APIs.
        try {
          Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
          getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
          setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
        } catch (ClassNotFoundException ignored) {
        }
      } catch (ClassNotFoundException | NoSuchMethodException ignored) {
      }

      return new Android(setUseSessionTickets, setHostname, trafficStatsTagSocket,
          trafficStatsUntagSocket, getAlpnSelectedProtocol, setAlpnProtocols);
    } catch (ClassNotFoundException ignored) {
      // This isn't an Android runtime.
    }

    // Find Jetty's ALPN extension for OpenJDK.
    try {
      String negoClassName = "org.eclipse.jetty.alpn.ALPN";
      Class<?> negoClass = Class.forName(negoClassName);
      Class<?> providerClass = Class.forName(negoClassName + "$Provider");
      Class<?> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
      Class<?> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
      Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
      Method getMethod = negoClass.getMethod("get", SSLSocket.class);
      Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
      return new JdkWithJettyBootPlatform(
          putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass);
    } catch (ClassNotFoundException | NoSuchMethodException ignored) {
    }

    return new Platform();
  }

這裏採用了JAVA的反射原理調用到class的method。最後在各自的平臺調用下發起invoke來執行相應方法。詳情請參看繼承了Platform的Android類。
當然要做這兩種的平臺適應,必須要知道當前平臺在內存中相關的class地址以及相關方法。

總結

1、從整體結構和類內部域中都可以看到OkHttpClient,有點類似與安卓的ApplicationContext。看起來更像一個單例的類,這樣使用好處是統一。但是如果你不是高手,建議別這麼用,原因很簡單:邏輯牽連太深,如果出現問題要去追蹤你會有深深地罪惡感的;
2、框架中的一些動態方法、靜態方法、匿名內部類以及Internal的這些代碼相當規整,每個不同類的不同功能能劃分在不同的地方。很值得開發者學習的地方;
3、從平臺的兼容性來講,也是很不錯的典範(如果你以後要從事API相關編碼,那更得好好注意對兼容性的處理);
4、由於時間不是很富裕,所以本人對細節的把握還是不夠,這方面還得多多努力;
5、對於初學網絡編程的同學來說,可能一開始學習都是從簡單的socket的發起然後獲取響應開始的。因爲沒有很好的場景能讓自己知道網絡編程到底有多麼的重要,當然估計也沒感受到網絡編程有多麼的難受。我想這是很多剛入行的同學們的一種內心痛苦之處;
6、不足的地方是沒有對SPDY的方式最詳細跟進剖析(手頭還有工作的事情,後面如果有時間再補起來吧)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章