OkHttp 官方中文文檔

OkHttp官方中文文檔

本文結構


一、Calls

HTTP客戶端的工作是接受你的request,併產生它的response。這個在理論上是簡單的,但在實踐中確是很棘手。

1.1 請求

每一個HTTP請求中都包含一個URL,一個方法(如GETPOST),和一個請求頭列表(headers)。請求還可以含有一個請求體(body):一個特定內容類型的數據流。

1.2 響應

每一個HTTP響應中都包含一個狀態碼(如200代表成功,404代表未找​​到),一個響應頭列表(headers)和一個可選的響應體(body)。

1.3重寫請求

當你的OkHttp發送一個HTTP請求,你在描述一個高層次的要求:“給我獲取這個網址中的這些請求頭。”對於正確性和效率,OkHttp發送前會重寫你的請求。

OkHttp可以在原先的請求中添加請求頭(headers),包括Content-Length, Transfer-Encoding, User-Agent, Host, Connection, 和 Content-Type。除非請求頭已經存在壓縮響應,否則它還將添加一個Accept-Encoding請求頭。如果你有cookies,OkHttp還將添加一個Cookie請求頭。

一些請求會有一個緩存的響應。當這個緩存的響應不是最新的時候,OkHttp會發送一個有條件的GET來下載更新的響應,如果它比緩存還新。它將會添加需要的請求頭,如IF-Modified-SinceIf-None-Match

1.4重寫響應

如果使用的是透明壓縮,OkHttp會丟失相應的響應頭Content-EncodingContent-Length,這是因爲它們不能用於解壓響應體(body)。

如果一個條件GET是成功的,在指定的規範下,響應來自於網絡和緩存的合併。

1.5後續請求

當你的請求的URL已經移動,Web服務器將返回一個響應碼像302,以表明本文檔的新的URL。OkHttp將按照重定向檢索最終響應。

如果響應問題是一個的授權盤問,OkHttp將會要求身份驗證(如果有一個已經配置好),以滿足盤問。如果身份驗證提供憑據,請求將會帶着憑證進行重試。

1.6請求重試

有時連接失敗:要麼是連接池已經過時和斷開,或是Web服務器本身無法達成。如果有一個是可用的,OkHttp將會使用不同的路由進行請求重試。

1.7 呼叫

隨着重寫,重定向,後續和重試,你簡單的要求可能會產生很多請求和響應。OkHttp使用呼叫(Call)並通過許多必要的中間請求和響應來滿足你請求的任務模型。通常情況,這是不是很多!如果您的網址被重定向,或者如果您故障轉移到另一個IP地址,但它會欣慰的知道你的代碼會繼續工作。

通過以下兩種方式進行呼叫:
- 同步:直到響應,你的線程塊是可讀的。
- 異步:你在任何線程進行排隊請求,並且當響應是可讀的時候,你會在另一個線程得到回調

呼叫(Calls)可以在任何線程中取消。如果它尚未完成,它將作爲失敗的呼叫(Calls)!當呼叫(Call)被取消的時候,如果代碼試圖進行寫請求體(request body)或讀取響應體(response body)會遭受IOException異常。

1.8調度

對於同步調用,你帶上你自己的線程,並負責管理併發請求。併發連接過多浪費資源; 過少的危害等待時間。

對於異步調用,調度實現了最大同時請求策略。您可以設置每個Web服務器最大值(默認值爲5),和整體值(默認爲64)。

二、Connections

雖然只提供了URL,但是OkHttp計劃使用三種類型連接到你的web服務器:URL, Address, 和 Route。

2.1URLs

