OkHttp:Java 平臺上的新一代 HTTP 客戶端

OkHttp 簡介html

OkHttp 庫的設計和實現的首要目標是高效。這也是選擇 OkHttp 的重要理由之一。OkHttp 提供了對最新的 HTTP 協議版本 HTTP/2 和 SPDY 的支持,這使得對同一個主機發出的全部請求均可以共享相同的套接字鏈接。若是 HTTP/2 和 SPDY 不可用,OkHttp 會使用鏈接池來複用鏈接以提升效率。OkHttp 提供了對 GZIP 的默認支持來下降傳輸內容的大小。OkHttp 也提供了對 HTTP 響應的緩存機制,能夠避免沒必要要的網絡請求。當網絡出現問題時,OkHttp 會自動重試一個主機的多個 IP 地址。java

在 Java 程序中使用 OkHttp 很是簡單,只須要在 Maven 的 POM 文件中添加代碼清單 1 中的依賴便可。目前 OkHttp 的最新版本是 2.5.0。web

清單 1. OkHttp 的 Maven 依賴聲明緩存

<!-- https://mvnrepository.com/artifact/com.squareup.okhttp/okhttp -->
        <dependency>
            <groupId>com.squareup.okhttp</groupId>
            <artifactId>okhttp</artifactId>
            <version>2.7.5</version>
        </dependency>服務器

HTTP 鏈接

雖然在使用 OkHttp 發送 HTTP 請求時只須要提供 URL 便可,OkHttp 在實現中須要綜合考慮 3 種不一樣的要素來肯定與 HTTP 服務器之間實際創建的 HTTP 鏈接。這樣作的目的是爲了達到最佳的性能。網絡

首先第一個考慮的要素是 URL 自己。URL 給出了要訪問的資源的路徑。好比 URL http://www.baidu.com 所對應的是百度首頁的 HTTP 文檔。在 URL 中比較重要的部分是訪問時使用的模式,即 HTTP 仍是 HTTPS。這會肯定 OkHttp 所創建的是明文的 HTTP 鏈接,仍是加密的 HTTPS 鏈接。數據結構

第二個要素是 HTTP 服務器的地址,如 baidu.com。每一個地址都有對應的配置,包括端口號,HTTPS 鏈接設置和網絡傳輸協議。同一個地址上的 URL 能夠共享同一個底層 TCP 套接字鏈接。經過共享鏈接能夠有顯著的性能提高。OkHttp 提供了一個鏈接池來複用鏈接。異步

第三個要素是鏈接 HTTP 服務器時使用的路由。路由包括具體鏈接的 IP 地址(經過 DNS 查詢來發現)和所使用的代理服務器。對於 HTTPS 鏈接還包括通信協商時使用的 TLS 版本。對於同一個地址,可能有多個不一樣的路由。OkHttp 在遇到訪問錯誤時會自動嘗試備選路由。ide

當經過 OkHttp 來請求某個 URL 時,OkHttp 首先從 URL 中獲得地址信息,再從鏈接池中根據地址來獲取鏈接。若是在鏈接池中沒有找到鏈接,則選擇一個路由來嘗試鏈接。嘗試鏈接須要經過 DNS 查詢來獲得服務器的 IP 地址,也會用到代理服務器和 TLS 版本等信息。當實際的鏈接創建以後,OkHttp 發送 HTTP 請求並獲取響應。當鏈接出現問題時,OkHttp 會自動選擇另外的路由進行嘗試。這使得 OkHttp 能夠自動處理可能出現的網絡問題。當成功獲取到 HTTP 請求的響應以後,當前的鏈接會被放回到鏈接池中,提供給後續的請求來複用。鏈接池會按期把閒置的鏈接關閉以釋放資源。post

清單 2. OkHttp 最基本的 HTTP 請求

package com.okhttp.test;

import java.io.IOException;

