OkHttp3-請求器-連接-攔截器

本文爲譯文,由於譯者水平有限,歡迎拍磚,讀者也可以閱讀原文
OkHttp3-基本用法OkHttp3-使用進階(Recipes)OkHttp3-請求器(Calls)OkHttp3-連接(Connections),OkHttp3-攔截器(Interceptor)】

OkHttp3-請求器(Calls)

OkHttp客戶端負責接收應用程序發出的請求,並且從服務器獲取響應返回給應用程序。理論聽起來十分簡單,但是在實踐中往往會出現很多意想不到的因素。

請求 (Request)

每一個Http請求都包含一個URL和一個請求方式(比如Get或者Post),以及一些請求頭信息。請求也有可能包含一個請求主體:當一個數據流存在指定的content type類型的請求頭時。

響應 (Responses)

服務器根據請求向你的應用程序返回響應,此響應包含了一個狀態碼(比如200表示請求成功,404表示請求失敗)、響應頭、以及可能包含的響應主體。

重寫請求 (Rewriting Requests)

OkHttp所發出去的每個Http請求都是高等級的:`“fetch me this URL with these headers.”。爲了請求的正確性和高效性,OkHttp在數據傳輸之前會自動重寫你的請求。

OkHttp可以自動爲你的請求添加一些請求所沒有的請求頭,包括Content-LengthTransfer-EncodingUser-AgentHostConnection,以及Content-Type。 如果你的原生請求中沒有定義Accept-Encoding類型,那麼OkHtpp會自動爲請求添加Accept-Encoding類型爲Gzip,以此希望服務端能返回壓縮過的數據。如果應用程序中存在Cookies,那麼OkHttp將會自動將它們添加到你的請求的Cookie頭部信息中。

OkHttp會將一些請求的響應緩存起來。當其中的一個緩存過期時,OkHttp將會發送一個帶有特定的請求頭信息(比如If-Modified-Since或者If-None-Match),並且以Get方式請求去重新從服務器上獲取數據,並且如果所獲取的新數據與舊的緩存數據不一致,OkHttp將新的數據保存覆蓋掉舊的緩存。

重寫響應 (Rewriting Responses)

如果響應主體是經過壓縮處理,那麼OkHttp會自動將響應頭部中的Content-EncodingContent-Length去掉,因爲它們並不適用於解壓縮響應主體的操作。

如果一個請求方法爲Get的網絡請求執行成功,那麼正常情況下從服務器返回的響應將會和緩存進行合併。

跟蹤請求 (Follow-up Requests)

當你所請求的主機的URL發生改變時,服務器將會返回一個爲302的響應狀態碼以及新的URL信息。OkHttp將會根據這個URL進行重定向操作,並且再次向新的URL發送請求獲取數據。

當你發送請求時,服務器可能會返回一個響應告訴你需要進行身份基本認證,那麼OkHttp此時會自動告訴Authenticator去解決這個認證問題,當然,Authenticator需要你自己進行配置。Authenticator處理身份認證通過時會獲取到認證成功憑證,OkHttp會攜帶着它再次向服務器發送原來的請求。

重試請求 (Retrying Requests)

有的時候會發生連接失敗:可能連接池過期而導致連接斷開,或者請求的服務器無法找到。OkHttp將會不斷嘗試不同的可用線路去發送請求。

請求器 (Calls)

通過重寫、重定向、跟蹤以及重試,你發送的一個簡單的請求可能會變成需要發送多個請求以及接收多次響應之後才能獲得最後的想要的響應。OkHttp會將這些全部請求以及響應(你的一個請求任務)塑造成一個Call對象,然而請求過程中所發生的多次請求以及響應是必須的。但是通常情況下中間請求及響應工作不會很多!令人欣慰的是,無論發生URL重定向還是因爲服務器出現問題而向一個備用IP地址再次發送請求的情況,你的代碼都會一直正常運行。

執行Call有兩種方式:

  • 同步:請求和處理響應發生在同一線程。並且此線程會在響應返回之前會一直被堵塞。
  • 異步:請求和處理響應發生在不同線程。將發送請求操作發生在一個線程,並且通過回調的方式在其他線程進行處理響應。(一般在子線程發送請求,主線程處理響應)

Calls可以在任何線程被取消。當這個Call尚未執行結束時,執行取消操作將會直接導致此Call失敗!當一個Call被取消時,無論是寫入請求主體或者讀取響應主體的代碼操作,都會拋出一個IOException異常。

調度者 (Dispatch)

對於在同步線程中執行Call而言,你最好創建子線程並且手動管理你所發出的併發請求。因爲太多併發連接浪費資源,以及可能會導致發生一些不好的小問題。

對於異步線程執行Call而言,Dispactcher實現了一個限制最大併發的接口。你也可以自定義設置對於每臺主機的最大併發數(默認爲5),以及總的併發數(默認爲64)。



OkHttp3-連接(Connections)

雖然通常你只需要提供一個URL給OkHttp,OkHttp就可以幫你完成其他事情。但是實際上OkHttp連接服務器需要三個條件:URLAddressRoute

URLs(統一資源定位符)

提供一個URL(比如https://github.com/square/okhttp)讓Http去連接服務器是最基本的工作。還有一種漸漸普遍的文件定位方式稱爲URN(同意名稱定位符),它是利用一種分散式的命名方案去指定所需要訪問的資源文件。
URLs是抽象的:

  • URL可以指定請求的類型是明文(Http)或者是密文(Https),但是無法指定使用的是哪個加密算法,也無法指定怎樣去驗證證書(HostnameVerifier)或者指定哪種證書可以被驗證(SSLSocketFactory)。
  • URL無法指定使用哪些代理服務器,以及指定哪些代理服務器進行身份認證。

URL是具體的:

  • 每一個URL都可以定義一個指定的路徑(比如/square/okhttp)以及查詢條件(比如?q=sharks&lang=en)。每個主機可以擁有多個URL

Address(地址)

Address在OKHttp中是一個對象,它爲OkHttp提供靜態配置!
地址指定了一個服務器(比如github.com)以及連接此服務器所需要的靜態配置:端口號,HTTPS設置,以及指定的網絡協議(比如HTTP/2或者SPDY)。

相同地址的URL也可以共用相同的底層TCP Socket連接。共用相同的連接對於性能有很大的提升:更低的延遲,更大的吞吐量(複用連接,由於每個TCP啓動的都需要較多的準備工作),更少的電能損耗。OkHttp使用一個連接池,來自動複用HTTP/1.x connectionsHTTP/2SPDY連接。

URL爲地址提供了一些字段(比如域名、主機名、端口號),其他的字段都來自於OkHttpClient

Routes(路由)

Routes在OKHttp中是一個對象,它爲OkHttp提供動態配置!
路由提供了實際連接到服務器所需要的動態配置。比如所指定用來嘗試連接服務器的IP地址(從DNS服務商獲得)、連接過程中實際所使用到的代理服務器(如果使用了ProxySelector),以及使用的是哪個版本的
TLS協議(當使用Https協議連接時候需要)。

對於一個地址來說 ,可能存在有很多種路由的方式。比如,當一個服務器被託管在多個數據中心,這時路由從DNS供應商獲取的響應中就可以獲取到多個IP地址。

Connections(連接)

當你使用OkHttp去請求一個URL時,OkHttp爲你做了如下事情:

  1. OkHttp使用一個URL以及經過配置的OkHttpClient去創建一個address。這個address表示我們將如何連接服務器。
  2. OkHttp嘗試從連接池中獲取一個適用於此addressConnection
  3. 如果OkHttp沒有找到對應的Connection,那麼OkHttp就會選擇一條路由去嘗試創建連接。這通常意味着需要向DNS供應商發送一個請求去獲取這個服務器的IP地址,以及可能還需要選擇TLS版本和代理服務器。
  4. 如果此路由是一條新的路由,它將通過構建一個Socket連接,一個TLS連接(通過Http代理的Https),或者直接通過一個TLS進行連接(它需要TLS握手)。
  5. OkHttp發送Http請求並讀取響應。

如果在連接的過程中出現問題,那麼OkHttp將會選擇其他的路由進行重新連接。這意味着當一個服務器的某一個IP地址無法訪問時,OkHttp可以嘗試別的IP地址進行訪問。或者當一個連接池過期或者你嘗試連接的所用的TLS版本不受服務器支持時,這種重連機制也是非常有用的。

一旦客戶端發來接收到來自服務器的響應,那麼這個Connection將會被放置到連接池中以備於將來新的連接進行復用。Connection在長期不使用的情況下,將會從這個連接池中被移除。

··



OkHttp3-OkHttp3-攔截器(Interceptor)

攔截器

攔截器是OkHttp中提供一種強大機制,它可以實現網絡監聽、請求以及響應重寫、請求失敗重試等功能。下面舉一個簡單打印日誌的栗子,此攔截器可以打印出網絡請求以及響應的信息。

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();

    long t1 = System.nanoTime();
    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",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));

    return response;
  }
}

在沒有本地緩存的情況下,每個攔截器都必須至少調用chain.proceed(request)一次,這個簡單的方法實現了Http請求的發起以及從服務端獲取響應。
攔截器可以進行鏈式處理。假如你同時有一個壓縮數據和校驗數據的攔截器,你可以決定將請求或者響應數據先進行壓縮還是先校驗大小。OkHttp利用List集合去跟蹤並且保存這些攔截器,並且會依次遍歷調用。

Application interceptors

攔截器可以以application或者network兩種方式註冊,分別調用addInterceptor()以及addNetworkInterceptor方法進行註冊。我們使用上文中日誌攔截器的使用來體現出兩種註冊方式的不同點。
首先通過調用addInterceptor()OkHttpClient.Builder鏈式代碼中註冊一個application攔截器:

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

請求的URLhttp://www.publicobject.com/helloworld.txt被重定向成https://publicobject.com/helloworld.txt,OkHttp支持自動重定向。注意,我們的application攔截器只會被調用一次,並且調用chain.proceed()之後獲得到的是重定向之後的最終的響應信息,並不會獲得中間過程的響應信息:

INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example

INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

我們可以看到請求的URL被重定向了,因爲response.request().url()request.url()是不一樣的。日誌打印出來的信息顯示兩個不同的URL。客戶端第一次請求執行的url爲http://www.publicobject.com/helloworld.txt,而響應數據的url爲https://publicobject.com/helloworld.txt

Network interceptors

註冊一個Network攔截器和註冊Application攔截器方法是非常相似的。註冊Application攔截器調用的是addInterceptor(),而註冊Network攔截器調用的是addNetworkInterceptor()

OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

我們運行這段代碼,發現這個攔截被執行了兩次。一次是初始化也就是客戶端第一次向URL爲http://www.publicobject.com/helloworld.txt發出請求,另外一次則是URL被重定向之後客戶端再次向https://publicobject.com/helloworld.txt發出請求。

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt

INFO: 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_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

NetWork請求包含了更多信息,比如OkHttp爲了減少數據的傳輸時間以及傳輸流量而自動添加的請求頭Accept-Encoding: gzip希望服務器能返回經過壓縮過的響應數據。Network 攔截器調用Chain方法後會返回一個非空的Connection對象,它可以用來查詢客戶端所連接的服務器的IP地址以及TLS配置信息。

選擇使用Application或Network攔截器?

每一個攔截器都有它的優點。

Application interceptors

  • 無法操作中間的響應結果,比如當URL重定向發生以及請求重試等,只能操作客戶端主動第一次請求以及最終的響應結果。
  • 在任何情況下只會調用一次,即使這個響應來自於緩存。
  • 可以監聽觀察這個請求的最原始未經改變的意圖(請求頭,請求體等),無法操作OkHttp爲我們自動添加的額外的請求頭,比如If-None-Match
  • 允許short-circuit (短路)並且允許不去調用Chain.proceed()。(編者注:這句話的意思是Chain.proceed()不需要一定要調用去服務器請求,但是必須還是需要返回Respond實例。那麼實例從哪裏來?答案是緩存。如果本地有緩存,可以從本地緩存中獲取響應實例返回給客戶端。這就是short-circuit (短路)的意思。。囧)
  • 允許請求失敗重試以及多次調用Chain.proceed()

Network Interceptors

  • 允許操作中間響應,比如當請求操作發生重定向或者重試等。
  • 不允許調用緩存來short-circuit (短路)這個請求。(編者注:意思就是說不能從緩存池中獲取緩存對象返回給客戶端,必須通過請求服務的方式獲取響應,也就是Chain.proceed()
  • 可以監聽數據的傳輸
  • 允許Connection對象裝載這個請求對象。(編者注:Connection是通過Chain.proceed()獲取的非空對象)

重寫請求

攔截器可以添加、移除或者替換請求頭。甚至在有請求主體時候,可以改變請求主體。舉個栗子,你可以使用application 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();
      }
    };
  }
}

重寫響應

和重寫請求相似,攔截器可以重寫響應頭並且可以改變它的響應主體。相對於重寫請求而言,重寫響應通常是比較危險的一種做法,因爲這種操作可能會改變服務端所要傳遞的響應內容的意圖。
當然,如果你 比較奸詐 在不得已的情況下,比如不處理的話的客戶端程序接受到此響應的話會Crash等,以及你還可以保證解決重寫響應後可能出現的問題時,重新響應頭是一種非常有效的方式去解決這些導致項目Crash的問題。舉個栗子,你可以修改服務器返回的錯誤的響應頭Cache-Control信息,去更好地自定義配置響應緩存保存時間。

/** 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();
  }
};

不過通常最好的做法是在服務端修復這個問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章