OKHttp原始碼解析

許佳佳233發表於2017-09-01

轉載出處:http://frodoking.github.io/2015/03/12/android-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

##使用

簡單使用程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.

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.
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.
Do not confuse this class with the misnamed HttpURLConnection, which isn’t so much a connection as a single request/response exchange.
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:

  • Server Name Indication (SNI) enables one IP address to negotiate secure connections for multiple domain names.
  • Application Layer Protocol Negotiation (ALPN) enables the HTTPS port (443) to be used for different HTTP and SPDY protocols.

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.

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.keepAlive true if HTTP and SPDY connections should be pooled at all. Default is true.
  • http.maxConnections maximum number of idle connections to each to keep in the pool. Default is 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.

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.

5.Request.java
An HTTP request. Instances of this class are immutable if their body is null or itself immutable.(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.

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.

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.

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

  • 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.
  • 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.

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.

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}.

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

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

  • Request Count: the number of HTTP requests issued since this cache was created.
  • 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.

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:

1
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:

1
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:

1
2
3
4
5
6
7
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:

1
2
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.

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.

##請求流程圖
下面是關於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原始碼中的一個引導例子來說明攔截器的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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();
  }
}

返回資訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
三月 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不應該手動去建立,所以作為開發使用者來說,一般用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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();
  }
}

返回資訊

1
2
3
4
5
6
7
8
9
10
資訊: 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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迴應體存在的話就開始讀取當前迴應頭。在這裡有發起返回存入快取系統,也有返回和快取系統進行一個對比的過程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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()方法說起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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"]的方式來配置。

##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中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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的方式最詳細跟進剖析(手頭還有工作的事情,後面如果有時間再補起來吧)。

##結束語
很早前都打算花點時間好好來看一個值得學習的框架,今天終於算是弄得差不多了。我相信從框架的前期使用、到程式碼的介入、再到原始碼分模組的剖析、最後到整理成文章。我想這都是一個很好的學習和成長的過程。

希望看到這篇文章的同學能做出評價,並且給出一些好的剖析點。

我也是一個普普通通的編碼人,只是內心多了一點點不“安分” ^.^。

##後續
最近看到一些網友建議把okhttp的連線池對Connection的重用維護機制以及HTTP和SPDY協議如何得到區分這兩部分內容做深入的分析
有需要的同學請移步:OKHttp原始碼解析-ConnectionPool對Connection重用機制&Http/Https/SPDY協議選擇

相關文章