import com.squareup.okhttp.Headers;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class Okhttp {

    public static void main(String[] args) throws IOException {
    
         OkHttpClient client = new OkHttpClient();
         Request request = new Request.Builder()
         .url("http://www.baidu.com")
         .build();
         Response response = client.newCall(request).execute();
            if (!response.isSuccessful()) {
                throw new IOException("服務器端錯誤: " + 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());
    }
}

HTTP 頭處理

HTTP 頭是 HTTP 請求和響應中的重要組成部分。在建立 HTTP 請求時須要設置一些 HTTP 頭。在獲得 HTTP 的響應以後,也會須要對其中包含的 HTTP 頭進行解析。從代碼的角度來講,HTTP 頭的數據結構是 Map<String, List<String>>類型。也就是說,對於每一個 HTTP 頭,可能有多個值。可是大部分 HTTP 頭都只有一個值,只有少部分 HTTP 頭容許多個值。OkHttp 採用了簡單的方式來區分這兩種類型,使得對 HTTP 頭的使用更加簡單。

在設置 HTTP 頭時,使用 header(name, value) 方法來設置 HTTP 頭的惟一值。對同一個 HTTP 頭,屢次調用該方法會覆蓋以前設置的值。使用 addHeader(name, value) 方法來爲 HTTP 頭添加新的值。在讀取 HTTP 頭時,使用 header(name) 方法來讀取 HTTP 頭的最近出現的值。若是該 HTTP 頭只有單個值,則返回該值;若是有多個值,則返回最後一個值。使用 headers(name) 方法來讀取 HTTP 頭的全部值。

代碼清單 3 中使用 header 方法設置了 User-Agent 頭的值,並添加了一個 Accept 頭的值。在進行解析時,經過 header 方法來獲取 Server 頭的單個值,經過 headers 方法來獲取 Set-Cookie 頭的全部值。

package com.okhttp.test;

import java.io.IOException;

import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class Headers {
   public static void main(String[] args) throws IOException {
    OkHttpClient client = new OkHttpClient();
 
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .header("User-Agent", "My super agent")
            .addHeader("Accept", "text/html")
            .build();
 
    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) {
        throw new IOException("服務器端錯誤: " + response);
    }
 
    System.out.println(response.header("Server"));
    System.out.println(response.headers("Set-Cookie"));
   }
}

POST 請求

HTTP POST 和 PUT 請求能夠包含要提交的內容。只須要在建立 Request 對象時,經過 post 和 put 方法來指定要提交的內容便可。代碼清單 4中經過 RequestBody 的 create 方法來建立媒體類型爲 text/plain 的內容並提交。

package com.okhttp.test;

import java.io.IOException;

import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

public class PostString {
   public static void main(String[] args) throws IOException {
    OkHttpClient client = new OkHttpClient();
    MediaType MEDIA_TYPE_TEXT = MediaType.parse("text/plain");
    String postBody = "Hello World";
 
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .post(RequestBody.create(MEDIA_TYPE_TEXT, postBody))
            .build();
 
    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) {
        throw new IOException("服務器端錯誤: " + response);
    }
 
    System.out.println(response.body().string());
   }
}

以 String 類型提交內容只適用於內容比較小的狀況。當請求內容較大時,應該使用流來提交。

代碼清單 5 中給了使用流方式來提交內容的示例。這裏建立了 RequestBody 的一個匿名子類。該子類的 contentType 方法須要返回內容的媒體類型,而 writeTo 方法的參數是一個 BufferedSink 對象。咱們須要作的就是把請求的內容寫入到 BufferedSink 對象便可。

package com.okhttp.test;

import java.io.IOException;

import okio.BufferedSink;

import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

public class PostStream {
   public static void main(String[] args) throws IOException {
    OkHttpClient client = new OkHttpClient();
    final MediaType MEDIA_TYPE_TEXT = MediaType.parse("text/plain");
    final String postBody = "Hello World";
 
    RequestBody requestBody = new RequestBody() {
        @Override
        public MediaType contentType() {
            return MEDIA_TYPE_TEXT;
        }
 
        @Override
        public void writeTo(BufferedSink sink) throws IOException {
            sink.writeUtf8(postBody);
        }
 
 @Override
        public long contentLength() throws IOException {
            return postBody.length();
        }
    };
 
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .post(requestBody)
            .build();
 
    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) {
        throw new IOException("服務器端錯誤: " + response);
    }
 
    System.out.println(response.body().string());
   }
}

若是所要提交的內容來自本地文件,則不須要額外的流操做,只須要經過 RequestBody 的 create 方法,並把 File 類型的對象做爲參數傳入便可。

若是須要模擬 HTML 中的表單提交,能夠經過 FormEncodingBuilder 來建立請求內容,見代碼清單 6

RequestBody formBody = new FormEncodingBuilder()
                .add("query", "Hello")
                .build();

若是須要模擬 HTML 中的文件上傳功能,能夠經過 MultipartBuilder 來建立 multipart 請求內容。

代碼清單 7 中的 multipart 請求添加了一個表單屬性 title 和一個文件 file。

MediaType MEDIA_TYPE_TEXT = MediaType.parse("text/plain");
    RequestBody requestBody = new MultipartBuilder()
        .type(MultipartBuilder.FORM)
        .addPart(
                Headers.of("Content-Disposition", "form-data; name=\"title\""),
                RequestBody.create(null, "測試文檔"))
        .addPart(
                Headers.of("Content-Disposition", "form-data; name=\"file\""),
                RequestBody.create(MEDIA_TYPE_TEXT, new File("input.txt")))
        .build();

響應緩存

OkHttp 能夠對 HTTP 響應的內容在磁盤上進行緩存。在進行 HTTP 請求時,若是該請求的響應已經被緩存並且沒有過時,OkHttp 會直接使用緩存中的響應內容,而不須要真正的發出 HTTP 請求到遠程服務器。在建立緩存時須要指定一個磁盤目錄和緩存的大小。

代碼清單 8 中,建立出 Cache 對象以後,經過 OkHttpClient 的 setCache 進行設置。經過 Response 對象的 cacheResponse 和 networkResponse 方法能夠獲得緩存的響應和從實際的 HTTP 請求獲得的響應。若是該請求的響應來自實際的網絡請求,則 cacheResponse 方法的返回值爲 null;若是響應來自緩存,則 networkResponse 的返回值爲 null。OkHttp 在進行緩存時會遵循 HTTP 協議的要求,所以能夠經過標準的 HTTP 頭 Cache-Control 來控制響應的緩存時間。

package com.okhttp.test;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

import com.squareup.okhttp.Cache;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class CacheResponse {
   public static void main(String[] args) throws IOException {
    int cacheSize = 100 * 1024 * 1024;
    File cacheDirectory = Files.createTempDirectory("cache").toFile();
    Cache cache = new Cache(cacheDirectory, cacheSize);
    OkHttpClient client = new OkHttpClient();
    client.setCache(cache);
 
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .build();
 
    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) {
        throw new IOException("服務器端錯誤: " + response);
    }
 
    System.out.println(response.cacheResponse());
    System.out.println(response.networkResponse());
   }
}

用戶認證

OkHttp 提供了對用戶認證的支持。當 HTTP 響應的狀態代碼是 401 時,OkHttp 會從設置的 Authenticator 對象中獲取到新的 Request 對象並再次嘗試發出請求。Authenticator 接口中的 authenticate 方法用來提供進行認證的 Request 對象,authenticateProxy 方法用來提供對代理服務器進行認證的 Request 對象。代碼清單 9 中新的 Request 對象中添加了 HTTP 基本認證的 Authorization 頭。

OkHttpClient client = new OkHttpClient();
    client.setAuthenticator(new Authenticator() {
    public Request authenticate(Proxy proxy, Response response) throws IOException {
        String credential = Credentials.basic("user", "password");
        return response.request().newBuilder()
                .header("Authorization", credential)
                .build();
    }
     
    public Request authenticateProxy(Proxy proxy, Response response)
    throws IOException {
        return null;
    }
    });

異步調用

OkHttp 除了支持經常使用的同步 HTTP 請求以外,還支持異步 HTTP 請求調用。在使用同步調用時,當前線程會被阻塞,直到 HTTP 請求完成。當同時發出多個 HTTP 請求時,同步調用的性能會比較差。這個時候經過異步調用能夠提升總體的性能。

代碼清單 10 給出了異步調用的示例。在經過 newCall 方法建立一個新的 Call 對象以後,不是經過 execute 方法來同步執行,而是經過 enqueue 方法來添加到執行隊列中。在調用 enqueue 方法時須要提供一個 Callback 接口的實現。在 Callback 接口實現中,經過 onResponse 和 onFailure 方法來處理響應和進行錯誤處理。

package com.okhttp.test;

import java.io.IOException;

import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class AsyncGet {
   public static void main(String[] args) throws IOException {
    OkHttpClient client = new OkHttpClient();
 
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .build();
 
    client.newCall(request).enqueue(new Callback() {
        public void onFailure(Request request, IOException e) {
            e.printStackTrace();
        }
 
        public void onResponse(Response response) throws IOException {
            if (!response.isSuccessful()) {
                throw new IOException("服務器端錯誤: " + response);
            }
 
            System.out.println(response.body().string());
        }
});
   }
}

取消調用

當一個 HTTP 調用執行以後,能夠經過 Call 接口的 cancel 方法來取消該請求。當一個調用被取消以後,等待該請求的響應的代碼會收到 IOException。同步和異步調用均可以被取消。若是須要同時取消多個請求,能夠在建立請求時經過 RequestBuilder 的 tag 方法來爲請求添加一個標籤。在須要時能夠經過 OkHttpClient 的 cancel 方法來取消擁有一個標籤的全部請求。代碼清單 11 中的全部請求都設置了標籤 website,能夠經過 cancel 方法來所有取消。

package com.okhttp.test;

import java.io.IOException;
import java.util.List;

import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class CancelRequest {
private OkHttpClient client = new OkHttpClient();
private String tag = "website";
 
public void sendAndCancel() {
    sendRequests(Lists.newArrayList(
            "http://www.baidu.com",
            "http://www.163.com",
            "http://www.sina.com.cn"));
    client.cancel(this.tag);
}
 
public void sendRequests(List<String> urls) {
    urls.forEach(url -> {
        client.newCall(new Request.Builder()
                    .url(url)
                    .tag(this.tag)
                    .build())
                .enqueue(new SimpleCallback());
    });
}
 
private static class SimpleCallback implements Callback {
 
    public void onFailure(Request request, IOException e) {
        e.printStackTrace();
    }
 
    public void onResponse(Response response) throws IOException {
        System.out.println(response.body().string());
    }
}
 
public static void main(String[] args) throws IOException {
    new CancelRequest().sendAndCancel();
}
}

攔截器

攔截器是 OkHttp 提供的對 HTTP 請求和響應進行統一處理的強大機制。攔截器在實現和使用上相似於 Servlet 規範中的過濾器。多個攔截器能夠連接起來,造成一個鏈條。攔截器會按照在鏈條上的順序依次執行。 攔截器在執行時,能夠先對請求的 Request 對象進行修改;再獲得響應的 Response 對象以後,能夠進行修改以後再返回。

代碼清單 12 中的攔截器 LoggingInterceptor 用來記錄 HTTP 請求和響應的相關信息。Interceptor 接口只包含一個方法 intercept,其參數是 Chain 對象。Chain 對象表示的是當前的攔截器鏈條。經過 Chain 的 request 方法能夠獲取到當前的 Request 對象。在使用完 Request 對象以後,經過 Chain 對象的 proceed 方法來繼續攔截器鏈條的執行。當執行完成以後,能夠對獲得的 Response 對象進行額外的處理。

package com.okhttp.test;

import java.io.IOException;

import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.Interceptor.Chain;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class LoggingInterceptor implements Interceptor {
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
 
    long t1 = System.nanoTime();
    System.out.println(String.format("發送請求: [%s] %s%n%s",
            request.url(), chain.connection(), request.headers()));
 
    Response response = chain.proceed(request);
 
    long t2 = System.nanoTime();
    System.out.println(String.format("接收響應: [%s] %.1fms%n%s",
            response.request().url(), (t2 - t1) / 1e6d, response.headers()));
 
    return response;
}
}

OkHttp 中的攔截器分紅應用和網絡攔截器兩種。應用攔截器對於每一個 HTTP 響應都只會調用一次,能夠經過不調用 Chain.proceed 方法來終止請求,也能夠經過屢次調用 Chain.proceed 方法來進行重試。網絡攔截器對於調用執行中的自動重定向和重試所產生的響應也會被調用,而若是響應來自緩存,則不會被調用。應用和網絡攔截器的添加方式見代碼清單 13

清單 13. 添加應用和網絡攔截器

1

2

3

client.interceptors().add(new LoggingInterceptor()); //添加應用攔截器

 

client.networkInterceptors().add(new LoggingInterceptor()); //添加網絡攔截器

 

OkHttp 做爲一個簡潔高效的 HTTP 客戶端,能夠在 Java 和 Android 程序中使用。相對於 Apache HttpClient 來講,OkHttp 的性能更好,其 API 設計也更加簡單實用。本文對 OkHttp 進行了詳細的介紹,包括同步和異步調用、HTTP GET 和 POST 請求處理、用戶認證、響應緩存和攔截器等。對於新開發的 Java 應用,推薦使用 OkHttp 做爲 HTTP 客戶端。

相關文章
相關標籤/搜索