Android中開源網絡請求框架OkHttp使用

1. 歷史上Http請求庫優缺點

在講述OkHttp之前, 我們看下沒有OkHttp的時代, 我們是如何完成http請求的.

在沒有OkHttp的日子, 我們使用 HttpURLConnection 或者 HttpClient . 那麼這兩者都有什麼優缺點呢? 爲什麼不在繼續使用下去呢?

HttpClient 是Apache基金會的一個開源網絡庫, 功能十分強大, API數量衆多, 但是正是由於龐大的API數量使得我們很難在不破壞兼容性的情況下對它進行升級和擴展, 所以Android團隊在提升和優化HttpClient方面的工作態度並不積極.

HttpURLConnection 是一種多用途, 輕量極的HTTP客戶端, 提供的API比較簡單, 可以容易地去使用和擴展. 不過在Android 2.2版本之前, HttpURLConnection 一直存在着一些令人厭煩的bug. 比如說對一個可讀的InputStream調用close()方法時,就有可能會導致連接池失效了。那麼我們通常的解決辦法就是直接禁用掉連接池的功能:

private void disableConnectionReuseIfNecessary() {    
    // 這是一個2.2版本之前的bug    
    if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {    
        System.setProperty("http.keepAlive", "false");    
    }    
}

因此, 一般的推薦是在2.2之前, 使用 HttpClient , 因爲其bug較少. 在2.2之後, 推薦使用 HttpURLConnection , 因爲API簡單, 體積小, 並且有壓縮和緩存機制, 並且Android團隊後續會繼續優化 HttpURLConnection .

但是, 上面兩個類庫和 OkHttp 比起來就弱爆了, 因爲OkHttp不僅具有高效的請求效率, 並且提供了很多開箱即用的網絡疑難雜症解決方案.

- 支持HTTP/2, HTTP/2通過使用多路複用技術在一個單獨的TCP連接上支持併發, 通過在一個連接上一次性發送多個請求來發送或接收數據

- 如果HTTP/2不可用, 連接池複用技術也可以極大減少延時

- 支持GZIP, 可以壓縮下載體積

- 響應緩存可以直接避免重複請求

- 會從很多常用的連接問題中自動恢復

- 如果您的服務器配置了多個IP地址, 當第一個IP連接失敗的時候, OkHttp會自動嘗試下一個IP

- OkHttp還處理了代理服務器問題和SSL握手失敗問題

使用 OkHttp 無需重寫您程序中的網絡代碼。OkHttp實現了幾乎和java.net.HttpURLConnection一樣的API。如果你用了 Apache HttpClient,則OkHttp也提供了一個對應的okhttp-apache 模塊。

還有一個好消息, 從Android 4.4起, 其 HttpURLConnection 的內部實現已經變爲 OkHttp , 您可以參考這兩個網頁: 爆棧網 和 Twitter .

2. OkHttp類與http請求響應的映射

在講解OkHttp使用之前, 再看下我們Http請求和響應都有哪些部分組成.

2.1 http請求

2QJbaqy.png!web

所以一個類庫要完成一個http請求, 需要包含 請求方法 , 請求地址 , 請求協議 , 請求頭 , 請求體 這五部分. 這些都在 okhttp3.Request 的類中有體現, 這個類正是代表http請求的類. 看下圖:

67NJbir.png!web

其中 HttpUrl 類代表 請求地址 , String method 代表 請求方法 , Headers 代表請求頭, RequestBody 代表請求體. Object tag 這個是用來取消http請求的標誌, 這個我們先不管. 這裏也許你在疑惑, 請求協議 呢? 爲什麼沒有請求協議對應的類. 且聽我慢慢道來, 下面就會講到這個問題.

2.1.1 請求協議的協商升級

目前, Http/1.1在全世界大範圍的使用中, 直接廢棄跳到http/2肯定不現實. 不是每個用戶的瀏覽器都支持http/2的, 也不是每個服務器都打算支持http/2的, 如果我們直接發送http/2格式的協議, 服務器又不支持, 那不是掛掉了! 總不能維護一個全世界的網站列表, 表示哪些支持http/2, 哪些不支持?

爲了解決這個問題, 從稍高層次上來說, 就是爲了更方便地部署新協議, HTTP/1.1 引入了 Upgrade 機制. 這個機制在 RFC7230 的「 6.7 Upgrade 」這一節中有詳細描述.