URLs(如https://github.com/square/okhttp)是HTTP和因特網的基礎。除了是網絡上通用和分散的命名方案,他們還指定了如何訪問網絡資源。

URLs摘要:
  • 它們指定該呼叫(Call)可以被明文(HTTP)或加密的(HTTPS),但不指定用哪種加密算法。他們也不指定如何驗證對方的證書(HostnameVerifier)或證書可以信任(SSLSocketFactory)。
  • 他們不指定是否應使用特定的代理服務器或如何與該代理服務器進行身份驗證。

他們還具體:每個URL識別特定的路徑(如 /square/okhttp)和查詢(如 ?q=sharks&lang=en)。每個Web服務器主機的網址。

2.2 Addresses

Addresses指定網絡服務器(如github.com)和所有的靜態必要的配置,以及連接到該服務器:端口號,HTTPS設置和首選的網絡協議(如HTTP / 2SPDY)。

共享相同地址的URL也可以共享相同的基礎TCP套接字連接。共享一個連接有實實在在的性能優點:更低的延遲,更高的吞吐量(由於TCP慢啓動)和保養電池。OkHttp使用的ConnectionPool自動重用HTTP / 1.x的連接和多樣的HTTP/ 2和SPDY連接。

在OkHttp地址的某些字段來自URL(scheme, hostname, port),其餘來自OkHttpClient

2.3 Routes

Routes提供連接到一個網絡服務器所必需的動態信息。就是嘗試特定的IP地址(如由DNS查詢發現),使用確切的代理服務器(如果一個特定的IP地址的ProxySelector在使用中)和協商的TLS版本(HTTPS連接)。

可能有單個地址對應多個路由。例如,在多個數據中心託管的Web服務器,它可能會在其DNS響應產生多個IP地址。

2.4Connections

當你使用OkHttp進行一個URL請求時,下面是它的操作流程:

  1. 它使用URL和配置OkHttpClient創建一個address。此地址指定我們將如何連接到網絡服務器。
  2. 它通過地址從連接池中取回一個連接。
  3. 如果它沒有在池中找到連接,它會選擇route嘗試。這通常意味着使用一個DNS請求, 以獲取服務器的IP地址。如果需要,它會選擇一個的TLS版本和代理服務器。
  4. 如果它是一個新的route,它連接通過建立無論是直接的socket連接,socket連接使用TLS安全通道(用於HTTPS通過一個HTTP代理),或直接TLS連接。它的TLS握手是必要的。
  5. 它發送HTTP請求並讀取響應。
    如果有連接出現問題,OkHttp將選擇另一條route,然後再試一次。這帶來的好處是當一個服務器的地址的一個子集是不可達時,OkHttp能夠自動恢復。當連接池是過時或者試圖TLS版本不受支持時,這種方式是很有用的。

一旦響應已經被接收到,該連接將被返回到池中,以便它可以在將來的請求中被重用。連接在池中閒置一段時間後,它會被趕出。

三、Recipes

我們已經寫了一些方法,演示瞭如何解決OkHttp常見問題。通過閱讀他們瞭解一切是如何正常工作的。可以自由剪切和粘貼這些例子。

3.1同步獲取

下載文件,打印其頭部,並以字符串形式打印其響應體。

string() 方法在響應體中是方便快捷的小型文件。但是,如果響應體較大(大於1 MIB以上),它會將整個較大文件加載到內存中,所以應該避免string() 。在這種情況下,更傾向於將響應體作爲流進行處理。

 private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    Headers responseHeaders = response.headers();
    for (int i = 0; i < responseHeaders.size(); i++) {
      System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
    }

    System.out.println(response.body().string());
  }

3.2異步獲取

下載一個工作線程的文件,當響應是可讀的時候,獲取回調(Callback)。當響應頭已經準備好後,將產生回調(Callback)。讀取響應體可能一直阻塞。目前OkHttp不提供異步API來接收響應體的部位。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    client.newCall(request).enqueue(new Callback() {
      @Override public void onFailure(Call call, IOException e) {
        e.printStackTrace();
      }

      @Override public void onResponse(Call call, Response response) throws IOException {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

        Headers responseHeaders = response.headers();
        for (int i = 0, size = responseHeaders.size(); i < size; i++) {
          System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
        }

        System.out.println(response.body().string());
      }
    });
  }

3.3訪問頭

典型的HTTP頭工作就像一個Map<String, String> :每個字段都有一個值或無值。但是,一些頭部(headers)允許多個值,比如Guava的Multimap。例如,它共同爲一個HTTP響應提供多個Vary頭。OkHttp的API,試圖使這兩種情況下都能舒適使用。

當寫請求頭,用header(name, value)來爲唯一出現的name設置value。如果它本身存在值,在添加新的value之前,他們會被移除。使用addHeader(name, value)來添加頭部不需要移除當前存在的headers

當讀取響應頭,用header(name)返回最後設置name的value。如果沒有valueheader(name)將返回null。可以使用headers(name)來讀取所有列表字段的值,。

要訪問所有的頭部,用Headers類,它支持索引訪問。

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

3.4Posting a String

使用HTTP POST的請求體發送到服務。下面例子post了一個markdown文檔到一個的Web服務(將markdown作爲HTML)。由於整個請求體是同時在內存中,應避免使用此API發送較大(大於1 MIB)的文件。

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    String postBody = ""
        + "Releases\n"
        + "--------\n"
        + "\n"
        + " * _1.0_ May 6, 2013\n"
        + " * _1.1_ June 15, 2013\n"
        + " * _1.2_ August 11, 2013\n";

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

