Android 網(wǎng)絡(luò)(三) HttpURLConnection OkHttp

參考
Android網(wǎng)絡(luò)請求心路歷程
Android Http接地氣網(wǎng)絡(luò)請求(HttpURLConnection)

一、對比

參考okhttp,retrofit,android-async-http,volley應(yīng)該選擇哪一個

  • HttpURLConnection
    HttpURLConnection是一種多用途、輕量極的HTTP客戶端,使用它來進行HTTP操作可以適用于大多數(shù)的應(yīng)用程序。雖然HttpURLConnection的API提供的比較簡單,但是同時這也使得我們可以更加容易地去使用和擴展它。從Android4.4開始HttpURLConnection的底層實現(xiàn)采用的是okHttp。

  • HttpClient
    Apache HttpClient早就不推薦httpclient,5.0之后干脆廢棄,后續(xù)會刪除。6.0刪除了HttpClient。

  • OkHttp
    okhttp是高性能的http庫,支持同步、異步,而且實現(xiàn)了spdy、http2、websocket協(xié)議,api很簡潔易用,和volley一樣實現(xiàn)了http協(xié)議的緩存。picasso就是利用okhttp的緩存機制實現(xiàn)其文件緩存,實現(xiàn)的很優(yōu)雅,很正確,反例就是UIL(universal image loader),自己做的文件緩存,而且不遵守http緩存機制。

  • volley
    volley是一個簡單的異步http庫,僅此而已。缺點是不支持同步,這點會限制開發(fā)模式。自帶緩存,支持自定義請求。不適合大文件上傳和下載。
    Volley在Android 2.3及以上版本,使用的是HttpURLConnection,而在Android 2.2及以下版本,使用的是HttpClient。
    Volley自己的定位是輕量級網(wǎng)絡(luò)交互,適合大量的,小數(shù)據(jù)傳輸。
    不過再怎么封裝Volley在功能拓展性上始終無法與OkHttp相比。Volley停止了更新,而OkHttp得到了官方的認可,并在不斷優(yōu)化。

  • android-async-http。
    與volley一樣是異步網(wǎng)絡(luò)庫,但volley是封裝的httpUrlConnection,它是封裝的httpClient,而android平臺不推薦用HttpClient了,所以這個庫已經(jīng)不適合android平臺了。

  • Retrofit
    Retrofit 是一個 RESTful 的 HTTP 網(wǎng)絡(luò)請求框架的封裝。注意這里并沒有說它是網(wǎng)絡(luò)請求框架,主要原因在于網(wǎng)絡(luò)請求的工作并不是 Retrofit 來完成的。Retrofit 2.0 開始內(nèi)置 OkHttp,前者專注于接口的封裝,后者專注于網(wǎng)絡(luò)請求的高效,二者分工協(xié)作,宛如古人的『你耕地來我織布』,小日子別提多幸福了。參考深入淺出 Retrofit
    retrofit與picasso一樣都是在okhttp基礎(chǔ)之上做的封裝,項目中可以直接用了。Retrofit因為也是square出的,所以大家可能對它更崇拜些。Retrofit的跟Volley是一個套路,但解耦的更徹底:比方說通過注解來配置請求參數(shù),通過工廠來生成CallAdapter,Converter,你可以使用不同的請求適配器(CallAdapter), 比方說RxJava,Java8, Guava。你可以使用不同的反序列化工具(Converter),比方說json, protobuff, xml, moshi等等。炒雞解耦,里面涉及到超多設(shè)計模式,個人覺得是很經(jīng)典的學(xué)習(xí)案例。雖然支持Java8, Guava你可能也不需要用到。xml,protobuff等數(shù)據(jù)格式你也可能不需要解析。but,萬一遇到鬼了呢。至于性能上,個人覺得這完全取決于請求client,也就是okhttp的性能,跟這些封裝工具沒太大關(guān)系。

二、HttpURLConnection

1.簡單封裝


public class NetUtils
{
    public static String post(String url, String content) {
        HttpURLConnection conn = null;
        try {
            // 創(chuàng)建一個URL對象
            URL mURL = new URL(url);
            // 調(diào)用URL的openConnection()方法,獲取HttpURLConnection對象
            conn = (HttpURLConnection) mURL.openConnection();
            
            conn.setRequestMethod("POST");// 設(shè)置請求方法為post
            conn.setReadTimeout(5000);// 設(shè)置讀取超時為5秒
            conn.setConnectTimeout(10000);// 設(shè)置連接網(wǎng)絡(luò)超時為10秒
            conn.setDoOutput(true);// 設(shè)置此方法,允許向服務(wù)器輸出內(nèi)容
            
            // post請求的參數(shù)
            String data = content;
            // 獲得一個輸出流,向服務(wù)器寫數(shù)據(jù),默認情況下,系統(tǒng)不允許向服務(wù)器輸出內(nèi)容
            OutputStream out = conn.getOutputStream();// 獲得一個輸出流,向服務(wù)器寫數(shù)據(jù)
            out.write(data.getBytes());
            out.flush();
            out.close();
            
            int responseCode = conn.getResponseCode();// 調(diào)用此方法就不必再使用conn.connect()方法
            if (responseCode == 200) {
                InputStream is = conn.getInputStream();
                String response = getStringFromInputStream(is);
                return response;
            } else {
                throw new NetworkErrorException("response status is "+responseCode);
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.disconnect();// 關(guān)閉連接
            }
        }
        
        return null;
    }
    
    public static String get(String url) {
        HttpURLConnection conn = null;
        try {
            // 利用string url構(gòu)建URL對象
            URL mURL = new URL(url);
            conn = (HttpURLConnection) mURL.openConnection();
            
            conn.setRequestMethod("GET");
            conn.setReadTimeout(5000);
            conn.setConnectTimeout(10000);
            
            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                
                InputStream is = conn.getInputStream();
                String response = getStringFromInputStream(is);
                return response;
            } else {
                throw new NetworkErrorException("response status is "+responseCode);
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            
            if (conn != null) {
                conn.disconnect();
            }
        }
        
        return null;
    }
    
    private static String getStringFromInputStream(InputStream is) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        // 模板代碼 必須熟練
        byte[] buffer = new byte[1024];
        int len = -1;
        while ((len = is.read(buffer)) != -1) {
            os.write(buffer, 0, len);
        }
        is.close();
        String state = os.toString();// 把流中的數(shù)據(jù)轉(zhuǎn)換成字符串,采用的編碼是utf-8(模擬器默認編碼)
        os.close();
        return state;
    }
}

注意網(wǎng)絡(luò)權(quán)限!被坑了多少次。
<uses-permission android:name="android.permission.INTERNET"/>

異步通常伴隨者他的好基友回調(diào)。這是通過回調(diào)封裝的Utils類。

public class AsynNetUtils {
    public interface Callback{
        void onResponse(String response);
    }
    
    public static void get(final String url, final Callback callback){
        final Handler handler = new Handler();
        new Thread(new Runnable() {
            @Override
            public void run() {
                final String response = NetUtils.get(url);
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onResponse(response);
                    }
                });
            }
        }).start();
    }
    
    public static void post(final String url, final String content, final Callback callback){
        final Handler handler = new Handler();
        new Thread(new Runnable() {
            @Override
            public void run() {
                final String response = NetUtils.post(url,content);
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onResponse(response);
                    }
                });
            }
        }).start();
    }
}

使用方法

private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    textView = (TextView) findViewById(R.id.webview);
    AsynNetUtils.get("http://www.baidu.com", new AsynNetUtils.Callback() {
        @Override
        public void onResponse(String response) {
            textView.setText(response);
        }
    });
}

嗯,一個蠢到哭的網(wǎng)絡(luò)請求方案成型了。愚蠢的地方有很多:

  • 每次都new Thread,new Handler消耗過大
  • 沒有異常處理機制
  • 沒有緩存機制
  • 沒有完善的API(請求頭,參數(shù),編碼,攔截器等)與調(diào)試模式
  • 沒有Https

2.HTTP緩存機制
參考HTTP緩存機制
緩存對于移動端是非常重要的存在。

  • 減少請求次數(shù),減小服務(wù)器壓力.
  • 本地數(shù)據(jù)讀取速度更快,讓頁面不會空白幾百毫秒。
  • 在無網(wǎng)絡(luò)的情況下提供數(shù)據(jù)。
流程圖
三、OkHttp

參考
OkHttp使用介紹
Okhttp使用詳解
OKHttp源碼解析

先看一下四大核心類:OkHttpClient、Request、Call 和 Response。
1.OkHttpClient
OkHttpClient表示了HTTP請求的客戶端類,在絕大多數(shù)的App中,我們只應(yīng)該執(zhí)行一次new OkHttpClient(),將其作為全局的實例進行保存,從而在App的各處都只使用這一個實例對象,這樣所有的HTTP請求都可以共用Response緩存、共用線程池以及共用連接池。

默認情況下,直接執(zhí)行OkHttpClient client = new OkHttpClient()就可以實例化一個OkHttpClient對象。

可以配置OkHttpClient的一些參數(shù),比如超時時間、緩存目錄、代理、Authenticator等,那么就需要用到內(nèi)部類OkHttpClient.Builder,設(shè)置如下所示:

OkHttpClient client = new OkHttpClient.Builder().
        readTimeout(30, TimeUnit.SECONDS).
        cache(cache).
        proxy(proxy).
        authenticator(authenticator).
        build();

OkHttpClient本身不能設(shè)置參數(shù),需要借助于其內(nèi)部類Builder設(shè)置參數(shù),參數(shù)設(shè)置完成后,調(diào)用Builder的build方法得到一個配置好參數(shù)的OkHttpClient對象。這些配置的參數(shù)會對該OkHttpClient對象所生成的所有HTTP請求都有影響。

有時候我們想單獨給某個網(wǎng)絡(luò)請求設(shè)置特別的幾個參數(shù),比如只想讓某個請求的超時時間設(shè)置為一分鐘,但是還想保持OkHttpClient對象中的其他的參數(shù)設(shè)置,那么可以調(diào)用OkHttpClient對象的newBuilder()方法,代碼如下所示:

OkHttpClient client = ...

OkHttpClient clientWith60sTimeout = client.newBuilder().
        readTimeout(60, TimeUnit.SECONDS).
        build();

clientWith60sTimeout中的參數(shù)來自于client中的配置參數(shù),只不過它覆蓋了讀取超時時間這一個參數(shù),其余參數(shù)與client中的一致。

2.Request
Request類封裝了請求報文信息:請求的Url地址、請求的方法(如GET、POST等)、各種請求頭(如Content-Type、Cookie)以及可選的請求體。一般通過內(nèi)部類Request.Builder的鏈式調(diào)用生成Request對象。

3.Call
Call代表了一個實際的HTTP請求,它是連接Request和Response的橋梁,通過Request對象的newCall()方法可以得到一個Call對象。Call對象既支持同步獲取數(shù)據(jù),也可以異步獲取數(shù)據(jù)。

執(zhí)行Call對象的execute()方法,會阻塞當(dāng)前線程去獲取數(shù)據(jù),該方法返回一個Response對象。
執(zhí)行Call對象的enqueue()方法,不會阻塞當(dāng)前線程,該方法接收一個Callback對象,當(dāng)異步獲取到數(shù)據(jù)之后,會回調(diào)執(zhí)行Callback對象的相應(yīng)方法。如果請求成功,則執(zhí)行Callback對象的onResponse方法,并將Response對象傳入該方法中;如果請求失敗,則執(zhí)行Callback對象的onFailure方法。

4.Response
Response類封裝了響應(yīng)報文信息:狀態(tài)嗎(200、404等)、響應(yīng)頭(Content-Type、Server等)以及可選的響應(yīng)體。可以通過Call對象的execute()方法獲得Response對象,異步回調(diào)執(zhí)行Callback對象的onResponse方法時也可以獲取Response對象。

5.同步GET
以下示例演示了如何同步發(fā)送GET請求,輸出響應(yīng)頭以及將響應(yīng)體轉(zhuǎn)換為字符串。

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

下面對以上代碼進行簡單說明:

client執(zhí)行newCall方法會得到一個Call對象,表示一個新的網(wǎng)絡(luò)請求。Call對象的execute方法是同步方法,會阻塞當(dāng)前線程,其返回Response對象。

通過Response對象的isSuccessful()方法可以判斷請求是否成功。通過Response的headers()方法可以得到響應(yīng)頭Headers對象,可以通過for循環(huán)索引遍歷所有的響應(yīng)頭的名稱和值??梢酝ㄟ^Headers.name(index)方法獲取響應(yīng)頭的名稱,通過Headers.value(index)方法獲取響應(yīng)頭的值。

除了索引遍歷,通過Headers.get(headerName)方法也可以獲取某個響應(yīng)頭的值,比如通過headers.get(“Content-Type”)獲得服務(wù)器返回給客戶端的數(shù)據(jù)類型。但是服務(wù)器返回給客戶端的響應(yīng)頭中有可能有多個重復(fù)名稱的響應(yīng)頭,比如在某個請求中,服務(wù)器要向客戶端設(shè)置多個Cookie,那么會寫入多個Set-Cookie響應(yīng)頭,且這些Set-Cookie響應(yīng)頭的值是不同的,訪問百度首頁,可以看到有7個Set-Cookie的響應(yīng)頭,如下圖所示:

Paste_Image.png

為了解決同時獲取多個name相同的響應(yīng)頭的值,Headers中提供了一個public List<String> values(String name)方法,該方法會返回一個List<String>對象,所以此處通過Headers對象的values(‘Set-Cookie’)可以獲取全部的Cookie信息,如果調(diào)用Headers對象的get(‘Set-Cookie’)方法,那么只會獲取最后一條Cookie信息。

通過Response對象的body()方法可以得到響應(yīng)體ResponseBody對象,調(diào)用其string()方法可以很方便地將響應(yīng)體中的數(shù)據(jù)轉(zhuǎn)換為字符串,該方法會將所有的數(shù)據(jù)放入到內(nèi)存之中,所以如果數(shù)據(jù)超過1M,最好不要調(diào)用string()方法以避免占用過多內(nèi)存,這種情況下可以考慮將數(shù)據(jù)當(dāng)做Stream流處理。

6.異步GET
以下示例演示了如何異步發(fā)送GET網(wǎng)絡(luò)請求,代碼如下所示:

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

下面對以上代碼進行一下說明:

要想異步執(zhí)行網(wǎng)絡(luò)請求,需要執(zhí)行Call對象的enqueue方法,該方法接收一個okhttp3.Callback對象,enqueue方法不會阻塞當(dāng)前線程,會新開一個工作線程,讓實際的網(wǎng)絡(luò)請求在工作線程中執(zhí)行。一般情況下這個工作線程的名字以“Okhttp”開頭,并包含連接的host信息,比如上面例子中的工作線程的name是Okhttp http://publicobject.com/...

當(dāng)異步請求成功后,會回調(diào)Callback對象的onResponse方法,在該方法中可以獲取Response對象。當(dāng)異步請求失敗或者調(diào)用了Call對象的cancel方法時,會回調(diào)Callback對象的onFailure方法。onResponse和onFailure這兩個方法都是在工作線程中執(zhí)行的。

7.請求頭和響應(yīng)頭
典型的HTTP請求頭、響應(yīng)頭都是類似于Map<String, String>,每個name對應(yīng)一個value值。不過像我們之前提到的,也會存在多個name重復(fù)的情況,比如相應(yīng)結(jié)果中就有可能存在多個Set-Cookie響應(yīng)頭,同樣的,也可能同時存在多個名稱一樣的請求頭。響應(yīng)頭的讀取我們在上文已經(jīng)說過了,在此不再贅述。一般情況下,我們只需要調(diào)用header(name, value)方法就可以設(shè)置請求頭的name和value,調(diào)用該方法會確保整個請求頭中不會存在多個名稱一樣的name。如果想添加多個name相同的請求頭,應(yīng)該調(diào)用addHeader(name, value)方法,這樣可以添加重復(fù)name的請求頭,其value可以不同,例如如下所示:

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

上面的代碼通過addHeader方法添加了兩個Accept請求頭,且二者的值不同,這樣服務(wù)器收到客戶端發(fā)來的請求后,就知道客戶端既支持application/json類型的數(shù)據(jù),也支持application/vnd.github.v3+json類型的數(shù)據(jù)。