簡單說來, 就是先問下你支持http/2麼? 如果你支持, 那麼接下來我就用http/2和你聊天. 如果你不支持, 那麼我還是用原來的http/1.1和你聊天.

1.客戶端在請求頭部中指定 Connection 和 Upgrade 兩個字段發起 HTTP/1.1 協議升級. HTTP/2 的協議名稱是 h2c, 代表 HTTP/2 ClearText.

GET / HTTP/1.1Host: example.comConnection: Upgrade, HTTP2-SettingsUpgrade: h2cHTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

2.如果服務端不同意升級或者不支持 Upgrade 所列出的協議,直接忽略即可(當成 HTTP/1.1 請求,以 HTTP/1.1 響應).

HTTP/1.1 200 OKContent-Length: 243Content-Type: text/html...

如果服務端同意升級,那麼需要這樣響應:

HTTP/1.1 101 Switching ProtocolsConnection: UpgradeUpgrade: h2c[ HTTP/2 connection ... ]

HTTP Upgrade 響應的狀態碼是 101,並且響應正文可以使用新協議定義的數據格式。

這樣就可以完成從http/1.1升級到http/2了. 同樣也可以從http/1.1升級到WebSocket.

這樣, 你就瞭解了爲什麼OkHttp沒有指定具體請求協議了吧. 因爲OkHttp使用了請求協議的協商升級, 無論是1.1還是2, 都先只以1.1來發送, 並在發送的信息頭裏包含協議升級字段. 接下來就看服務器是否支持協議升級了. OkHttp使用的協議升級字段是 ALPN , 如果有興趣, 可以更深入的查閱相關資料.

2.1.2 OkHttp請求

接下來我們構造一個http請求, 並查看請求具體內容.

final Request request = new Request.Builder().url("https://github.com/").build();

我們看下在內存中, 這個請求是什麼樣子的, 是否如我們上文所說和 請求方法 , 請求地址 , 請求頭 , 請求體 一一對應.

Mr2eUr6.png!web

2.2 http響應

我們看下一個http響應由哪些部分組成, 先看下響應組成圖:

ZN73euE.png!web

可以看到大體由 應答首行 , 應答頭 , 應答體 構成. 但是 應答首行 表達的信息過多,HTTP/1.1 表示 訪問協議 , 200 是響應碼, OK 是描述狀態的消息. 根據單一職責, 我們不應該把這麼多內容用一個 應答首行 來表示. 這樣的話, 我們的響應就應該由 訪問協議 , 響應碼 , 描述信息 , 響應頭 , 響應體 來組成.

2.2.1 OkHttp響應

我們看下OkHttp庫怎麼表示一個響應:

faAF3eM.png!web

可以看到 Response 類裏面有 Protocol 代表 請求協議 , int code 代表 響應碼 ,String message 代表 描述信息 , Headers 代表 響應頭 , ResponseBody 代表 響應體. 當然除此之外, 還有 Request 代表持有的請求, Handshake 代表SSL/TLS握手協議驗證時的信息, 這些額外信息我們暫時不問.

有了剛纔說的OkHttp響應的類組成, 我們看下OkHttp請求後響應在內存中的內容:

final Request request = new Request.Builder().url("https://github.com/").build();Response response = client.newCall(request).execute();

2YR7faQ.png!web可以看到和我們的分析十分一致.

講了OkHttp裏的請求類和響應類, 我們接下來就可以直接講述OkHttp的使用方法了.

3 HTTP GET

3.1 同步GET

同步GET的意思是一直等待http請求, 直到返回了響應. 在這之間會阻塞進程, 所以通過get不能在Android的主線程中執行, 否則會報錯.

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

OkHttpClient實現了 Call.Factory 接口, 是Call的工廠類, Call負責發送執行請求和讀取響應.

Request代表Http請求, 通過Request.Builder輔助類來構建.

client.newCall(request)通過傳入一個http request, 返回一個Call調用. 然後執行execute()方法, 同步獲得

Response代表Http請求的響應. response.body()是ResponseBody類, 代表響應體, 可以通過responseBody.string()獲得字符串的表達形式, 或responseBody.bytes()獲得字節數組的表達形式, 這兩種形式都會把文檔加入到內存. 也可以通過responseBody.charStream()和responseBody.byteStream()返回流來處理.

上述代碼完成的功能是下載一個文件, 打印他的響應頭, 以string形式打印響應體.

響應體的string()方法對於小文檔來說十分方便高效. 但是如果響應體太大(超過1MB), 應避免使用 string()方法, 因爲它會將把整個文檔加載到內存中.

對於超過1MB的響應body, 應使用流的方式來處理響應body. 這和我們處理xml文檔的邏輯是一致的, 小文件可以載入內存樹狀解析, 大文件就必須流式解析.

3.2 異步GET

異步GET是指在另外的工作線程中執行http請求, 請求時不會阻塞當前的線程, 所以可以在Android主線程中使用.

下面是在一個工作線程中下載文件, 當響應可讀時回調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(Request request, Throwable throwable) {
        throwable.printStackTrace();
      }

      @Override public void onResponse(Response response) throws IOException {        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());
      }
    });
}

4 HTTP POST

4.1 Post方式提交String

下面是使用HTTP POST提交請求到服務. 這個例子提交了一個markdown文檔到web服務, 以HTML方式渲染markdown. 因爲整個請求體都在內存中, 因此避免使用此api提交大文檔(大於1MB).

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

4.2 Post方式提交流

以流的方式POST提交請求體. 請求體的內容由流寫入產生. 這個例子是流直接寫入Okio的BufferedSink. 你的程序可能會使用OutputStream, 你可以使用BufferedSink.outputStream()來獲取. OkHttp的底層對流和字節的操作都是基於Okio庫, Okio庫也是Square開發的另一個IO庫, 填補I/O和NIO的空缺, 目的是提供簡單便於使用的接口來操作IO.

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

4.3 Post方式提交文件

以文件作爲請求體是十分簡單的。

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

4.4 Post方式提交表單

使用FormEncodingBuilder來構建和HTML <form> 標籤相同效果的請求體. 鍵值對將使用一種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());
  }

4.5 Post方式提交分塊請求

MultipartBody.Builder可以構建複雜的請求體, 與HTML文件上傳形式兼容. 多塊請求體中每塊請求都是一個請求體, 可以定義自己的請求頭. 這些請求頭可以用來描述這塊請求, 例如它的Content-Disposition. 如果Content-Length和Content-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());
  }

5. 其他用法

5.1 提取響應頭

典型的HTTP頭像是一個 Map<String, String> : 每個字段都有一個或沒有值. 但是一些頭允許多個值, 像Guava的Multimap.

例如: HTTP響應裏面提供的Vary響應頭, 就是多值的. OkHttp的api試圖讓這些情況都適用.

當寫請求頭的時候, 使用header(name, value)可以設置唯一的name、value. 如果已經有值, 舊的將被移除, 然後添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).

當讀取響應頭時, 使用header(name)返回最後出現的name、value. 通常情況這也是唯一的name、value. 如果沒有值, 那麼header(name)將返回null. 如果想讀取字段對應的所有值, 使用headers(name)會返回一個list.

爲了獲取所有的Header, Headers類支持按index訪問.

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

5.2 使用Gson來解析JSON響應

Gson是一個在JSON和Java對象之間轉換非常方便的api庫. 這裏我們用Gson來解析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;
  }

5.3 響應緩存

爲了緩存響應, 你需要一個你可以讀寫的緩存目錄, 和緩存大小的限制. 這個緩存目錄應該是私有的, 不信任的程序應不能讀取緩存內容.

一個緩存目錄同時擁有多個緩存訪問是錯誤的. 大多數程序只需要調用一次new OkHttp(), 在第一次調用時配置好緩存, 然後其他地方只需要調用這個實例就可以了. 否則兩個緩存示例互相干擾, 破壞響應緩存, 而且有可能會導致程序崩潰.

響應緩存使用HTTP頭作爲配置. 你可以在請求頭中添加Cache-Control: max-stale=3600 , OkHttp緩存會支持. 你的服務通過響應頭確定響應緩存多長時間, 例如使用Cache-Control: max-age=9600.

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();
    client.setCache(cache);
}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));
}

如果需要阻值response使用緩存, 使用 CacheControl.FORCE_NETWORK . 如果需要阻值response使用網絡, 使用 CacheControl.FORCE_CACHE .

警告: 如果你使用 FORCE_CACHE , 但是response要求使用網絡, OkHttp將會返回一個504 Unsatisfiable Request 響應.

5.3.1 Force a Network Response