3.5 Post Streaming

在這裏,我們POST請求體作爲stream。將正在生成請求體的內容寫入到stream中。下面例子streams直接進入 Okio緩衝水槽。你的程序可能更喜歡使用OutputStream,你可以通過BufferedSink.outputStream()獲得 OutputStream。

 public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody requestBody = new RequestBody() {
      @Override public MediaType contentType() {
        return MEDIA_TYPE_MARKDOWN;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.writeUtf8("Numbers\n");
        sink.writeUtf8("-------\n");
        for (int i = 2; i <= 997; i++) {
          sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
        }
      }

      private String factor(int n) {
        for (int i = 2; i < n; i++) {
          int x = n / i;
          if (x * i == n) return factor(x) + " × " + i;
        }
        return Integer.toString(n);
      }
    };

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(requestBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

3.6 Posting a File

將文件作爲請求體是很容易的。

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    File file = new File("README.md");

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

3.7 發佈表單參數

使用FormBody.Builder建立一個請求體,它就像一個HTML 的標記。Namesvalues將使用HTML兼容的表單URL編碼進行編碼。

 private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody formBody = new FormBody.Builder()
        .add("search", "Jurassic Park")
        .build();
    Request request = new Request.Builder()
        .url("https://en.wikipedia.org/w/index.php")
        .post(formBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

3.8 發佈multipart請求

MultipartBody.Builder可以構建與HTML文件上傳表單兼容的複雜請求主體。multipart請求體的每一部分本身就是請求體,並且可以定義自己的頭部。如果存在,這些頭應該描述的部分請求體,如它的Content-Disposition。如果Content-LengthContent-Type頭部可以使用,則他們會自動添加。

 private static final String IMGUR_CLIENT_ID = "...";
  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("title", "Square Logo")
        .addFormDataPart("image", "logo-square.png",
            RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
        .build();

    Request request = new Request.Builder()
        .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
        .url("https://api.imgur.com/3/image")
        .post(requestBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

3.9 通過GSON解析響應的JSON

GSON是實現JSON和Java對象之間便利轉換的API。這裏,我們用它來解碼從GitHub的API 響應的JSON。

需要注意的是ResponseBody.charStream()使用的Content-Type響應頭進行解碼時,所使用的字符集默認爲UTF-8 。

private final OkHttpClient client = new OkHttpClient();
  private final Gson gson = new Gson();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://api.github.com/gists/c2a7c39532239ff261be")
        .build();
    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
    for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
      System.out.println(entry.getKey());
      System.out.println(entry.getValue().content);
    }
  }

  static class Gist {
    Map<String, GistFile> files;
  }

  static class GistFile {
    String content;
  }

3.10 響應緩存

要緩存響應,你需要有一個緩存目錄來進行讀取和寫入,並限制緩存的大小。緩存目錄應該是私有的,不被信任的應用程序不能夠閱讀其內容!

多個緩存同時訪問相同的緩存目錄,這是錯誤的。大多數應用程序應該調用一次new OkHttpClient(),在任何地方都使用相同的實例和自己的緩存配置。否則,這兩個緩存實例將踩到對方,破壞響應緩存,這可能使你的程序崩潰。

響應緩存使用HTTP頭進行配置。您可以添加請求頭Cache-Control: max-stale=3600,這樣OkHttp的緩存就會遵循他們。你的網絡服務器可以通過自己的響應頭配置緩存多長時間的響應,如Cache-Control: max-age=9600。有緩存頭強制緩存的響應,強制網絡響應,或強制使用條件GET驗證的網絡響應。

 private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    client = new OkHttpClient.Builder()
        .cache(cache)
        .build();
  }

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

使用CacheControl.FORCE_NETWORK可以禁止使用緩存的響應。使用CacheControl.FORCE_CACHE可以禁止使用網絡。警告:如果您使用FORCE_CACHE和響應來自網絡,OkHttp將會返回一個504不可滿足請求的響應。

3.11 取消Call

通過Call.cancel()來立即停止正在進行的Call。如果一個線程目前正在寫請求或讀響應,它還將收到一個IOException異常。當一個Call不需要時,使用取消Call來保護網絡; 例如,當用戶從應用程序導航離開。同步和異步調用可以被取消。

private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    final long startNanos = System.nanoTime();
    final Call call = client.newCall(request);

    // Schedule a job to cancel the call in 1 second.
    executor.schedule(new Runnable() {
      @Override public void run() {
        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
        call.cancel();
        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
      }
    }, 1, TimeUnit.SECONDS);

    try {
      System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
      Response response = call.execute();
      System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, response);
    } catch (IOException e) {
      System.out.printf("%.2f Call failed as expected: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, e);
    }
  }