8.用POST發(fā)送String

可以使用POST方法發(fā)送請求體。下面的示例演示了如何將markdown文本作為請求體發(fā)送到服務(wù)器,服務(wù)器會將其轉(zhuǎn)換成html文檔,并發(fā)送給客戶端。

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

下面對以上代碼進行說明:
Request.Builder的post方法接收一個RequestBody對象。

RequestBody就是請求體,一般可通過調(diào)用該類的5個重載的static的create()方法得到RequestBody對象。create()方法第一個參數(shù)都是MediaType類型,create()方法的第二個參數(shù)可以是String、File、byte[]或okio.ByteString。除了調(diào)用create()方法,還可以調(diào)用RequestBody的writeTo()方法向其寫入數(shù)據(jù),writeTo()方法一般在用POST發(fā)送Stream流的時候使用。

MediaType指的是要傳遞的數(shù)據(jù)的MIME類型,MediaType對象包含了三種信息:type、subtype以及CharSet,一般將這些信息傳入parse()方法中,這樣就能解析出MediaType對象,比如在上例中text/x-markdown; charset=utf-8,type值是text,表示是文本這一大類;/后面的x-markdown是subtype,表示是文本這一大類下的markdown這一小類;charset=utf-8則表示采用UTF-8編碼。如果不知道某種類型數(shù)據(jù)的MIME類型,可以參見連接Media Types和MIME 參考手冊,較詳細地列出了所有的數(shù)據(jù)的MIME類型。以下是幾種常見數(shù)據(jù)的MIME類型值:

  • json :application/json
  • xml:application/xml
  • png:image/png
  • jpg: image/jpeg
  • gif:image/gif

在該例中,請求體會放置在內(nèi)存中,所以應(yīng)該避免用該API發(fā)送超過1M的數(shù)據(jù)。

9.用POST發(fā)送Stream流
下面的示例演示了如何使用POST發(fā)送Stream流。

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

下面對以上代碼進行說明:
以上代碼在實例化RequestBody對象的時候重寫了contentType()和writeTo()方法。
覆寫contentType()方法,返回markdown類型的MediaType。
覆寫writeTo()方法,該方法會傳入一個Okia的BufferedSink類型的對象,可以通過BufferedSink的各種write方法向其寫入各種類型的數(shù)據(jù),此例中用其writeUtf8方法向其中寫入UTF-8的文本數(shù)據(jù)。也可以通過它的outputStream()方法,得到輸出流OutputStream,從而通過OutputSteram向BufferedSink寫入數(shù)據(jù)。

10.用POST發(fā)送File
下面的代碼演示了如何用POST發(fā)送文件。

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

我們之前提到,RequestBody.create()靜態(tài)方法可以接收File參數(shù),將File轉(zhuǎn)換成請求體,將其傳遞給post()方法。

11.用POST發(fā)送Form表單中的鍵值對
如果想用POST發(fā)送鍵值對字符串,可以使用在post()方法中傳入FormBody對象,F(xiàn)ormBody繼承自RequestBody,類似于Web前端中的Form表單。可以通過FormBody.Builder構(gòu)建FormBody。示例代碼如下所示:

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

需要注意的是,在發(fā)送數(shù)據(jù)之前,Android會對非ASCII碼字符調(diào)用encodeURIComponent方法進行編碼,例如”Jurassic Park”會編碼成”Jurassic%20Park”,其中的空格符被編碼成%20了,服務(wù)器端會其自動解碼。

12.用POST發(fā)送multipart數(shù)據(jù)
我們可以通過Web前端的Form表單上傳一個或多個文件,Okhttp也提供了對應(yīng)的功能,如果我們想同時發(fā)送多個Form表單形式的文件,就可以使用在post()方法中傳入MultipartBody對象。MultipartBody繼承自RequestBody,也表示請求體。只不過MultipartBody的內(nèi)部是由多個part組成的,每個part就單獨包含了一個RequestBody請求體,所以可以把MultipartBody看成是一個RequestBody的數(shù)組,而且可以分別給每個RequestBody單獨設(shè)置請求頭。
示例代碼如下所示:

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

