OKHttp 官方文檔【一】


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

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事件時,你需要重寫其中相應的方法。

wx_gzh.jpg


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM