Okhttp解析—Okhttp概覽


Okhttp解析—Okhttp概覽

Okhttp作為目前Android使用最為廣泛的網絡框架之一,我們有必要去深入了解一下,本文是Okhttp解析的第一篇,主要是從宏觀上認識Okhttp整個架構是如何實現的。

一、什么是Okhttp

HTTP是當今應用程序通過網絡交換數據和媒體的方式。 有效地使用 HTTP 可以使應用加載得更快並節省帶寬。
Okhttp是一個高效的HTTP Client,高效性體現在:

  • Http / 2支持允許對同一主機的所有請求共享一個套接字
  • 連接池減少了請求延遲
  • 透明 GZIP 縮小了下載大小
  • 對於重復請求,響應緩存可以完全避免網絡請求

當網絡出現問題時,OkHttp 不會立即結束: 它會默默地從常見的連接問題中恢復過來。 如果您的服務有多個 IP 地址,如果第一次連接失敗,OkHttp 將嘗試替代地址。 這對於 IPv4 + IPv6和承載於冗余數據中心的服務是必要的。 Okhttp 支持現代 TLS 特性(TLS 1.3、 ALPN、證書ping)。 它可以配置為回退到可用的連接。
並且Okhttp是易用的,其通過Builder模式設計請求 / 響應 API,支持同步阻塞調用和帶回調的異步調用。

二、Okhttp的請求機制以及相關概念

首先我們來了解下HTTP client、request、response。
HTTP client的作用就是接受我們的request並返回response。
request通常包含一個 URL, 一個方法 (比如GET/POST), 以及一個headers列表還可能包含一個body(特定內容類型的數據流)。
response則通常用響應代碼(比如200表示成功,404表示未找到)、headers和可選的body來回答request。

我們日常使用http都是按以下步驟:
1、創建httpClient
2、創建request
3、使用httpClient請求request然后獲取respone

使用Okhttp也是如此,我們創建OkhttpClient然后把Reques交給它,最后拿到Respone,但是Okhttp在內部實際進行http請求時並不是這樣簡單的拿Request去請求然后獲得Resopne返回。

下面就來看下Okhttp的請求機制,可以概括為以下流程:

  1. 當我們創建OkhttpClient然后把Reques交給它之后,Okhttp為了提高正確性和效率在傳輸請求之前會重寫請求。
  2. 然后Okhttp會嘗試連接webserver,我們知道request中是帶有URL的但是Okhttp在連接webserver時不僅僅使用URL它還會用Address和Route。連接webserver成功后獲取respone,連接webserver失敗Okhttp會進行重試操作。
  3. 在把respone返回給client之前Okhttp一般還會重寫respone以及緩存respone。還有就是如果請求過程中產生重定向Okhttp也會進行處理並返回最終的respone。
    上邊就是Okhttp的請求以及返回的大致流程

Rewriting Requests

Okhttp 可以添加原始請求中缺少的headers,包括Content-Length,Transfer-Encoding,User-Agent ,Host ,Connection , 和Content-Type。 除非Accept-Encoding頭已經存在,否則它將添加一個用於透明響應壓縮的 Accept-Encoding 頭。 如果你有 cookies,OkHttp 會添加一個 Cookie 頭。

有些請求會有一個緩存response。 當這個緩存過期,OkHttp 可以執行一個有條件的 GET 來下載新的response,這需要添加如 If-Modified-Since 和 If-None-Match 這樣的headers。

Connections

Okhttp連接webserver時使用了URL、Address和Route。

URL

Url是 HTTP 和互聯網的基礎,每個 URL 標識一個特定的路徑。它是一個通用的,分散的網絡命名方案,它指定了如何訪問網絡資源、指定調用是純文本(http) 或加密(https)方式。它們沒有指定是否應該使用特定的代理服務器或者如何通過該代理服務器的身份驗證

Address

Address指定一個 web 服務器(比如 github. com)和連接到該服務器所需的所有靜態配置: 端口號、 HTTPS 設置和首選網絡協議(比如 http / 2或 SPDY)。

具有相同Address的 url 也可能有相同的底層 TCP 套接字連接。 共享一個連接有很大的性能優勢比如更低的延遲,更高的吞吐量(由於 TCP 緩慢啟動)和節省電池。 Okhttp 使用一個 ConnectionPool 自動重用 http / 1.x 連接以及多路傳輸 http / 2和 SPDY 連接。
在 OkHttp 中,Address的一些字段來自 URL (scheme, hostname, port) ,其余字段來自 OkHttpClient。

Route

Route提供實際連接到網絡服務器所需的動態信息。 它會嘗試的特定 IP 地址(由 DNS 查詢發現)、使用的准確代理服務器(如果使用 ProxySelector)以及協商的 TLS 版本(用於 HTTPS 連接)。
一個地址可能有多條Route。 例如,承載於多個數據中心的 web 服務器在其 DNS 響應中可能會產生多個 IP 地址。

當你使用 OkHttp 請求一個 URL 時,它是這樣做的:

  1. 它使用 URL 並配置 OkHttpClient 來創建一個address。 這個address指定了我們如何連接到網絡服務器
  2. 它試圖從連接池(connection pool)中檢索具有該地址的連接
  3. 如果它沒有在連接池中找到連接,它會選擇一條route進行嘗試。 這通常意味着發出 DNS 請求來獲取服務器的 IP 地址。 然后,如果有需要,它會選擇一個 TLS 版本和代理服務器
  4. 如果是一個新的route,則通過構建直接的套接字連接、 TLS 隧道(通過 HTTP 代理使用 HTTPS)或直接的 TLS 連接進行連接。 必要時,它會進行 TLS 握手
  5. 發送 HTTP 請求並讀取respone

如果連接有問題,OkHttp 會選擇另一條route,再試一次。 這讓 OkHttp 在服務器地址的一個子集無法訪問時從錯誤中恢復。 當池連接過時或者不支持當前使用的 TLS 版本時,它也很有用。
一旦接收到respone,連接將返回到池中,以便可以在將來的請求中重用它。 連接在一段時間的不活動會被從連接池中清除。

Rewriting Response

如果使用透明壓縮,OkHttp 將刪除相應的 Content-Encoding 和 Content-Length,因為它們不適用於解壓縮的響應體。
如果條件 GET請求 成功,來自網絡和緩存的respone將按照規范的指示進行合並。

Follow-up Requests

當你請求的 URL 被重定向,webserver 將返回一個響應代碼,比如302來指示新 URL。Okhttp將會重定向檢索最終的respone。
如果respone發出了一個授權驗證,OkHttp 將要求 Authenticator (如果配置了一個)滿足這個驗證。 如果身份驗證者提供了憑據,那么request會攜帶該憑據去重試。

Retrying Requests

有時候連接會失敗(比如池連接過時並斷開連接或無法連接到網絡服務器本身)如果此時有一個可用的Route,OkHttp 會用該Route重試請求。

Okhttp在實現流程的時候還引入了一些概念比如Call、Interceptors、ConnectionSpec、Events等等。

Call

經過重寫、重定向、重試等操作,簡單請求可能會產生許多請求和響應。 Okhttp 使用 Call 來封裝表示request,Call就是一個已經准備好可以執行的請求。如果 url 被重定向,或者故障轉移到另一個 IP 地址,那么代碼將繼續工作,直至返回最終respone。

Call有兩種調用方式:
同步:線程阻塞直到響應可讀為止
異步: 可以在任何線程上對請求進行排隊,當響應可讀時在另一個線程上被調回
可以從任何線程取消Call調用, 這將導致調用失敗。在寫入請求body或讀取響應body時調用cancel會拋出IOException。

dispatcher

Dispatcher調度器,它實際就是負責Okhttp請求策略。
對於同步調用,調用請求的線程自己負責管理同時發出的請求數量。但是要注意的是太多的並發請求會浪費資源; 太少也不好。
對於異步調用,Dispatcher 實現最大並發請求的策略。 它設置了每個 web 服務器的最大值並發請求數為5,總並發數為64。當然我們也可以自行設置並發數。

Interceptors

攔截器可以說是Okhttp的精髓之一,它是一種強大的機制,它可以監測、重寫和重試Call調用。系統提供了5種已經定義好的攔截器,上面說的request/respone 重寫,失敗重試等都是在攔截器中完成的。
對 chain.proceed (request)的調用是每個攔截器實現的關鍵部分。 這個簡單的外觀方法是所有攔截器完成其功能的地方,並且還生成滿足請求的響應。 注意如果 chain.proceed (request)被調用多次,則必須關閉以前的響應body。
實際使用過程是通過攔截器鏈把攔截器鏈起來然后按順序調用攔截器。
interceptor