下面對以上代碼進行說明:

MultipartBody要通過其內(nèi)部類MultipartBody.Builder進行構(gòu)建。

通過MultipartBody.Builder的setType()方法設(shè)置MultipartBody的MediaType類型,一般情況下,將該值設(shè)置為MultipartBody.FORM,即W3C定義的multipart/form-data類型,詳見Forms in HTML documents。

通過MultipartBody.Builder的方法addFormDataPart(String name, String value)或addFormDataPart(String name, String filename, RequestBody body)添加數(shù)據(jù),其中前者添加的是字符串鍵值對數(shù)據(jù),后者可以添加文件。

MultipartBody.Builder還提供了三個重載的addPart方法,其中通過addPart(Headers headers, RequestBody body)方法可以在添加RequestBody的時候,同時為其單獨設(shè)置請求頭。

13.用Gson處理JSON響應(yīng)
Gson是Google開源的一個用于進行JSON處理的Java庫,通過Gson可以很方面地在JSON和Java對象之間進行轉(zhuǎn)換。我們可以將Okhttp和Gson一起使用,用Gson解析返回的JSON結(jié)果。
下面的示例代碼演示了如何使用Gson解析GitHub API的返回結(jié)果。

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

下面對以上代碼進行說明:

訪問GitHub的https://api.github.com/gists/c2a7c39532239ff261be的返回結(jié)果如下所示:

Paste_Image.png

Gist類對應(yīng)著整個JSON的返回結(jié)果,Gist中的Map<String, GistFile> files對應(yīng)著JSON中的files。

files中的每一個元素都是一個key-value的鍵值對,key是String類型,value是GistFile類型,并且GistFile中必須包含一個String content。OkHttp.txt就對應(yīng)著一個GistFile對象,其content值就是GistFile中的content。

通過代碼Gist gist = gson.fromJson(response.body().charStream(), Gist.class),將JSON字符串轉(zhuǎn)換成了Java對象。將ResponseBody的charStream方法返回的Reader傳給Gson的fromJson方法,然后傳入要轉(zhuǎn)換的Java類的class。

14.緩存響應(yīng)結(jié)果
如果想緩存響應(yīng)結(jié)果,我們就需要為Okhttp配置緩存目錄,Okhttp可以寫入和讀取該緩存目錄,并且我們需要限制該緩存目錄的大小。Okhttp的緩存目錄應(yīng)該是私有的,不能被其他應(yīng)用訪問。

Okhttp中,多個緩存實例同時訪問同一個緩存目錄會出錯,大部分的應(yīng)用只應(yīng)該調(diào)用一次new OkHttpClient(),然后為其配置緩存目錄,然后在App的各個地方都使用這一個OkHttpClient實例對象,否則兩個緩存實例會互相影響,導(dǎo)致App崩潰。

緩存示例代碼如下所示:

private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    String okhttpCachePath = getCacheDir().getPath() + File.separator + "okhttp";
    File okhttpCache = new File(okhttpCachePath);
    if(!okhttpCache.exists()){
        okhttpCache.mkdirs();
    }

    Cache cache = new Cache(okhttpCache, 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));
  }

下面對以上代碼進行說明:

我們在App的cache目錄下創(chuàng)建了一個子目錄okhttp,將其作為Okhttp專門用于緩存的目錄,并設(shè)置其上限為10M,Okhttp需要能夠讀寫該目錄。

不要讓多個緩存實例同時訪問同一個緩存目錄,因為多個緩存實例會相互影響,導(dǎo)致出錯,甚至系統(tǒng)崩潰。在絕大多數(shù)的App中,我們只應(yīng)該執(zhí)行一次new OkHttpClient(),將其作為全局的實例進行保存,從而在App的各處都只使用這一個實例對象,這樣所有的HTTP請求都可以共用Response緩存。

