OKHttp 官方文件【一】

xiaxveliang發表於2020-07-30

最近工作比較忙,文章更新出現了延時。雖說寫技術部落格最初主要是寫給自己,但隨著文章越寫越多,現在更多的是寫給關注我技術文章的小夥伴們。最近一段時間沒有更新文章,雖有工作生活孩子佔用了大部分時間的原因,但也有自身的懶惰,這裡向小夥伴們也向自己說一聲抱歉...

OkHttp 是這幾年比較流行的 Http 客戶端實現方案,其支援HTTP/2、支援同一Host 連線池複用、支援Http快取、支援自動重定向 等等,有太多的優點。
一直想找時間瞭解一下 OkHttp 的實現原理 和 具體原始碼實現,不過還是推薦在使用 和 瞭解其原理之前,先通讀一遍 OkHttp 的官方文件,由於官方文件為英文,我在通讀的時候,順便翻譯了一下,如翻譯有誤,請幫忙指正

OkHttp官方API地址:
https://square.github.io/okhttp/

一、概述

Http是現在流行的應用程式請求方法。Http幫助我們交換資料和多媒體內容。有效地執行HTTP可以使您的內容載入更快,並節省頻寬。

OkHttp 是一個執行效率比較高的Http客戶端:

  • 支援HTTP/2 ,當多個請求對應同一host地址時,可共用同一個socket;
  • 連線池可減少請求延遲(如果HTTP/2不可用);
  • 支援GZIP壓縮,減少網路傳輸的資料大小;
  • 支援Response資料快取,避免重複網路請求;

當網路出現問題時,OkHttp會不斷重試:
OkHttp將從常見的連線問題中靜默恢復您的網路請求;如果您的服務具有多個IP地址,則在第一次連線失敗時,OkHttp將嘗試使用備用地址,這對於IPv4 + IPv6、減少伺服器的資料駐留是必需的;OkHttp支援TLS功能(TLS 1.3, ALPN, certificate pinning),OkHttp可將其配置回退以獲得廣泛的連線性。

使用OkHttp很容易,OkHttp的請求/響應API使用builder方式構建,OkHttp支援同步阻塞呼叫、非同步回撥呼叫。

1.1、Get a URL

Get方法請求一個url地址,並將response結果列印出來:完整的Http Get請求舉例

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

1.2、Post to a Server

向伺服器發起Post請求;完整的Http Post請求舉例