3.12 超時

當無法訪問查詢時,將調用超時失敗。超時在網絡劃分中可以是由於客戶端連接問題,服務器可用性的問題,或兩者之間的任何東西。OkHttp支持連接,讀取和寫入超時。

private final OkHttpClient client;

  public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    Response response = client.newCall(request).execute();
    System.out.println("Response completed: " + response);
  }

3.13 每個呼叫配置

所有的HTTP客戶端都在OkHttpClient中配置,這包括代理設置,超時和緩存。當你需要改變單一Call的配置時,調用OkHttpClient.newBuilder() 。這將返回共享相同的連接池,調度和配置的原客戶端的建造器。在下面的例子中,我們做了500毫秒超時,另外一個3000毫秒超時請求。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
        .build();

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(500, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();
      System.out.println("Response 1 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 1 failed: " + e);
    }

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(3000, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();
      System.out.println("Response 2 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 2 failed: " + e);
    }
  }

3.14 認證處理

OkHttp能夠自動重試未經授權的請求。當響應是401 Not Authorized,一個Authenticator被要求提供憑據。應該建立一個包含缺少憑據的新要求。如果沒有憑證可用,則返回null跳過重試。

使用Response.challenges()獲得任何認證挑戰方案和領域。當完成一個基本的挑戰,用Credentials.basic(username, password)編碼請求頭。

private final OkHttpClient client;

  public Authenticate() {
    client = new OkHttpClient.Builder()
        .authenticator(new Authenticator() {
          @Override public Request authenticate(Route route, Response response) throws IOException {
            System.out.println("Authenticating for response: " + response);
            System.out.println("Challenges: " + response.challenges());
            String credential = Credentials.basic("jesse", "password1");
            return response.request().newBuilder()
                .header("Authorization", credential)
                .build();
          }
        })
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/secrets/hellosecret.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

爲了避免驗證時不工作的重試,你可以返回null放棄。例如,當這些確切的憑據已經嘗試,您可以跳過重試:

if (credential.equals(response.request().header("Authorization"))) {
    return null; //如果我們已經使用這些憑據失敗,不重試
   }

當你的應用嘗試的次數超過了限制的次數時,你可以跳過重試:

if (responseCount(response) >= 3) {
    return null; //如果我們已經失敗了3次,放棄。 .
  }

這上面的代碼依賴於下面的responseCount()方法:

 private int responseCount(Response response) {
    int result = 1;
    while ((response = response.priorResponse()) != null) {
      result++;
    }
    return result;
  }

四、攔截器

攔截器是一個強大的機制,它可以監控,重寫和重試Calls。下面是一個簡單記錄傳出請求和響應傳入的攔截器。

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使用列表來跟蹤攔截器,。
這裏寫圖片描述

4.1 應用攔截器

攔截器可以註冊爲應用攔截器或網絡攔截器。我們將使用LoggingInterceptor來區別。

通過在OkHttpClient.Builder上調用addInterceptor()來註冊應用程序攔截器:

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();

該URL http://www.publicobject.com/helloworld.txt重定向到https://publicobject.com/helloworld.txt,並OkHttp遵循這種自動重定向。我們的應用攔截器被調用一次,並且從返回的響應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

我們可以看到,我們被重定向是因爲response.request().url()不同於request.url() 。這兩個日誌語句記錄兩個不同的URL。

4.2 網絡攔截器

註冊網絡攔截器很類似。調用addNetworkInterceptor()代替addInterceptor()

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();

當我們運行這段代碼,攔截器運行兩次。一個是初始請求http://www.publicobject.com/helloworld.txt,另一個是用於重定向到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

網絡請求還包含很多數據,如OkHttp加入Accept-Encoding: gzip頭部通知支持壓縮響應。網絡攔截器的鏈具有非空的連接,它可用於詢問IP地址和連接到網絡服務器的TLS配置。

4.3 應用程序和網絡攔截之間進行選擇

每個攔截器鏈(interceptor chain)都具有相對優勢。

應用攔截器

  • 不必擔心像重定向和重試的中間響應。
  • 總是被調用一次,即使HTTP響應來自緩存服務。
  • 觀察應用程序的原意。不關心OkHttp注入的頭文件,如 If-None-Match
  • 允許短路和不調用Chain.proceed()
  • 允許重試,並多次調用Chain.proceed() 。

網絡攔截器

  • 能夠操作像重定向和重試的中間響應。
  • 在短路網絡上不調用緩存的響應。
  • 觀察在網絡上傳輸的數據。
  • 訪問Connection承載請求。

4.4重寫請求

攔截器可以添加,刪除或替換請求頭。他們還可以轉換請求體。例如,如果你連接到已知支持它的網絡服務器,你可以使用應用程序攔截器添加請求體的壓縮。

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

4.5 重寫響應

相對應的,攔截器也可以重寫響應頭和轉換響應體。通常不要重寫請求頭,因爲它可能違反了Web服務器的期望導致更危險!

在一個棘手的情況下,如果已經做好應對的後果,重寫響應頭是解決問題的有效方式。例如,您可以修復服務器的配置錯誤的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();
  }
};