Okhttp攔截器分為2類:Application Interceptors和Network Interceptors。

這兩類攔截器本質上沒有區別只是它們作用的時機不同。由上圖我們可以看出Application Interceptors作用於Okhttp Core之前而Network Interceptors則作用於Okhttp Core之后。我的理解就是Application Interceptors調用是在請求發出之前,Network Interceptors則是在請求發出后與webserver連接的過程。
每種攔截器鏈都有其優點:
Application interceptors

  • 不必擔心重定向和重試之類的中間響應
  • 總是調用一次,即使 HTTP 響應是從緩存中提供的
  • 關注應用程序的原始意圖,而不關注OkHttp注入的headers
  • 允許短路和不調用Chain.proceed().
  • 允許重試並多次調用Chain.proceed().
    Network Interceptors
  • 能夠操作中間respone,如重定向和重試
  • 不會為短路網絡的緩存響應調用
  • 僅當數據將要通過網絡傳輸時才會關注
  • 允許帶有Connection請求
    最后補充兩個示例:
    使用Interceptor重寫請求
/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), gzip(originalRequest.body()))
        .build();
    return chain.proceed(compressedRequest);
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override public MediaType contentType() {
        return body.contentType();
      }

      @Override public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}

使用Interceptor重寫響應

/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Response originalResponse = chain.proceed(chain.request());
    return originalResponse.newBuilder()
        .header("Cache-Control", "max-age=60")
        .build();
  }
};

ConnectionSpec

ConnectionSpec的引入是為了HTTPS,在協商與 HTTPS 服務器的連接時,OkHttp 需要知道要提供哪些 TLS 版本和密碼套件。Okhttp為了與盡可能多的主機連接的同時保證連接的安全性引入ConnectionSpec,它實現了特定的安全性和連接性決策,Okhttp 包括四個內置的連接規范RESTRICTED_TLS、MODERN_TLS、COMPATIBLE_TLS、CLEARTEXT。
默認情況下,OkHttp 將嘗試建立一個 MODERN_TLS 連接。 但是,通過配置client的 connectionSpecs,如果 MODERN_TLS失敗,回退到 COMPATIBLE_TLS 連接。

OkHttpClient client = new OkHttpClient.Builder()
    .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
    .build();

Events

Events的引入是為了監控call請求,因為我們有必要了解我們應用的HTTP請求。具體來說就是監控以下內容:
應用程序 HTTP 請求調用的頻率和請求大小。
基礎網絡的性能監控,如果網絡差你應該減少請求或者改善網絡。

EventListener

Okhttp提供了EventListener,我們可以繼承它並重寫我們感興趣的方法來進行Event的監控。
Evnets
上圖是在沒有重試和重定向的情況下EventListener所能監控的Events流。下面是對應的代碼

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");
  }

  ...
}

Eventlistener. Factory

上面Eventlistener只適用於沒有並發的情況,如果有多個請求並發執行我們需要使用Eventlistener. Factory來給每個請求創建一個Eventlistener。

下面是一個給每個請求創建一個帶唯一ID的Eventlistener的示例:

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");
  }

  ...
}

上面說的都是請求正常時event,接下來說下非正常情況下的event流程。

Events with Failures

當請求失敗時,將調用一個失敗方法 connectFailed () ,用於在建立到服務器的連接時發生故障,當 HTTP 請求永久失敗時調用 callFailed ()。 當發生故障時,有可能開始事件沒有相應的結束事件。
event

Events with Retries and Follow-Ups

Okhttp是健壯的,可以從一些連接故障中自動恢復。 在這種情況下,connectFailed ()事件不是終結符,后面不跟 callFailed ()。 當嘗試重試時,事件偵聽器將收到多個相同類型的事件。
單個 HTTP 調用可能需要發出后續請求來處理身份驗證、重定向和 HTTP 層超時。 在這種情況下,可以嘗試多個連接、請求和響應。 重定向是單個請求可能觸發同一類型多個事件的另一個原因。
event

以上就是本文全部內容,接下來就是源碼分析了,不過建議在看源碼前把Okhttp的整個運行過程以及其中涉及的概念搞懂這樣看源碼才會事半功倍。


免責聲明!

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



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