Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> OKHTTP 的使用完全解析

OKHTTP 的使用完全解析

編輯:關於Android編程

一、前言

在Android客戶端開發中,使用網絡請求是非常常見的事情,一般我們使用HttpURLConnection是可以滿足需求的,不過隨著業務邏輯復雜,依然還是有很多不便,一個神奇的公司square開源了一個網絡請求庫——Okhttp。隨著Okhttp越來越火,越來越多的人使用Okhttp+retrofit+Rxjava,我們還是很有必要了解一下。本文的實力代碼來自官方wiki。

二、下載配置

現在最新的版本是3.X,android支持2.3+,java應用程序中使用,java最低版本是1.7。
可以通過下載jar包獲取,也可以通過Maven獲取:


  com.squareup.okhttp3
  okhttp
  3.4.1

或者Gradle

compile 'com.squareup.okhttp3:okhttp:3.4.1'

三、基礎使用

Synchronous (同步的) Get
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 創建一個請求對象,我們可以通過這個配置緩存、超時目錄、代理、攔截器等,大多數時候我們只應該創建一個對象,所有的請求可以共用緩存、連接池、 `攔截器等,如下所示:

OkHttpClient client = new OkHttpClient.Builder()
            .readTimeout(8000, TimeUnit.SECONDS)
            .connectTimeout(8000, TimeUnit.SECONDS)
            .writeTimeout(8000, TimeUnit.SECONDS)
            .addInterceptor(new Interceptor())
            .cache(cache)
            .build();

Requests 請求,每個HTTP請求包含一個URL、一個方法(如GET或POST),同時包含頭信息的列表。請求也可能包含一個實體:具體類型內容的數據流。

Responses 響應,根據請求返回的響應碼(成功的200或沒有找到內容的404),頭信息,和可選的實體。

Calls 代表一個實際的http請求,一般調用會執行兩種方式中的一種:

- 同步:執行execute()方法,你的線程被鎖住直到響應返回.
- 異步:執行enqueue()方法,你在任何線程安排請求,在另一個線程獲得響應回調,不會阻塞當前線程,通過Callback 對象的成功和失敗方法獲取響應。

這裡寫圖片描述

以上代碼運行返回如圖所示,上面幾行是響應頭信息,我們可以通過Resbonse的response.headers() 的到Headers對象,我們可以通過索引遍歷獲取響應頭的信息和值,我們不通過遍歷也可以通過responseHeaders.get("Cache-Control"); 獲取,如果響應頭一樣,有多個值返回,我們可以通過`public List values(String name) {} 返回一個集合,如果通過Header對象的get方法獲取只會返回最後一個數據。