public static final MediaType JSON
    = MediaType.get("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(json, JSON);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

1.3、Requirements

OkHttp執行環境為 Android 5.0+ (API level 21+) 、Java 8+
OkHttp 3.12.x分支執行環境為Android 2.3+(API level 9+)、Java 7+

OkHttp依賴高效能I/O庫Okio,依賴Kotlin library使用Kotlin開發語言;這兩個依賴庫很小,並有很強的向後相容;

我們強烈建議你保持使用OkHttp最新版本。與自動更新的Web瀏覽器一樣,保持HTTPS客戶端的最新狀態是防範潛在安全問題的重要防禦措施。我們跟蹤有危險的TLS生態系統並調整OkHttp以改善連線性和安全性。

OkHttp當前使用平臺的內建TLS實現。 在Java平臺上,OkHttp還支援Conscrypt,它將BoringSSL與Java整合在一起。如果Conscrypt是最安全的SSL提供程式,OkHttp將使用Conscrypt。

OkHttp 3.12.x分支執行環境為Android 2.3+(API level 9+)、Java 7+。OkHttp 3.12.x不支援TLS 1.2,因此不推薦使用。因為升級困難,我們將在2021年12月31日之前,向3.12.x分支新增向後相容的補丁程式。

Security.insertProviderAt(Conscrypt.newProvider(), 1);

1.4、Releases

release history可參考 https://square.github.io/okhttp/changelog/

最新阪本已上傳 Maven Central。

implementation("com.squareup.okhttp3:okhttp:4.8.0")

OkHttp支援R8壓縮、混淆規則: https://square.github.io/okhttp/r8_proguard/

在一個使用了OkHttp依賴包的Android工程中,如果你使用了預設的R8壓縮演算法,你不用為引入Okhttp而多做任何事情。特定的規則已經整合到OkHttp提供的JAR包中,這些規則支援R8自動解析。

但是,如果你使用的不是R8,則必須應用以下混淆規則;你可能還需要新增Okio相關混淆規則,因為OkHttp使用了Okio依賴庫。

# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**

# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*

# OkHttp platform used only on JVM and when Conscrypt dependency is available.
-dontwarn okhttp3.internal.platform.ConscryptPlatform

1.5、MockWebServer

OkHttp includes a library for testing HTTP, HTTPS, and HTTP/2 clients.

OkHttp包含一個測試HTTP、HTTPS、HTTP/2的客戶端工程。

最新測試工程已上傳 Maven Central

testImplementation("com.squareup.okhttp3:mockwebserver:4.8.0")

1.6、 軟體開源許可協議

詳細瞭解 Apache License 2.0許可協議,可參考我的文章:https://blog.csdn.net/xiaxl/article/details/106137088

Copyright 2019 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

二、Calls

HTTP客戶端用於接收Http request請求,並響應Http response資料。理論原理並不複雜,但在是實現上確實比較難。

2.1、Requests

每一個 HTTP 請求包含一個請求地址URL請求方法(如GET或POST)請求Headers,很多請求可能包含請求 Body (特定內容型別的資料流);

2.2、Responses

響應資料包含 響應狀態碼(如200表示成功或404表示未找到)、響應Headers可選的響應Body

2.3、Rewriting Requests

當你使用OkHttp發起HTTP請求時,你的思想處於這樣一個高度 “使用這個URL地址 和這些請求Headers獲取資料”。為了保證效率和正確性,OkHttp在傳輸請求之前會先對其進行重寫。

OkHttp可以新增原始請求中缺少的Header,包括Content-Length、Transfer-Encoding、User-Agent、Host、Connection、Content-Type;如Accept-Encoding不存在,OkHttp會新增Header用於壓縮傳輸的Accept-Encoding;如果你獲取過cookies,OkHttp將自動將cookies新增到header中。

一些請求支援response資料快取。當快取的response資料過期時,OkHttp在一定條件下可以發起一個GET請求,以獲取新的response資料。這類請求的headers,例如If-Modified-SinceIf-None-Match會被新增。

2.4、Rewriting Responses

如果資料傳輸中使用壓縮演算法,OkHttp將刪除相應的響應Header 如Content-Encoding、Content-Length,因為它們不適用於解壓縮的response body。

如果conditional GET 成功,則會按照規範將 network與cache 的響應資料進行合併。

2.5、Follow-up Requests

如果你請求的 URL 已被重定向,在發生網路請求時,webserver將會返回一個 302 響應碼,此302響應碼用來標識重定向後新的 URL 請求地址。OkHttp將會自動重定向,並獲取最終的response資料。

如果請求的響應資訊提示要求進行 authorization 授權挑戰,OkHttp將會安全的完成授權挑戰(如果此時挑戰資訊已配置);如果認證器支援證照,OkHttp將使用內建證照進行重試;

2.6、Retrying Requests

有時連線失敗:
無論是本地連線池原因,還是網路原因造成webserver不可達,當網路條件可用時,OkHttp將會重試

2.7、Calls

通過 rewrites、redirects、follow-ups、retries,你的請求可能會產生許多中間請求和響應資料。OkHttp使用API Call來建立Request請求,為了保證請求的安全性,許多中間請求和響應是必不可少的。通常,中間請求不多!但是,值得高興的是,如果你的URLs被重定向,或你的伺服器出現故障,該請求將會被重試。

Calls 會以以下兩種方式執行:

  • 同步執行:執行執行緒會被阻塞,直到response資料返回;
  • 非同步執行:你可以將請求放在任何執行緒上,響應的回撥資料將在另一個執行緒中獲取。

Calls 可以在任何執行緒中被取消掉,這將倒是請求失敗,如果請求尚未完成。當取消請求時,寫request body或讀response body位置處將丟擲IOException異常。

2.8、Dispatch

對於同步呼叫,您需要自己建立執行執行緒,並負責控制您發出的請求數量, 同時連線過多會浪費資源, 太少會造成延遲;

對於非同步呼叫,Dispatcher預設請求策略為: 每個webserver伺服器預設最大請求數量預設為5,整體的最大請求數量為64,並且這兩個值使用者可自行定義。

三、Caching

OkHttp網路快取預設是關閉的,使用者可以選擇開啟。OkHttp實現網路快取功能依賴的是RFC標準,存在模糊定義的情況下,以當前比較流行的瀏覽器軟體Firefox/Chrome為準。

3.1、Basic Usage

  private val client: OkHttpClient = OkHttpClient.Builder()
      .cache(Cache(
          directory = File(application.cacheDir, "http_cache"),
          // $0.05 worth of phone storage in 2020
          maxSize = 50L * 1024L * 1024L // 10 MiB
      ))
      .build()

3.2、EventListener events

快取的回撥事件API為EventListener,典型場景如下:

3.2.1、Cache Hit

在理想情況下,快取資料可以完全滿足對應的request請求,而無需發起任何網路請求。應用Http網路快取資料後,將跳過常規網路請求事件,如DNS解析、連線到網路以及下載response資料。

根據HTTP RFC的建議,基於“Last-Modified”,文件的最長過期時間預設為文件計劃服務時間的10%。預設過期日期不應用於查詢的URI。

  • CallStart
  • CacheHit
  • CallEnd

3.2.2、Cache Miss

快取未命中時,可以看到正常的網路請求,但回撥事件顯示快取存在。根據響應headers,如果資料未從網路中獲取、不可快取、快取過期,則快取未命中很常見。

  • CallStart
  • CacheMiss
  • ProxySelectStart
  • … Standard Events …
  • CallEnd

3.2.3、Conditional Cache Hit

當需要檢查快取結果仍然有效時,跟隨在cachehitmiss後,會收到一個cacheConditionalHit事件。然後是快取命中或未命中。 至關重要的是,在快取命中的情況下,伺服器不會傳送響應正文。重要的是,在快取命中的情況下,不會 server 不會傳送response body 資料。

The response will have non-null cacheResponse and networkResponse. The cacheResponse will be used as the top level response only if the response code is HTTP/1.1 304 Not Modified.

在 HTTP/1.1 伺服器返回的Response 為 304 Not Modified情況下,請求的response將返回非空的 cacheResponsenetworkResponse。cacheResponse 的優先順序最高。

  • CallStart
  • CacheConditionalHit
  • ConnectionAcquired
  • … Standard Events…
  • ResponseBodyEnd (0 bytes)
  • CacheHit
  • ConnectionReleased
  • CallEnd

3.2.4、Cache directory

快取目錄必須有且僅有一個單例類持有。

可以在不再需要快取時刪除它,但這可能會刪除App重啟之前保持不變的快取。(Deleting the cache when it is no longer needed can be done. However this may delete the purpose of the cache which is designed to persist between app restarts.)

cache.delete()

3.2.5、Pruning the Cache

可以使用 evictAll 刪除整個快取

cache.evictAll()

可以使用 url 迭代方式,刪除某個單獨的Item。典型的應用場景是,使用者通過 下拉重新整理(pull to refresh)強制啟動一個重新整理動作。

val urlIterator = cache.urls()
while (urlIterator.hasNext()) {
  if (urlIterator.next().startsWith("https://www.google.com/")) {
    urlIterator.remove()
  }
}

3.2.6、Troubleshooting

1、有效的,可快取的 responses 資料,未被快取(Valid cacheable responses are not being cached)

確保完全讀取 responses 響應資料,除非完全讀取響應資料 或 請求被取消。

3.2.7、Overriding normal cache behaviour

See Cache documentation. https://square.github.io/okhttp/4.x/okhttp/okhttp3/-cache/

四、Connections

儘管你只提供了 URL ,但 OkHttp 規劃與對應伺服器(webserver)的網路連線(connection)時,使用以下三種型別:URL、Address、and Route.

4.1、URLs

URL( 如 https://github.com/square/okhttp )是 HTTP 和 Internet 的基礎。除了針對網路上所有內容的通用,還規定了如何訪問 web 資源。

URLs are abstract:

  • 指定請求(call)可以是明文 ( http ) 或加密 ( https ),但不指定應該使用哪種加密演算法。也沒有指定如何驗證對等方的證照(HostnameVerifier)或哪些證照可以信任(SSLSocketFactory);
  • 沒有指定是否應使用特定的代理伺服器或如何向該代理伺服器進行身份驗證;

每個 URL 標識一個特定的路徑( 如 /square/okhttp )和 查詢(如 ?q=sharks&lang= en),每個 webserver 管理許多URL。

4.2、Addresses

Addresses 指定一個 webserver (如 github.com) 和 連線到該伺服器所需的所有靜態配置:埠號、HTTPS設定、首選網路協議(如 HTTP/2、SPDY)。

多個 URL 共享同一個 address,也可能共享相同的 TCP socket 連線;共享連線具有顯著的效能優勢:更低的延遲、更高的吞吐量(由於 TCP 連線建立緩慢)、節省電量。 OkHttp使用一個 ConnectionPool,它可以自動重用 HTTP / 1.x 連線並多路複用 HTTP/2、SPDY連線。

在OkHttp中,地址的某些欄位來自URL(scheme、hostname、port),其餘部分來自OkHttpClient

4.3、Routes

Routes 提供實際連線到 webserver 所需的動態資訊。特定IP地址(由 DNS 查詢獲取)、要使用的確切代理伺服器(如果正在使用 ProxySelector )、要協商的TLS版本(用於HTTPS連線)。

一個 address 可能有很多 routes。 例如,一個 webserver 可以託管在多個資料中心中,DNS查詢時可以產生多個IP地址。

4.4、Connections

當你使用 OkHttp 向某個 URL 發起一個 Request 網路請求時,OkHttp做了以下幾件事:

  • OkHttp 使用URL,並配置 OkHttpClient 來建立一個 address。 此 address 指定了我們如何連線到 webserver ;
  • OkHttp 嘗試從連線池中檢索該 address 的連線;
  • 如果在池中找不到對應的連線,則選擇一個路由進行嘗試。 這通常意味著,建立一個DNS請求,以獲取伺服器的IP地址; 然後根據需要選擇TLS版本和代理伺服器;
  • 如果是新建一個路由,則可以通過建立 socket 連線、TLS隧道(基於HTTP代理的HTTPS),直接通過 TLS 建立隧道進行連線。如果有必要會進行TLS握手;
  • 傳送Http請求 和 讀取 Response 資料;

如果連線出現問題,OkHttp 將選擇其他路由進行重試, 這樣當一部分伺服器無法訪問時,OkHttp可以恢復使用; 當共用連線失效 或 TLS版本不受支援時,此功能也很有用。

一旦請求的 response 資料返回到客戶端,這個connection 將被釋放返回到連線池中,以保證該 這個connection 可以被其他請求複用。閒置一段時間後,連線將從池中退出。

五、Events

Events 可以讓你獲取應用程式執行中HTTP的狀態,使用 Events 來監聽狀態變化:

  • 應用程式發出的HTTP請求的的數量和頻率。 如果您發起了太多的Http請求,或者您的請求的內容太大,那麼您應該知道這些回撥資料!
  • 網路請求的效能。 如果網路的效能不足,則需要改善網路或減少使用。

5.1、EventListener

Subclass EventListener and override methods for the events you are interested in. In a successful HTTP call with no redirects or retries the sequence of events is described by this flow.

重寫介面 EventListener 中,你感興趣的的方法。在一次成功的Http請求獲取中,沒有重定向或重試的前提下,事件的呼叫順序如下圖所示:

執行順序

以下為 EventListener 使用舉例:

class PrintingEventListener extends EventListener {
  private long callStartNanos;

  private void printEvent(String name) {
    long nowNanos = System.nanoTime();
    if (name.equals("callStart")) {
      callStartNanos = nowNanos;
    }
    long elapsedNanos = nowNanos - callStartNanos;
    System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  @Override public void dnsStart(Call call, String domainName) {
    printEvent("dnsStart");
  }

  @Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
    printEvent("dnsEnd");
  }

  ...
}

建立兩個Http請求:

Request request = new Request.Builder()
    .url("https://publicobject.com/helloworld.txt")
    .build();

System.out.println("REQUEST 1 (new connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

System.out.println("REQUEST 2 (pooled connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

執行以上兩個請求時,PrintingEventListener列印的呼叫日誌如下:

REQUEST 1 (new connection)
0.000 callStart
0.010 dnsStart
0.017 dnsEnd
0.025 connectStart
0.117 secureConnectStart
0.586 secureConnectEnd
0.586 connectEnd
0.587 connectionAcquired
0.588 requestHeadersStart
0.590 requestHeadersEnd
0.591 responseHeadersStart
0.675 responseHeadersEnd
0.676 responseBodyStart
0.679 responseBodyEnd
0.679 connectionReleased
0.680 callEnd
REQUEST 2 (pooled connection)
0.000 callStart
0.001 connectionAcquired
0.001 requestHeadersStart
0.001 requestHeadersEnd
0.002 responseHeadersStart
0.082 responseHeadersEnd
0.082 responseBodyStart
0.082 responseBodyEnd
0.083 connectionReleased
0.083 callEnd

注意:為什麼第二次呼叫沒有觸發connect事件? 它重用了第一個請求的連線,從而顯著提高了效能。

5.2、EventListener.Factory

In the preceding example we used a field, callStartNanos, to track the elapsed time of each event. This is handy, but it won’t work if multiple calls are executing concurrently. To accommodate this, use a Factory to create a new EventListener instance for each Call. This allows each listener to keep call-specific state.

在前面的示例中,我們使用一個名為 callStartNanos 的欄位來跟蹤每個事件方法的執行時間。 這很方便,但是如果同時執行多個Http請求,它將不起作用。 為此,請使用 Factory 為每個 Call 請求建立一個新的 EventListener ,這會使每個 listener 保持良好的監聽狀態。

factory 為每一個Call 請求建立一個單獨的ID,使用該ID區分log訊息中不同的請求。

class PrintingEventListener extends EventListener {
  public static final Factory FACTORY = new Factory() {
    final AtomicLong nextCallId = new AtomicLong(1L);

    @Override public EventListener create(Call call) {
      long callId = nextCallId.getAndIncrement();
      System.out.printf("%04d %s%n", callId, call.request().url());
      return new PrintingEventListener(callId, System.nanoTime());
    }
  };

  final long callId;
  final long callStartNanos;

  public PrintingEventListener(long callId, long callStartNanos) {
    this.callId = callId;
    this.callStartNanos = callStartNanos;
  }

  private void printEvent(String name) {
    long elapsedNanos = System.nanoTime() - callStartNanos;
    System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  ...
}

我們可以以上PrintingEventListener 來監聽一對併發的HTTP請求:

Request washingtonPostRequest = new Request.Builder()
    .url("https://www.washingtonpost.com/")
    .build();
client.newCall(washingtonPostRequest).enqueue(new Callback() {
  ...
});

Request newYorkTimesRequest = new Request.Builder()
    .url("https://www.nytimes.com/")
    .build();
client.newCall(newYorkTimesRequest).enqueue(new Callback() {
  ...
});

網路環境為家庭Wifi,執行效率上 00020001更早執行完成:

0001 https://www.washingtonpost.com/
0001 0.000 callStart
0002 https://www.nytimes.com/
0002 0.000 callStart
0002 0.010 dnsStart
0001 0.013 dnsStart
0001 0.022 dnsEnd
0002 0.019 dnsEnd
0001 0.028 connectStart
0002 0.025 connectStart
0002 0.072 secureConnectStart
0001 0.075 secureConnectStart
0001 0.386 secureConnectEnd
0002 0.390 secureConnectEnd
0002 0.400 connectEnd
0001 0.403 connectEnd
0002 0.401 connectionAcquired
0001 0.404 connectionAcquired
0001 0.406 requestHeadersStart
0002 0.403 requestHeadersStart
0001 0.414 requestHeadersEnd
0002 0.411 requestHeadersEnd
0002 0.412 responseHeadersStart
0001 0.415 responseHeadersStart
0002 0.474 responseHeadersEnd
0002 0.475 responseBodyStart
0001 0.554 responseHeadersEnd
0001 0.555 responseBodyStart
0002 0.554 responseBodyEnd
0002 0.554 connectionReleased
0002 0.554 callEnd
0001 0.624 responseBodyEnd
0001 0.624 connectionReleased
0001 0.624 callEnd

EventListener.Factory 還可以限行部分呼叫,以下隨機了10%:

class MetricsEventListener extends EventListener {
  private static final Factory FACTORY = new Factory() {
    @Override public EventListener create(Call call) {
      if (Math.random() < 0.10) {
        return new MetricsEventListener(call);
      } else {
        return EventListener.NONE;
      }
    }
  };

  ...
}

5.3、Events with Failures

當請求失敗時,請求失敗的回撥方法將會被呼叫。當與server建立連線失敗時,呼叫 connectFailed();當HTTP 請求失敗時,呼叫 callFailed()。當發生請求失敗時,可能存在Start事件,但無End事件。

Events with Failures

5.4、Events with Retries and Follow-Ups

網路請求中,當發生網路連線錯誤時,OkHttp將自動重試。當這種場景出現時,connectFailed()callFailed()事件後,事件回撥不會終止。網路請求重試時,將會收到很多其他重試事件。

單個HTTP請求,後續可能存在對個網路請求,比如 authentication鑑權挑戰、重定向、HTTP網路層連線超時。這種場景下,會存在多個網路連線、requests、responses。因此後續,同一個型別的事件,可能存在很多的事件回撥。

Events with Retries and Follow-Ups

5.5、Availability

Events is available as a public API in OkHttp 3.11. Future releases may introduce new event types; you will need to override the corresponding methods to handle them.

Events 相關API 在OkHttp 3.11為共有API,後續版本中可能會增加新的回撥事件。使用Events事件時,你需要重寫其中相應的方法。

相關文章