有些時候, 比如用戶剛剛點擊 刷新 按鈕, 這時必須跳過緩存, 直接從服務器抓取數據. 爲了強制全面刷新, 我們需要添加 no-cache 指令:

connection.addRequestProperty("Cache-Control", "no-cache");

這樣就可以強制每次請求直接發送給源服務器, 而不經過本地緩存版本的校驗, 常用於需要確認認證的應用和嚴格要求使用最新數據的應用.

5.3.2 Force a Cache Response

有時你會想立即顯示資源. 這樣即使在後臺正下載着最新資源, 你的客戶端仍然可以先顯示原有資源, 畢竟有個東西顯示比沒有東西顯示要好.

如果需要限制讓請求優先使用本地緩存資源, 需要增加 only-if-cached 指令:

try {
     connection.addRequestProperty("Cache-Control", "only-if-cached");     InputStream cached = connection.getInputStream();     // the resource was cached! show it
  catch (FileNotFoundException e) {     // the resource was not cached
 }
}

5.4 取消一個Call

使用Call.cancel()可以立即停止掉一個正在執行的call. 如果一個線程正在寫請求或者讀響應, 將會引發IOException. 當call沒有必要的時候, 使用這個api可以節約網絡資源. 例如當用戶離開一個應用時, 不管同步還是異步的call都可以取消.

你可以通過tags來同時取消多個請求. 當你構建一請求時, 使用RequestBuilder.tag(tag)來分配一個標籤, 之後你就可以用OkHttpClient.cancel(tag)來取消所有帶有這個tag的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);
    }
  }

5.5 超時

沒有響應時使用超時結束call. 沒有響應的原因可能是客戶點鏈接問題、服務器可用性問題或者這之間的其他東西. 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);
  }

5.6 每個call的配置

使用OkHttpClient, 所有的HTTP Client配置包括代理設置、超時設置、緩存設置. 當你需要爲單個call改變配置的時候, 調用 OkHttpClient.newBuilder() . 這個api將會返回一個builder, 這個builder和原始的client共享相同的連接池, 分發器和配置.

下面的例子中,我們讓一個請求是500ms的超時、另一個是3000ms的超時。

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

5.7 處理驗證

這部分和HTTP AUTH有關.

5.7.1 HTTP AUTH

使用HTTP AUTH需要在server端配置http auth信息, 其過程如下:

- 客戶端發送http請求

- 服務器發現配置了http auth, 於是檢查request裏面有沒有”Authorization”的http header

- 如果有, 則判斷Authorization裏面的內容是否在用戶列表裏面, Authorization header的典型數據爲”Authorization: Basic jdhaHY0=”, 其中Basic表示基礎認證, jdhaHY0=是base64編碼的”user:passwd”字符串. 如果沒有,或者用戶密碼不對,則返回http code 401頁面給客戶端.

- 標準的http瀏覽器在收到401頁面之後, 應該彈出一個對話框讓用戶輸入帳號密碼; 並在用戶點確認的時候再次發出請求, 這次請求裏面將帶上Authorization header.

一次典型的訪問場景是:

  • 瀏覽器發送http請求(沒有Authorization header)

  • 服務器端返回401頁面

  • 瀏覽器彈出認證對話框

  • 用戶輸入帳號密碼,並點確認

  • 瀏覽器再次發出http請求(帶着Authorization header)

  • 服務器端認證通過,並返回頁面

  • 瀏覽器顯示頁面

5.7.2 OkHttp認證

OkHttp會自動重試未驗證的請求. 當響應是 401 Not Authorized 時, Authenticator 會被要求提供證書. Authenticator的實現中需要建立一個新的包含證書的請求. 如果沒有證書可用, 返回null來跳過嘗試.

使用 Response.challenges() 來獲得任何 authentication challenges 的 schemes 和 realms. 當完成一個 Basic challenge , 使用 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());
  }

當認證無法工作時, 爲了避免多次重試, 你可以返回空來放棄認證. 例如, 當 exact credentials 已經嘗試過, 你可能會直接想跳過認證, 可以這樣做:

if (credential.equals(response.request().header("Authorization"))) {    return null; // If we already failed with these credentials, don't retry.
   }

當重試次數超過定義的次數, 你若想跳過認證, 可以這樣做:

if (responseCount(response) >= 3) {    return null; // If we've failed 3 times, give up.
  }  private int responseCount(Response response) {    int result = 1;    while ((response = response.priorResponse()) != null) {
      result++;
    }    return result;
  }


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