天天看點

初識OkHttp(一)OKHttp使用總體設計 OKHttp中重要的類請求流程圖詳細類關系圖同步與異步的實作攔截器有什麼作用緩存政策HTTP連接配接的實作方式(說說連接配接池)重連機制Gzip的使用方式安全性平台适應性總結

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使用總體設計 OKHttp中重要的類請求流程圖詳細類關系圖同步與異步的實作攔截器有什麼作用緩存政策HTTP連接配接的實作方式(說說連接配接池)重連機制Gzip的使用方式安全性平台适應性總結

上面是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}:

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 =  *  *  * ; // 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(一)OKHttp使用總體設計 OKHttp中重要的類請求流程圖詳細類關系圖同步與異步的實作攔截器有什麼作用緩存政策HTTP連接配接的實作方式(說說連接配接池)重連機制Gzip的使用方式安全性平台适應性總結

詳細類關系圖

由于整個設計類圖比較大,是以本人将從核心入口client、cache、interceptor、網絡配置、連接配接池、平台适配性…這些方面來逐一進行分析源代碼的設計。

下面是核心入口OkHttpClient的類設計圖

初識OkHttp(一)OKHttp使用總體設計 OKHttp中重要的類請求流程圖詳細類關系圖同步與異步的實作攔截器有什麼作用緩存政策HTTP連接配接的實作方式(說說連接配接池)重連機制Gzip的使用方式安全性平台适應性總結

從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) / d, 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();
  }
}
           

傳回資訊

三月 ,  :: 下午 com.squareup.okhttp.recipes.LoggingInterceptors$1 intercept
資訊: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:, proxy=DIRECT hostAddress= cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA protocol=http/}
Host: publicobject.com 
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: 

三月 ,  :: 下午 com.squareup.okhttp.recipes.LoggingInterceptors$1 intercept
資訊: Received response for https://publicobject.com/helloworld.txt in ms
Server: nginx/ (Ubuntu)
Date: Thu,  Mar  :: GMT
Content-Type: text/plain
Content-Length: 
Last-Modified: Tue,  May  :: GMT
Connection: keep-alive
ETag: "5383fa03-6df"
Accept-Ranges: bytes
OkHttp-Selected-Protocol: http/
OkHttp-Sent-Millis: 
OkHttp-Received-Millis: 
           

從這裡的執行來看,攔截器主要是針對Request和Response的切面處理。

那再來看看源碼到底在什麼位置做的這個處理呢?為了更加直覺的反應執行流程,本人截圖了一下執行堆棧

初識OkHttp(一)OKHttp使用總體設計 OKHttp中重要的類請求流程圖詳細類關系圖同步與異步的實作攔截器有什麼作用緩存政策HTTP連接配接的實作方式(說說連接配接池)重連機制Gzip的使用方式安全性平台适應性總結

另外如果還有同學對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 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  response:          Response{protocol=http/, code=, message=OK, url=https://publicobject.com/helloworld.txt}
Response  cache response:    null
Response  network response:  Response{protocol=http/, code=, message=OK, url=https://publicobject.com/helloworld.txt}
Response  response:          Response{protocol=http/, code=, message=OK, url=https://publicobject.com/helloworld.txt}
Response  cache response:    Response{protocol=http/, code=, message=OK, url=https://publicobject.com/helloworld.txt}
Response  network response:  null
Response  equals Response ? true

Process finished with exit code 
           

上邊這一段代碼同樣來之于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(, 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(一)OKHttp使用總體設計 OKHttp中重要的類請求流程圖詳細類關系圖同步與異步的實作攔截器有什麼作用緩存政策HTTP連接配接的實作方式(說說連接配接池)重連機制Gzip的使用方式安全性平台适應性總結

這張圖畫得比較簡單,詳細的過程以及連接配接池的使用下面大緻說明一下:

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(一)OKHttp使用總體設計 OKHttp中重要的類請求流程圖詳細類關系圖同步與異步的實作攔截器有什麼作用緩存政策HTTP連接配接的實作方式(說說連接配接池)重連機制Gzip的使用方式安全性平台适應性總結

同時回到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的方式最詳細跟進剖析(手頭還有工作的事情,後面如果有時間再補起來吧)。