上面代碼,我們對于同一個URL,我們先后發(fā)送了兩個HTTP請求。第一次請求完成后,Okhttp將請求到的結(jié)果寫入到了緩存目錄中,進行了緩存。response1.networkResponse()返回了實際的數(shù)據(jù),response1.cacheResponse()返回了null,這說明第一次HTTP請求的得到的響應(yīng)是通過發(fā)送實際的網(wǎng)絡(luò)請求,而不是來自于緩存。然后對同一個URL進行了第二次HTTP請求,response2.networkResponse()返回了null,response2.cacheResponse()返回了緩存數(shù)據(jù),這說明第二次HTTP請求得到的響應(yīng)來自于緩存,而不是網(wǎng)絡(luò)請求。

如果想讓某次請求禁用緩存,可以調(diào)用
request.cacheControl(CacheControl.FORCE_NETWORK)方法,這樣即便緩存目錄有對應(yīng)的緩存,也會忽略緩存,強制發(fā)送網(wǎng)絡(luò)請求,這對于獲取最新的響應(yīng)結(jié)果很有用。如果想強制某次請求使用緩存的結(jié)果,可以調(diào)用
request.cacheControl(CacheControl.FORCE_CACHE),這樣不會發(fā)送實際的網(wǎng)絡(luò)請求,而是讀取緩存,即便緩存數(shù)據(jù)過期了,也會強制使用該緩存作為響應(yīng)數(shù)據(jù),如果緩存不存在,那么就返回504 Unsatisfiable Request錯誤。也可以向請求中中加入類似于Cache-Control: max-stale=3600之類的請求頭,Okhttp也會使用該配置對緩存進行處理。

15.取消請求
當(dāng)請求不再需要的時候,我們應(yīng)該中止請求,比如退出當(dāng)前的Activity了,那么在Activity中發(fā)出的請求應(yīng)該被中止??梢酝ㄟ^調(diào)用Call的cancel方法立即中止請求,如果線程正在寫入Request或讀取Response,那么會拋出IOException異常。同步請求和異步請求都可以被取消。
示例代碼如下所示:

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

  public void run() throws Exception {
    // This URL is served with a 2 second delay.
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2")
        .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);
    }
  }

上述請求,服務(wù)器端會有兩秒的延時,在客戶端發(fā)出請求1秒之后,請求還未完成,這時候通過cancel方法中止了Call,請求中斷,并觸發(fā)IOException
異常。

16.設(shè)置超時
一次HTTP請求實際上可以分為三步:

  • 客戶端與服務(wù)器建立連接
  • 客戶端發(fā)送請求數(shù)據(jù)到服務(wù)器,即數(shù)據(jù)上傳
  • 服務(wù)器將響應(yīng)數(shù)據(jù)發(fā)送給客戶端,即數(shù)據(jù)下載

由于網(wǎng)絡(luò)、服務(wù)器等各種原因,這三步中的每一步都有可能耗費很長時間,導(dǎo)致整個HTTP請求花費很長時間或不能完成。

為此,可以通過OkHttpClient.Builder的connectTimeout()方法設(shè)置客戶端和服務(wù)器建立連接的超時時間,通過writeTimeout()方法設(shè)置客戶端上傳數(shù)據(jù)到服務(wù)器的超時時間,通過readTimeout()方法設(shè)置客戶端從服務(wù)器下載響應(yīng)數(shù)據(jù)的超時時間。

在2.5.0版本之前,Okhttp默認不設(shè)置任何的超時時間,從2.5.0版本開始,Okhttp將連接超時、寫入超時(上傳數(shù)據(jù))、讀取超時(下載數(shù)據(jù))的超時時間都默認設(shè)置為10秒。如果HTTP請求需要更長時間,那么需要我們手動設(shè)置超時時間。

示例代碼如下所示:

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 {
    // This URL is served with a 2 second delay.
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") 
        .build();

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

如果HTTP請求的某一部分超時了,那么就會觸發(fā)異常。

17.處理身份驗證
有些網(wǎng)絡(luò)請求是需要用戶名密碼登錄的,如果沒提供登錄需要的信息,那么會得到401 Not Authorized未授權(quán)的錯誤,這時候Okhttp會自動查找是否配置了Authenticator,如果配置過Authenticator,會用Authenticator中包含的登錄相關(guān)的信息構(gòu)建一個新的Request,嘗試再次發(fā)送HTTP請求。
示例代碼如下所示:

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

上面對以上代碼進行說明:

OkHttpClient.Builder的authenticator()方法接收一個Authenticator對象,我們需要實現(xiàn)Authenticator對象的authenticate()方法,該方法需要返回一個新的Request對象,該新的Request對象基于原始的Request對象進行拷貝,而且要通過header("Authorization", credential)方法對其設(shè)置登錄授權(quán)相關(guān)的請求頭信息。

通過Response對象的challenges()方法可以得到第一次請求失敗的授權(quán)相關(guān)的信息。如果響應(yīng)碼是401 unauthorized,那么會返回”WWW-Authenticate”相關(guān)信息,這種情況下,要執(zhí)行OkHttpClient.Builder的authenticator()方法,在Authenticator對象的authenticate()中 對新的Request對象調(diào)用header("Authorization", credential)方法,設(shè)置其Authorization請求頭;如果Response的響應(yīng)碼是407 proxy unauthorized,那么會返回”Proxy-Authenticate”相關(guān)信息,表示不是最終的服務(wù)器要求客戶端登錄授權(quán)信息,而是客戶端和服務(wù)器之間的代理服務(wù)器要求客戶端登錄授權(quán)信息,這時候要執(zhí)行OkHttpClient.Builder的proxyAuthenticator()方法,在Authenticator對象的authenticate()中 對新的Request對象調(diào)用header("Proxy-Authorization", credential)方法,設(shè)置其Proxy-Authorization請求頭。

如果用戶名密碼有問題,那么Okhttp會一直用這個錯誤的登錄信息嘗試登錄,我們應(yīng)該判斷如果之前已經(jīng)用該用戶名密碼登錄失敗了,就不應(yīng)該再次登錄,這種情況下需要讓Authenticator對象的authenticate()方法返回null,這就避免了沒必要的重復(fù)嘗試,代碼片段如下所示:

if (credential.equals(response.request().header("Authorization"))) {
   return null; 
}

18.ResponseBody
通過Response的body()方法可以得到響應(yīng)體ResponseBody,響應(yīng)體必須最終要被關(guān)閉,否則會導(dǎo)致資源泄露、App運行變慢甚至崩潰。

ResponseBody和Response都實現(xiàn)了Closeable和AutoCloseable接口,它們都有close()方法,Response的close()方法內(nèi)部直接調(diào)用了ResponseBody的close()方法,無論是同步調(diào)用execute()還是異步回調(diào)onResponse(),最終都需要關(guān)閉響應(yīng)體,可以通過如下方法關(guān)閉響應(yīng)體:

  • Response.close()
  • Response.body().close()
  • Response.body().source().close()
  • Response.body().charStream().close()
  • Response.body().byteString().close()
  • Response.body().bytes()
  • Response.body().string()

對于同步調(diào)用,確保響應(yīng)體被關(guān)閉的最簡單的方式是使用try代碼塊,如下所示:

Call call = client.newCall(request);
 try (Response response = call.execute()) {
   ... // Use the response.
 }

將Response response = call.execute()放入到try()的括號之中,由于Response 實現(xiàn)了Closeable和AutoCloseable接口,這樣對于編譯器來說,會隱式地插入finally代碼塊,編譯器會在該隱式的finally代碼塊中執(zhí)行Response的close()方法。

也可以在異步回調(diào)方法onResponse()中,執(zhí)行類似的try代碼塊,try()代碼塊括號中的ResponseBody也實現(xiàn)了Closeable和AutoCloseable接口,這樣編譯器也會在隱式的finally代碼塊中自動關(guān)閉響應(yīng)體,代碼如下所示:

Call call = client.newCall(request);
   call.enqueue(new Callback() {
     public void onResponse(Call call, Response response) throws IOException {
       try (ResponseBody responseBody = response.body()) {
         ... // Use the response.
       }
     }

     public void onFailure(Call call, IOException e) {
       ... // Handle the failure.
     }
   });

響應(yīng)體中的數(shù)據(jù)有可能很大,應(yīng)該只讀取一次響應(yīng)體的數(shù)據(jù)。調(diào)用ResponseBody的bytes()或string()方法會將整個響應(yīng)體數(shù)據(jù)寫入到內(nèi)存中,可以通過source()、byteStream()或charStream()進行流式處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容