簡介
OkHttp是一個高效的HTTP客戶端,它有以下默認特性:
- 支持HTTP/2,允許所有同一個主機地址的請求共享同一個socket連接
- 連接池減少請求延時
- 透明的GZIP壓縮減少響應數據的大小
- 緩存響應內容,避免一些完全重複的請求
當網絡出現問題的時候OkHttp依然堅守自己的職責,它會自動恢復一般的連接問題,如果你的服務有多個IP地址,當第一個IP請求失敗時,OkHttp會交替嘗試你配置的其他IP,這對於使用IPv4+IPv6託管在冗餘數據中心中的服務是必需的,OkHttp使用現代TLS技術(TLS 1.3、ALPN、證書固定)初始化新的連接,當握手失敗時會回退到TLS 1.0。
OkHttp官網地址:http://square.github.io/okhttp/
OkHttp GitHub地址:https://github.com/square/okhttp
本文代碼下載地址:GitHub
使用
implementation 'com.squareup.okhttp3:okhttp:3.14.2'
我使用的是3.14.2版本,4.XX.XX版本使用的是Kotlin
需要在清單文件聲明訪問Internet的權限,如果使用緩存,那還得聲明寫外存的權限
同步Get請求
/**
* 異步GET請求:
* new OkHttpClient;
* 構造Request對象;
* 通過前兩步中的對象構建Call對象;
* 通過Call#enqueue(Callback)方法來提交異步請求;
*/
private void asynchronousGetRequests() {
String url = "https://wwww.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder()
.url(url)
.get()//默認就是GET請求
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("asynchronousGetRequests onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
runOnUiThread(() -> tvContent.setText("asynchronousGetRequests onResponse: " + result));
}
});
}
異步發起的請求會被加入到 Dispatcher
中的 runningAsyncCalls
雙端隊列中通過線程池來執行。
響應體的 string() 方法對於小文檔來說十分方便、高效。但是如果響應體太大(超過1MB),應避免適應 string()方法,因爲他會將把整個文檔加載到內存中。對於超過1MB的響應body,應使用流的方式來處理body。
同步Get請求
在Android中應放在子線程中執行,否則有可能引起ANR異常,Android3.0 以後已經不允許在主線程訪問網絡。
/**
* 同步GET請求
* new OkHttpClient;
* 構造Request對象;
* 通過前兩步中的對象構建Call對象;
* 在子線程中通過Call#execute()方法來提交同步請求;
*/
private void synchronizedGetRequests() {
String url = "https://wwww.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder()
.url(url)
.get()//默認就是GET請求
.build();
final Call call = okHttpClient.newCall(request);
new Thread(() -> {
try {
//直接execute call
Response response = call.execute();
String result = response.body().string();
runOnUiThread(() -> tvContent.setText("synchronizedGetRequests run: " + result));
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
POST方式提交String
RequstBody的幾種構造方式:
/**
* POST方式提交String
* 在構造 Request對象時,需要多構造一個RequestBody對象,攜帶要提交的數據。
* 在構造 RequestBody 需要指定MediaType,用於描述請求/響應 body 的內容類型
*/
private void postString() {
MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");
String requestBody = "I am zza.";
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(mediaType, requestBody))
.build();
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("postString onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
StringBuffer buffer = new StringBuffer();
buffer.append("postString: " + "\r\n");
buffer.append(response.protocol() + " " + response.code() + " " + response.message() + "\r\n");
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
buffer.append(headers.name(i) + ":" + headers.value(i) + "\r\n");
}
buffer.append("onResponse: " + response.body().string());
runOnUiThread(() -> tvContent.setText(buffer.toString()));
}
});
}
POST方式提交流
/**
* POST方式提交流
*/
private void postStream() {
RequestBody requestBody = new RequestBody() {
@Nullable
@Override
public MediaType contentType() {
return MediaType.parse("text/x-markdown; charset=utf-8");
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("I am zza.");
}
};
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("postStream onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
StringBuffer buffer = new StringBuffer();
buffer.append("postStream: " + "\r\n");
buffer.append(response.protocol() + " " + response.code() + " " + response.message() + "\r\n");
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
buffer.append(headers.name(i) + ":" + headers.value(i) + "\r\n");
}
buffer.append("onResponse: " + response.body().string());
runOnUiThread(() -> tvContent.setText(buffer.toString()));
}
});
}
POST提交文件
/**
* POST提交文件
* 文件沒有的話會失敗
* 需要在路徑下添加文件
* 還需要權限
*/
private void postFile() {
MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");
OkHttpClient okHttpClient = new OkHttpClient();
String path = Environment.getExternalStorageDirectory().getPath() + "/zza.md";
File file = new File(path);
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(mediaType, file))
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("postFile onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
StringBuffer buffer = new StringBuffer();
buffer.append("postFile: " + "\r\n");
buffer.append(response.protocol() + " " + response.code() + " " + response.message() + "\r\n");
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
buffer.append(headers.name(i) + ":" + headers.value(i) + "\r\n");
}
buffer.append("postFile: " + response.body().string());
runOnUiThread(() -> tvContent.setText(buffer.toString()));
}
});
}
POST方式提交表單
提交表單時,使用 RequestBody
的實現類FormBody
來描述請求體,它可以攜帶一些經過編碼的 key-value
請求體,鍵值對存儲在下面兩個集合中:
private final List<String> encodedNames;
private final List<String> encodedValues;
/**
* POST方式提交表單
* 通過FormBody#Builder構造RequestBody
*/
private void postForm() {
//https://www.wanandroid.com/article/query/0/json
//方法:POST
//參數:
// 頁碼:拼接在鏈接上,從0開始。
// k : 搜索關鍵詞
RequestBody requestBody = new FormBody.Builder()
.add("k", "okhttp")
.build();
Request request = new Request.Builder()
.url("https://www.wanandroid.com/article/query/0/json")
.post(requestBody)
.build();
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("postForm onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
StringBuffer buffer = new StringBuffer();
buffer.append("postForm: " + "\r\n");
buffer.append(response.protocol() + " " + response.code() + " " + response.message() + "\r\n");
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
buffer.append(headers.name(i) + ":" + headers.value(i) + "\r\n");
}
buffer.append("postForm: " + response.body().string());
runOnUiThread(() -> tvContent.setText(buffer.toString()));
}
});
}
POST方式提交分塊請求
MultipartBody 可以構建複雜的請求體,與HTML文件上傳形式兼容。多塊請求體中每塊請求都是一個請求體,可以定義自己的請求頭。這些請求頭可以用來描述這塊請求,例如它的 Content-Disposition
。如果 Content-Length
和 Content-Type
可用的話,他們會被自動添加到請求頭中。
/**
* POST方式提交分塊請求
* <p>
* 使用公司項目的一個接口調試通過,爲避免一些問題,接口和參數刪掉
*/
private void postMultipartBody() {
OkHttpClient client = new OkHttpClient();
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/logo_star_dust.png");
MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
RequestBody filebody = MultipartBody.create(MEDIA_TYPE_PNG, file);
MultipartBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("data", file.getName(), filebody)
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"vin\""),
RequestBody.create(null, ""))
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"iccid\""),
RequestBody.create(null, ""))
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"type\""),
RequestBody.create(null, ""))
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"jobId\""),
RequestBody.create(null, ""))
.build();
Request request = new Request.Builder()
.url("https://www.baidu.com")
.post(body)
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("postMultipartBody onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
runOnUiThread(() -> tvContent.setText("postMultipartBody onResponse: " + result));
}
});
}
攔截器
OkHttp的攔截器鏈可謂是其整個框架的精髓,用戶可傳入的 interceptor
分爲兩類:
-
應用攔截器(Interceptors)
addInterceptor()
- 不必要擔心響應和重定向之間的中間響應。
- 通常只調用一次,即使HTTP響應是通過緩存提供的。
- 遵從應用層的最初目的。與OkHttp的注入頭部無關,如If-None-Match。
- 允許短路而且不調用Chain.proceed()。
- 允許重試和多次調用Chain.proceed()。
-
網絡攔截器(networkInterceptors)
addNetworkInterceptor()
- 允許像重定向和重試一樣操作中間響應。
- 網絡發生短路時不調用緩存響應。
- 在數據被傳遞到網絡時觀察數據。
- 有權獲得裝載請求的連接。
/**
* 攔截器
* <p>
* 請查看打印輸出
*/
private void testInterceptor() {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("https://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("testInterceptor onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
ResponseBody body = response.body();
String result = response.body().string();
if (body != null) {
LogUtil.d("testInterceptor: " + result);
body.close();
}
runOnUiThread(() -> tvContent.setText("testInterceptor onResponse: " + result));
}
});
}
支持緩存
爲了緩存響應,你需要一個你可以讀寫的緩存目錄,和緩存大小的限制。這個緩存目錄應該是私有的,不信任的程序應不能讀取緩存內容。
一個緩存目錄同時擁有多個緩存訪問是錯誤的。大多數程序只需要調用一次new OkHttp(),在第一次調用時配置好緩存,然後其他地方只需要調用這個實例就可以了。否則兩個緩存示例互相干擾,破壞響應緩存,而且有可能會導致程序崩潰。
/**
* 支持緩存
*/
private void responseCaching() {
String url = "https://publicobject.com/helloworld.txt";
File cacheDirectory = new File(Environment.getExternalStorageDirectory() + "/cache");
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(cache)
.build();
final Request request = new Request.Builder()
.url(url)
.get()//默認就是GET請求
.build();
final Call call1 = okHttpClient.newCall(request);
final Call call2 = okHttpClient.newCall(request);
new Thread(() -> {
try {
StringBuffer buffer = new StringBuffer();
//直接execute call
Response response1 = call1.execute();
if (!response1.isSuccessful()) {
runOnUiThread(() -> tvContent.setText("responseCaching onFailure"));
} else {
String result = response1.body().string();
buffer.append("responseCaching1 onResponse: " + result + "\r\n");
buffer.append("responseCaching1 cache response: " + response1.cacheResponse() + "\r\n");
buffer.append("responseCaching1 network response: " + response1.networkResponse() + "\r\n");
runOnUiThread(() -> tvContent.setText(buffer));
}
Response response2 = call2.execute();
if (!response2.isSuccessful()) {
runOnUiThread(() -> tvContent.setText("responseCaching onFailure"));
} else {
String result = response2.body().string();
buffer.append("responseCaching2 onResponse: " + result + "\r\n");
buffer.append("responseCaching2 cache response: " + response2.cacheResponse() + "\r\n");
buffer.append("responseCaching2 network response: " + response2.networkResponse() + "\r\n");
runOnUiThread(() -> tvContent.setText(buffer));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
爲了防止使用緩存的響應,可以用CacheControl.FORCE_NETWORK
。爲了防止它使用網絡,使用CacheControl.FORCE_CACHE
。需要注意的是:如果您使用FORCE_CACHE和網絡的響應需求,OkHttp則會返回一個504提示,告訴你不可滿足請求響應。
取消請求
使用Call.cancel()可以立即停止掉一個正在執行的call。如果一個線程正在寫請求或者讀響應,將會引發IOException。當call沒有必要的時候,使用這個api可以節約網絡資源。例如當用戶離開一個應用時。
不管同步還是異步的call都可以取消。
你可以通過tags來同時取消多個請求。當你構建一請求時,使用RequestBuilder.tag(tag)
來分配一個標籤。之後你就可以用OkHttpClient.cancel(tag)
來取消所有帶有這個tag的call。.
/**
* 取消請求
*/
private void cancelCall() {
final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
final OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://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);
StringBuffer buffer = new StringBuffer();
// Schedule a job to cancel the call in 1 second.
executor.schedule(new Runnable() {
@Override
public void run() {
buffer.append(String.format("%.2f Canceling call.%n",
(System.nanoTime() - startNanos) / 1e9f));
call.cancel();
buffer.append(String.format("%.2f Canceled call.%n",
(System.nanoTime() - startNanos) / 1e9f) );
runOnUiThread(() -> tvContent.setText(buffer));
}
}, 1, TimeUnit.SECONDS);
executor.schedule(new Runnable() {
@Override
public void run() {
buffer.append(String.format("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f) );
try (Response response = call.execute()) {
buffer.append(String.format("%.2f Call was expected to fail, but completed: %s%n",
(System.nanoTime() - startNanos) / 1e9f, response));
} catch (IOException e) {
buffer.append(String.format("%.2f Call failed as expected: %s%n",
(System.nanoTime() - startNanos) / 1e9f, e) );
}
}
}, 0, TimeUnit.SECONDS);
}
超時
OkHttp支持連接,讀取和寫入超時。
/**
* 超時
*/
private void timeout() {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("https://httpbin.org/delay/12")
.get()
.build();
final Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("timeout onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
StringBuffer buffer = new StringBuffer();
buffer.append("timeout onResponse: " + result + "\r\n");
runOnUiThread(() -> tvContent.setText(buffer));
}
});
}
每個call配置
使用OkHttpClient,所有的HTTP Client配置包括代理設置、超時設置、緩存設置。當你需要爲單個call改變配置的時候,clone 一個 OkHttpClient。這個api將會返回一個淺拷貝(shallow copy),你可以用來單獨自定義。
/**
* 每個call配置
*/
private void perCallConfiguration() {
OkHttpClient client = new OkHttpClient.Builder()
.build();
Request request = new Request.Builder()
.url("https://httpbin.org/delay/1")
.get()
.build();
OkHttpClient clientCopy = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
final Call call = clientCopy.newCall(request);
StringBuffer buffer = new StringBuffer();
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("perCallConfiguration onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
buffer.append("perCallConfiguration onResponse: " + result + "\r\n");
runOnUiThread(() -> tvContent.setText(buffer));
}
});
}
處理身份驗證
OkHttp會自動重試未驗證的請求。當響應是401 Not Authorized時,Authenticator會被要求提供證書。Authenticator的實現中需要建立一個新的包含證書的請求。如果沒有證書可用,返回null來跳過嘗試。
/**
* 處理身份驗證
*/
private void handlingAuthentication() {
StringBuffer buffer = new StringBuffer();
OkHttpClient client = new OkHttpClient.Builder()
.authenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
if (response.request().header("Authorization") != null) {
return null; // Give up, we've already attempted to authenticate.
}
buffer.append("Authenticating for response: " + response + "\r\n");
buffer.append("Challenges: " + response.challenges() + "\r\n");
String credential = Credentials.basic("jesse", "password1");
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
})
.build();
Request request = new Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.get()
.build();
final Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> tvContent.setText("handlingAuthentication onFailure: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
buffer.append("handlingAuthentication onResponse: " + result + "\r\n");
runOnUiThread(() -> tvContent.setText(buffer));
}
});
}
其他
-
推薦讓
OkHttpClient
保持單例,用同一個OkHttpClient
實例來執行你的所有請求,因爲每一個OkHttpClient
實例都擁有自己的連接池和線程池,重用這些資源可以減少延時和節省資源,如果爲每個請求創建一個OkHttpClient
實例,顯然就是一種資源的浪費。 -
每一個Call(其實現是RealCall)只能執行一次,否則會報異常,具體參見
RealCall#execute()
歡迎關注我的公衆號,持續分析優質技術文章