通常此方法效果最好,它補充了在Web服務器上相應的修復!

4.6 可用性

OkHttp的攔截器需要OkHttp 2.2或更高。不幸的是,攔截器不能與OkUrlFactory工作,或者建立在這之上的庫,包括 Retrofit ≤1.8和 Picasso≤2.4。

五、 HTTPS

OkHttp試圖平衡兩個相互競爭的擔憂:

  • 連接到儘可能多的主機越好。這包括運行最新版本的先進主機boringssl和運行舊版的日期主機OpenSSL
  • 安全的連接。這包括遠程Web服務器證書的驗證和強密碼交換的數據隱私。

當涉及到HTTPS服務器的連接,OkHttp需要知道提供哪些TLS版本密碼套件。如果客戶端想要最大限度地連接包括過時的TLS版本和弱由設計的密碼套件。通過使用最新版本的TLS和實力最強的加密套件來最大限度地提高客戶端的安全性。

具體的安全與連接是由ConnectionSpec接口決定。OkHttp包括三個內置的連接規格:

  • MODERN_TLS是連接到現代的HTTPS服務器安全的配置。
  • COMPATIBLE_TLS是連接到一個安全,但不是現代的-HTTPS服務器的安全配置。
  • CLEARTEXT是用於不安全配置的http://網址。
    默認情況下,OkHttp先嚐試MODERN_TLS連接,如果現代配置失敗的話將退回到COMPATIBLE_TLS連接。

在每一個規範的TLS版本和密碼套件都可隨每個發行版而更改。例如,在OkHttp 2.2,我們下降支持響應POODLE攻擊的SSL 3.0。而在OkHttp 2.3我們下降的支持RC4。對於桌面Web瀏覽器,保持最新的OkHttp是保持安全的最好辦法。

你可以用一組自定義TLS版本和密碼套件建立自己的連接規格。例如,限制配置三個備受推崇的密碼套件。它的缺點是,它需要的Andr​​oid 5.0+和一個類似的電流網絡服務器

ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)  
    .tlsVersions(TlsVersion.TLS_1_2)
    .cipherSuites(
          CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
          CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
          CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
    .build();

OkHttpClient client = new OkHttpClient.Builder() 
    .connectionSpecs(Collections.singletonList(spec))
    .build();

5.1證書釘扎

默認情況下,OkHttp信任主機平臺的證書頒發機構。這種策略最多的連接,但它受證書頒發機構的襲擊,如2011 DigiNotar的攻擊。它還假定您的HTTPS服務器的證書是由證書頒發機構簽署。

使用CertificatePinner來限制哪些證書和證書頒發機構是可信任的。證書釘扎增強了安全性,但這會限制你的服務器團隊更新自己的TLS證書。在沒有你的服務器的TLS管理員的同意下,不要使用證書釘扎!

public CertificatePinning() {
    client = new OkHttpClient.Builder()
        .certificatePinner(new CertificatePinner.Builder()
            .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
            .build())
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/robots.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    for (Certificate certificate : response.handshake().peerCertificates()) {
      System.out.println(CertificatePinner.pin(certificate));
    }
  }

5.2定製信任證書

下面完整的代碼示例演示瞭如何用自定義證書替換主機平臺的證書。如上所述,在沒有你的服務器的TLS管理員的同意下,不要使用自定義證書!

private final OkHttpClient client;

  public CustomTrust() {
    SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream());
    client = new OkHttpClient.Builder()
        .sslSocketFactory(sslContext.getSocketFactory())
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    Response response = client.newCall(request).execute();
    System.out.println(response.body().string());
  }

  private InputStream trustedCertificatesInputStream() {
    ... // Full source omitted. See sample.
  }

  public SSLContext sslContextForTrustedCertificates(InputStream in) {
    ... // Full source omitted. See sample.
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章