下面返回的就是獲得響應體對象response.body() ,調用string() 方法將其轉成string對象,對小文件來說string()方法響應實體是方便和高效的.但如果響應實體很大(大於1 MiB),避免使用string(),因為它會將整個文檔加載到內存中.在這種情況下,更傾向於用流處理實體。 `<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCkFzeW5jaHJvbm91cyjS7LK9KSBHZXQ8YnIgLz4NCrT6wuvI58/Cy/nKvqO6IDwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+DQo8cHJlIGNsYXNzPQ=="brush:java;"> 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()); } }); }

以上代碼異步執行,不會阻塞當前線程,該方法接受一個okhttp3.callback對象,當請求成功後執行callback對象的onResponse方法,我們通過調用isSuccessful() 可以判斷返回的響應碼是否在200和300之間。當請求失敗或取消的會調用callback對象的onFailure 。回調的方法都是執行在工作線程的,不可以直接更新UI。

Accessing Headers 訪問頭信息
典型的HTTP頭信息的工作像一個Map每個字段都有一個值或沒有。但是一些頭信息允許多個值,如 Guava’s Multimap.例如,一個HTTP響應提供多個不同的頭信息是合法和常見的。OkHttp’s APIs試圖使這兩種情況下都方便使用,當寫請求頭信息時,使用header(name, value)為value設置唯一的name。如果values已經存在,他們將被刪除然後添加的新value。使用addHeader(name, value)來添加一個新的頭信息而不需要移除已經存在的頭信息。如下所示:
  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();
Posting a String 上傳字符串
代碼如下所示
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());
  }

post方發接收一個RequestBody 對象,使用一個HTTP POST發送請求實體到服務.這個例子提交一個markdown文檔發送給web服務,將markdown呈現為HTML.因為整個請求實體同時在內存中,避免使用這個API發布大文檔(大於1 MiB).

Post 提交鍵值對
其實提交鍵值對跟上傳表格參數差不多,我們一般都是一個Map集合傳遞近來就是了,不用每次都要一個一個add,如下所示:
private final OkHttpClient client = new OkHttpClient();
    public void run(Map params) throws Exception{
        FormBody.Builder builder = new FormBody.Builder();
        for (Map.Entry entry : params.entrySet()){
            builder.add(entry.getKey(),entry.getValue().toString());
        }
        RequestBody formBody = builder.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());
    }
Post 提交json
public static final MediaType JSON
    = MediaType.parse("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();
  Response response = client.newCall(request).execute();
  return response.body().string();
}
Post Streaming 上傳流
這裡我們發布一個請求實體作為一個流. 這個請求實體被寫的時候它的內容就產生了.這個例子的流直接進入到 Okio 緩沖池.你的項目可能更喜歡一個OutputStream,你可以從BufferedSink.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());
  }
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());
  }
Posting form parameters 上傳表格參數
使用FormBody.Builder構建的請求實體,工作起來就像一個HTML 標簽。名稱和值將使用一種 HTML-compatible 的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());
  }
Posting a multipart request 上傳多部分的請求
MultipartBody.Builder 可以構建兼容HTML文件上傳表單的復雜的請求實體。一個多部分請求的每個部分本身就是一個請求實體,並可以定義自己的頭信息。如果存在的話,這些頭信息應該描述該部分實體,比如它的內容目錄.內容長度和內容類型的頭信息如果可用的話會自動添加。
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());
  }

設置setType 我們一般將該值設置為MultipartBody.FORM ,addFormDataPart(String name, String value) 我們可以向裡面添加鍵值對,addFormDataPart(String name, String filename, RequestBody body) 我們可以向裡面添加文件,addPart 提供了三個重載方法,我們可以通過addPart(Headers headers, RequestBody body)可以在添加RequestBody的時候,同時為其單獨設置請求頭。如下所示:

RequestBody requestBody = new MultipartBuilder()
     .type(MultipartBuilder.FORM)
     .addPart(Headers.of("Content-Disposition", 
              "form-data; name=\"username\""), 
          RequestBody.create(null, "test"))
     .build();
Parse a JSON Response With Gson 用Gson解析一個JSON響應
Gson 是一個方便JSON和Java對象之間互相轉換的API.這裡我們用它來解析一個來自GitHub API的JSON響應.
 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 entry : gist.files.entrySet()) {
      System.out.println(entry.getKey());
      System.out.println(entry.getValue().content);
    }
  }

  static class Gist {
    Map files;
  }

  static class GistFile {
    String content;
  }

上面就是通過Gson解析返回的數據,跟平時用法差不多。

Response Caching 響應緩存
緩存平時我們用的還是挺多的,下面示例代碼演示:
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));
  }

為了緩存響應,你需要一個緩存目錄,你可以讀和寫,並限制緩存的大小。緩存目錄應該是私有的,不受信任的應用程序不應該能夠閱讀其內容,所以我們一般這樣配置,緩存的數據存放在context.getCacheDir()的子目錄中::

final @Nullable File baseDir = context.getCacheDir();
if (baseDir != null) {
  final File cacheDir = new File(baseDir, "HttpResponseCache");
  okHttpClient.setCache(new Cache(cacheDir, 10 * 10 * 1024));
}

第一次請求完成後,Okhttp將請求結果寫入了緩存當中,第一次response1.networkResponse()為請求的值,response1.cacheResponse() 打印的值為null;第二次response2.cacheResponse() 打印的是第一次網絡請求的值,response2.networkResponse() 打印的值是null,說明你第一次走的網絡請求,第二次請求來自於緩存,兩次的值response1Body.equals(response2Body) 返回也是true,很好的驗證上面的說法。我們還可以配置響應頭信息Cache-Control:max-stale=3600 Cache-Control: max-age=9600 ,okhttp也會對配置進行緩存處理,超過時間走網絡請求。禁止一個響應使用緩存,只獲取網絡響應,使用CacheControl.FORCE_NETWORK。禁止一個響應使用網絡,只使用緩存,使用CacheControl.FORCE_CACHE。注意:如果你使用FORCE_CACHE請求緩存,緩存不存在,,OkHttp將返回一個504不可滿足的請求響應。

Canceling a Call 取消一個調用
當一個activity退出的時候,使用 Call.cancel()立即停止一個正在進行中的調用,如果一個線程正在寫一個請求或讀一個響應,它將接收一個 IOException。下面請求服務端會有兩秒延遲,客戶端發出請求1秒的時候,請求還未完成,這個時候終止了請求,拋出了IOException 。
 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);
    }
  }
Timeouts 超時
訪問服務器一般分為3部分,客服端和服務端建立連接connectTimeout,客戶端發送數據到服務端writeTimeout,服務端將相應數據發送到客戶端readTimeout,這三步每一步都有可能耗時,所以我們設置耗時時間非常有必要,超過某一部分時間就要拋出異常。
  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);
  }
Per-call Configuration 每個調用的設置
所有的HTTP客戶端配置在OkHttpClient中,包括代理設置,超時和緩存。當你需要為單個調用改變配置的時候,調用OkHttpClient.newBuilder(),這返回一個builder,分享同樣的連接池,分配器和原始OkHttpClient的配置。在下面的示例中,我們讓一個請求500ms 超時,另一個請求3000 ms超時。
 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);
    }
  }
Handling authentication 處理身份驗證
OkHttp可以自動重試未經身份驗證的請求.當響應401未授權, 一個Authenticator被訪問來提供證書.實現為建立一個新的請求,包含缺失的憑證.如果沒有證書可用,返回null來跳過重試。
 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());
  }

如果用戶名或者密碼有問題,那麼okhttp會一直使用這個錯誤的信息嘗試,那麼我們應該加一個判斷,如果之前用該用戶名和密碼登陸失敗了,就不應該再次登錄:

 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.
  }

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

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

四、總結

Okhttp很強大,我們看一下wiki基本上就可以上手,下一篇我會講解攔截器和Okhttp封裝,讓更簡潔的調用。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved