[計算機網絡/HTTP/網絡請求] Okhttp: 網絡請求框架
序
- 準備改造一套數據源連接框架,需要借助
OkHttp,故記錄之。
本文主要是轉載第1篇參考文獻。
概述:Okhttp
Android中的網絡請求框架,基本是okhttp和Retrofit一統天下,而Retrofit又是以okhttp為基礎。
所以,系統學習okhttp的使用和原理就很有必要了。
-
okhttp是由square公司開發,Android中公認最好用的網絡請求框架,在接口封裝上做的簡單易用, -
它有以下默認特性:
- 支持HTTP/2,允許所有同一個主機地址的請求共享同一個socket連接
- 使用連接池減少請求延時
- 透明的GZIP壓縮減少響應數據的大小
- 緩存響應內容,避免一些完全重復的請求
- 當網絡出現問題的時候OkHttp 會自動恢復一般的連接問題,如果你的服務有多個IP地址,當第一個IP請求失敗時,OkHttp會交替嘗試你配置的其他IP。
安裝指南
Maven依賴
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'com.squareup.okio:okio:1.17.5'
其中
Okio庫是對Java.io和java.nio的補充,以便能夠更加方便,快速的訪問、存儲和處理你的數據。
OkHttp的底層使用該庫作為支持。
另外,如果是在Android上運行,別忘了申請網絡請求權限,如果還使用網絡請求的緩存功能,那么還要申請讀寫外存的權限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
使用指南
get請求
-
get方式中又可以分為2種情況:同步請求和異步請求; -
同步請求在進行請求的時候,當前線程會被阻塞住,直到得到服務器的響應后,后面的代碼才會執行;
-
而異步請求不會阻塞當前線程,它采用了回調的方式,請求是在另一個線程中執行的,不會影響當前的線程。
-
以百度主頁為例,進行Get請求:
同步請求
import okhttp3.Request;
import okhttp3.Call;
import okhttp3.OkHttpClient;
public void getSync() {
//同步請求
OkHttpClient httpClient = new OkHttpClient();
String url = "https://www.baidu.com/";
Request getRequest = new Request.Builder()
.url(url)
.get()
.build();
//準備好請求的Call對象
Call call = httpClient.newCall(getRequest);//只是配置請求,不會觸發網絡操作 (需確保每個 Call 對象只執行一次,避免重復使用)
try {
Response response = call.execute();//必須調用 execute() 或 enqueue() 才會真正發起請求
Log.i(TAG, "okHttpGet run: response:"+ response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
- 首先,創建了
OkHttpClient實例,接著用Request.Builder構建了Request實例、并傳入了百度主頁的url- 然后,
httpClient.newCall方法傳入Request實例生成call,最后在子線程調用call.execute()執行請求獲得結果response。
使用
OkHttp進行get請求,是比較簡單的,只要在構建Request實例時更換url就可以了。
異步請求
call.execute()是同步方法。- 想要在主線程直接使用可以嘛?當然可以,使用
call.enqueue(callback)即可
public void getAsync() {
//異步請求
OkHttpClient httpClient = new OkHttpClient();
String url = "https://www.baidu.com/";
Request getRequest = new Request.Builder()
.url(url)
.get()
.build();
//準備好請求的Call對象
Call call = httpClient.newCall(getRequest);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpGet enqueue: onResponse:" + response.body().string());
ResponseBody body = response.body();
String string = body.string();
byte[] bytes = body.bytes();
InputStream inputStream = body.byteStream();
}
});
}
call.enqueue會異步執行,需要注意的是,2個回調方法onFailure、onResponse是執行在子線程的
所以,如果想要執行UI操作,需要使用Handler或其他方式切換到UI線程。
取消請求
- 每一個
Call對象只能執行一次。
如果想要取消正在執行的請求,可以使用
call.cancel(),通常在離開頁面時都要取消執行的請求的。
結果處理
- 請求回調的兩個方法是指 傳輸層 的失敗和成功。
onFailure通常是connection連接失敗或讀寫超時;onResponse是指成功得從服務器獲取到了結果,但是這個結果的響應碼可能是404、500等,也可能就是200(response.code()的取值)。- 如果
response.code()是200,表示應用層請求成功了。此時我們可以獲取Response的
ResponseBody,這是響應體。
從面看到,可以從ResponseBody獲取string、byte[ ]、InputStream,這樣就可以對結果進行很多操作了。
比如UI上展示string(要用Handler切換到UI線程)、通過InputStream寫入文件等等。
上面異步請求執行后 結果打印如下:
okHttpGet run: response:<!DOCTYPE html> <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css
href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg>
<img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新聞</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地圖</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>視頻</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>貼吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登錄</a> </noscript> <script>document.write('<a + encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登錄</a>'); </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多產品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>關于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必讀</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意見反饋</a> 京ICP證030173號 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
post請求
POST請求將參數放在請求的主體中,不會直接顯示在URL中。Post請求也分為同步和異步方式,和get方式用法相同
同步POST
public void postSync(){//同步請求
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient okHttpClient = new OkHttpClient();
FormBody formBody = new FormBody.Builder()
.add("a","1")
.add("b","2")
.build();
Request request=new Request.Builder()
.post(formBody)
.url("https://www.httpbin.org/post")
.build();
//準備好請求的Call對象
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
Log.i("postSync",response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
異步POST
public void postAsync(){//異步請求
OkHttpClient okHttpClient=new OkHttpClient();
FormBody formBody=new FormBody.Builder() //okhttp3.FormBody extends RequestBody
.add("a","1")
.add("b","2")
.build();
Request request=new Request.Builder()
.post(formBody)
.url("https://www.httpbin.org/post")
.build();
//準備好請求的Call對象
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if(response.isSuccessful()){
Log.i("postAsync",response.body().string());
}
}
});
}
post請求提交多種 MediaType 格式
text/x-markdown : String、文件
post請求與get請求的區別 是在構造Request對象時,需要多構造一個RequestBody對象,用它來攜帶我們要提交的數據,其他都是一樣的。
示例如下:
import okhttp3.MediaType;
import okhttp3.RequestBody;
OkHttpClient httpClient = new OkHttpClient();
MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
String content = "hello!";
RequestBody body = RequestBody.create(contentType, content);
Request getRequest = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(body)
.build();
Call call = httpClient.newCall(getRequest);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
}
});
- 對比
get請求,把構建Request時的get()改成post(body),并傳入RequestBody實例。
RequestBody實例是通過
create方法創建,需要指定請求體內容類型、請求體內容。
這里是傳入了一個指定為markdown格式的文本。
application/json : json
- 傳入
RequestBody的MediaType還可以是其他類型,如客戶端要給后臺發送json字符串、發送一張圖片,那么可以定義為:
- demo1
// RequestBody:jsonBody,json字符串
String json = "jsonString";
RequestBody jsonBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
//RequestBody:fileBody, 上傳文件
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
RequestBody fileBody = RequestBody.create(MediaType.parse("image/png"), file);
- demo2
if(esUrl.endsWith("/_search")){//dsl語法
JSONObject jsonObject = JSON.parseObject(sql);
jsonStr = JSON.toJSONString(jsonObject);
} else {//es opendistro 插件的SQL語法
json.put("query", sql);
jsonStr = JSON.toJSONString(json);
}
OkHttpClient okHttpClient = SpringContextUtil.getBean(OkHttpClient.class);
String credential = Credentials.basic( username, password);// base64({user}:{password})
Request request = new Request.Builder()
.header("Authorization", credential)
.method("POST", RequestBody.create(MediaType.get("application/json"), jsonStr))
.url(esUrl)
.build();
MediaType
Call newCall = okHttpClient.newCall(request);//只是配置請求,不會觸發網絡操作 (需確保每個 Call 對象只執行一次,避免重復使用)
Response response = newCall.execute();//必須調用 execute() 或 enqueue() 才會真正發起請求
if (!response.isSuccessful()) {
ObjectMapper mapper = new ObjectMapper();
String body = new String(response.body().bytes());
logger.error("code:{},message:{},body:{}", response.code(), response.message(), body);
ErrorMessage errorMessage = mapper.readValue(body, ErrorMessage.class);
if (null != errorMessage && errorMessage.getError() != null && errorMessage.getError().getType() != null) {
throw new RuntimeException(ErrorCodeEnum.QUERY_ES_FAIL.getCode(), ErrorCodeEnum.QUERY_ES_FAIL.getMsg() + ", cause that : " + errorMessage.getError().getType());
}
}
application/x-www-form-urlencoded : 提交表單
- 構建RequestBody除了上面的方式,還有它的子類FormBody,FormBody用于提交表單鍵值對,這種能滿足平常開發大部分的需求。
//RequestBody:FormBody,表單鍵值對
RequestBody formBody = new FormBody.Builder()
.add("username", "henry")
.add("password", "666")
.build();
FormBody是通過FormBody.Builder用構建者模式創建,add鍵值對即可。它的contentType在內部已經指定了。
public final class FormBody extends RequestBody {
private static final MediaType CONTENT_TYPE = MediaType.get("application/x-www-form-urlencoded");
...
}
multipart/* : 提交復雜請求體
RequestBody另一個子類MultipartBody,用于post請求提交復雜類型的請求體。
- 復雜請求體可以同時包含多種類型的的請求體數據。
上面介紹的 post請求 string、文件、表單,只有單一類型。
考慮一種場景–注冊場景,用戶填寫完姓名、電話,同時要上傳頭像圖片,這時注冊接口的請求體就需要 接受 表單鍵值對 以及文件了,那么前面講的的post就無法滿足了。
那么就要用到MultipartBody了。
- 源碼
/** An <a >RFC 2387</a>-compliant request body. */
public final class MultipartBody extends RequestBody {
/**
* The "mixed" subtype of "multipart" is intended for use when the body parts are independent and
* need to be bundled in a particular order. Any "multipart" subtypes that an implementation does
* not recognize must be treated as being of subtype "mixed".
*/
public static final MediaType MIXED = MediaType.get("multipart/mixed");
/**
* The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the
* semantics are different. In particular, each of the body parts is an "alternative" version of
* the same information.
*/
public static final MediaType ALTERNATIVE = MediaType.get("multipart/alternative");
/**
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In
* particular, in a digest, the default {@code Content-Type} value for a body part is changed from
* "text/plain" to "message/rfc822".
*/
public static final MediaType DIGEST = MediaType.get("multipart/digest");
/**
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In
* particular, in a parallel entity, the order of body parts is not significant.
*/
public static final MediaType PARALLEL = MediaType.get("multipart/parallel");
/**
* The media-type multipart/form-data follows the rules of all multipart MIME data streams as
* outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who
* fills out the form. Each field has a name. Within a given form, the names are unique.
*/
public static final MediaType FORM = MediaType.get("multipart/form-data");
...
- demo
OkHttpClient httpClient = new OkHttpClient();
// MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
// String content = "hello!";
// RequestBody body = RequestBody.create(contentType, content);
//RequestBody:fileBody,上傳文件
File file = drawableToFile(this, R.mipmap.bigpic, new File("00.jpg"));
RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpg"), file);
//RequestBody:multipartBody, 多類型 (用戶名、密碼、頭像)
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("username", "hufeiyang")
.addFormDataPart("phone", "123456")
.addFormDataPart("touxiang", "00.png", fileBody)
.build();
Request getRequest = new Request.Builder()
.url("http://yun918.cn/study/public/file_upload.php")
.post(multipartBody)
.build();
Call call = httpClient.newCall(getRequest);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i(TAG, "okHttpPost enqueue: \n onFailure:"+ call.request().toString() +"\n body:" +call.request().body().contentType()
+"\n IOException:"+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
}
});
更多 MedisType
MediaType更多類型信息可以查看RFC 2045
- 查看各個文件類型所對應的Content-type字符串,可以訪問:
請求配置項
- 前置問題
- 如何全局設置超時時長?
- 緩存位置、最大緩存大小 呢?
- 考慮有這樣一個需求,我要監控App通過 OkHttp 發出的 所有 原始請求,以及整個請求所耗費的時間,如何做?
這些問題,在OkHttp這里很簡單。把OkHttpClient實例的創建,換成以下方式即可:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.cache(new Cache(getExternalCacheDir(),500 * 1024 * 1024))
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String url = request.url().toString();
Log.i(TAG, "intercept: proceed start: url"+ url+ ", at " + System.currentTimeMillis());
Response response = chain.proceed(request);
ResponseBody body = response.body();
Log.i(TAG, "intercept: proceed end: url"+ url+ ", at " + System.currentTimeMillis());
return response;
}
})
.build();
這里通過
OkHttpClient.Builder通過構建者模式設置了連接、讀取、寫入的超時時長,用cache()方法傳入了由緩存目錄、緩存大小構成的Cache實例,這樣就解決了前兩個問題。
- 使用
addInterceptor()方法添加了Interceptor實例,且重寫了intercept方法。Interceptor意為攔截器,intercept()方法會在開始執行請求時調用。
其中chain.proceed(request)內部是真正請求的過程,是阻塞操作,執行完后會就會得到請求結果ResponseBody
所以chain.proceed(request)的前后取當前時間,那么就知道整個請求所耗費的時間。上面chain.proceed(request)的前后分別打印的日志和時間,這樣第三個問題也解決了。
- 具體
Interceptor是如何工作,后面介紹。另外,通常
OkHttpClient實例是全局唯一的,這樣這些基本配置就是統一,且內部維護的連接池也可以有效復用(后面介紹)。
全局配置的有了,單個請求的也可以有一些單獨的配置。
Request getRequest = new Request.Builder()
.url("http://yun918.cn/study/public/file_upload.php")
.post(multipartBody)
.addHeader("key","value")
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
- 使用
addHeader()方法添加了請求頭。- 使用
cacheControl(CacheControl.FORCE_NETWORK)設置此次請求是能使用網絡,不用緩存。(還可以設置只用緩存FORCE_CACHE)
最佳實踐: MyOkHttpClientConfig
- pom.xml
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1 or 3.14.9[推薦]</version>
</dependency>
- MyOkHttpClientConfig
import lombok.extern.slf4j.Slf4j;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.internal.connection.Transmitter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Configuration
@Slf4j
public class OkHttpClientConfig {
//Socket.setSoTimeout(int timeout)
public final static String OKHTTP_CONNECTTIMEOUT_PARAM = "OKHTTP_CONNECTTIMEOUT";
public final static String OKHTTP_CONNECTTIMEOUT_DEFAULT = "1000";
public final static String OKHTTP_CALLTIMEOUT_PARAM = "OKHTTP_CALLTIMEOUT";
public final static String OKHTTP_CALLTIMEOUT_DEFAULT = "3000";
public final static String OKHTTP_READTIMEOUT_PARAM = "OKHTTP_READTIMEOUT";
public final static String OKHTTP_READTIMEOUT_DEFAULT = "1000";
public final static String OKHTTP_CONNECTIONPOOL_MAXIDLE_PARAM = "OKHTTP_CONNECTIONPOOL_MAXIDLE";
public final static String OKHTTP_CONNECTIONPOOL_MAXIDLE_DEFAULT = "100";
public final static String OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION_PARAM = "OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION";
public final static String OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION_DEFAULT = "30000";
@Bean(name = "okHttpClient")
public OkHttpClient okHttpClient() {
OkHttpClient okHttpClient = buildOkHttpClient( OkHttpClient.class.getSimpleName() + "SpringBean(okHttpClient)");
return okHttpClient;
}
/**
* 構建 OkHttpClient 連接池
* @param caller 可選參數,標明調用方
* @return
*/
public static OkHttpClient buildOkHttpClient(@Nullable String caller){
// MyOkHttpRetryInterceptor.Builder builder = new MyOkHttpRetryInterceptor.Builder()
// .retryInterval(500)
// .executionCount(3);
Map<String, String> env = System.getenv();
long connectTimeout = Long.valueOf( env.getOrDefault(OKHTTP_CONNECTTIMEOUT_PARAM, OKHTTP_CONNECTTIMEOUT_DEFAULT) );
long callTimeout = Long.valueOf( env.getOrDefault(OKHTTP_CALLTIMEOUT_PARAM, OKHTTP_CALLTIMEOUT_DEFAULT) );
long readTimeout = Long.valueOf( env.getOrDefault(OKHTTP_READTIMEOUT_PARAM, OKHTTP_READTIMEOUT_DEFAULT) );
int maxIdleConnections = Integer.valueOf( env.getOrDefault( OKHTTP_CONNECTIONPOOL_MAXIDLE_PARAM, OKHTTP_CONNECTIONPOOL_MAXIDLE_DEFAULT ) );
long keepAliveDuration = Long.valueOf( env.getOrDefault( OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION_PARAM, OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION_DEFAULT ) );
log.info("config({}) | connectTimeout:{}, callTimeout:{}, readTimeout:{}, maxIdleConnections:{}, keepAliveDuration:{}"
, caller==null?"":caller
, connectTimeout, callTimeout, readTimeout, maxIdleConnections, keepAliveDuration
);
return new OkHttpClient().newBuilder()
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.callTimeout(callTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true) //開啟 okhttp 自帶的 重試阻攔器
.followRedirects(true)//重定向
//.addInterceptor( new MyOkHttpRetryInterceptor(builder) )//本重置攔截器,尚不可靠
.connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.MILLISECONDS))
.build();
}
public static OkHttpClient buildOkHttpClient(){
return buildOkHttpClient(null);
}
}
攔截器的使用
OkHttp的攔截器(Interceptors)提供了強大的自定義和修改HTTP請求和響應的能力。- 攔截器允許在發送請求前、收到響應后以及其他階段對HTTP流量進行攔截和處理。
例如:攔截器可以修改請求的URL、請求方法、請求頭部、請求體等。
這對于添加身份驗證頭、設置緩存控制頭等場景很有用。
用法如下:
public void interceptor(){
OkHttpClient okHttpClient=new OkHttpClient.Builder()//添加攔截器的使用OkHttpClient的內部類Builder
.addInterceptor(new Interceptor() {//使用攔截器可以對所有的請求進行統一處理,而不必每個request單獨去處理
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
//前置處理,以proceed方法為分割線:提交請求前
Request request = chain.request().newBuilder()
.addHeader("id", "first request")
.build();
Response response = chain.proceed(request);
//后置處理:收到響應后
return response;
}
})
.addNetworkInterceptor(new Interceptor() {//這個在Interceptor的后面執行,無論添加順序如何
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Log.i("id",chain.request().header("id"));
return chain.proceed(chain.request());
}
})
.cache(new Cache(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()+"/cache"),1024*1024))//添加緩存
.build();
Request request=new Request.Builder()
.url("https://www.httpbin.org/get?a=1&b=2")
.build();
//準備好請求的Call對象
Call call = okHttpClient.newCall(request);
//異步請求
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if(response.isSuccessful()){
Log.i("interceptor",response.body().string());
}
}
});
}
Cookie的使用
- 大家應該有這樣的經歷,就是有些網站的好多功能都需要用戶登錄之后才能訪問,而這個功能可以用
cookie實現:
- 首先,在客戶端登錄之后,服務器給客戶端發送一個cookie,由客戶端保存;
- 然后,客戶端在訪問需要登錄之后才能訪問的功能時,只要攜帶這個cookie,服務器就可以識別該用戶是否登錄。
用法如下
public void cookie(){
Map<String,List<Cookie>> cookies = new HashMap<>();
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cookieJar(new CookieJar() {
@Override
public void saveFromResponse(@NonNull HttpUrl httpUrl, @NonNull List<Cookie> list) {//保存服務器發送過來的cookie
cookies.put("cookies",list);
}
@NonNull
@Override
public List<Cookie> loadForRequest(@NonNull HttpUrl httpUrl) {//請求的時候攜帶cookie
if(httpUrl.equals("www.wanandroid.com")){
return cookies.get("cookies");
}
return new ArrayList<>();
}
})
.build();
FormBody formBody = new FormBody.Builder()
.add("username","ibiubiubiu")
.add("password","Lhh823924.")
.build();
Request request = new Request.Builder() //模擬登錄
.url("https://wanandroid.com/user/lg")
.post(formBody)
.build();
//準備好請求的Call對象
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
Log.i("login",response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
//請求收藏頁面,必須登錄之后才能訪問到
request=new Request.Builder()
.url("https://wanandroid.com/lg/collect")
.build();
//準備好請求的Call對象
call = okHttpClient.newCall(request);
try {
Response response = call.execute();
Log.i("collect",response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
})
.start();
}
連接池 : OkHttp ConnectionPool
基礎概念
-
OkHttp ConnectionPool 是 OkHttp 庫中的一個關鍵組件,用于管理 HTTP 連接的復用。
-
OkHttp ConnectionPool 是一個連接池,用于存儲和管理 HTTP 連接。
它允許在多個請求之間復用連接,從而提高網絡請求的效率。
連接池中的連接可以被多個線程共享,并且會根據配置的策略(如最大空閑連接數、連接超時時間等)來管理連接的生命周期。
優勢
- 提高性能:通過復用連接,減少了每次請求時的握手和連接建立時間。
- 減少資源消耗:避免頻繁地創建和銷毀連接,節省了系統資源。
- 支持并發:允許多個線程同時使用同一個連接,提高了并發處理能力。
參數項
OkHttp ConnectionPool 主要有以下幾種配置參數:
- maxIdleConnections:池中最大空閑連接數。
- keepAliveDuration:連接保持活躍的時間。
- timeUnit:keepAliveDuration 的時間單位。
應用場景
- Web 服務器和客戶端之間的通信:在高并發場景下,使用連接池可以顯著提高響應速度。
- 移動應用網絡請求:在移動應用中,頻繁的網絡請求可以通過連接池優化性能。
- 微服務架構中的服務間調用:在微服務架構中,服務之間的調用可以通過連接池提高效率。
常用方法
//連接數 : Returns total number of connections in the pool.
int : okHttpClient.connectionPool().connectionCount();
//空閑連接數 : Returns the number of idle connections in the pool.
int : okHttpClient.connectionPool().idleConnectionCount()
//關閉所有連接 : Close and remove all idle connections in the pool.
void : okHttpClient.connectionPool().evictAll();
okhttp3.ConnectionPool的 底層實現類:okhttp3.internal.connection.RealConnectionPool
借助連接池可解決的常見問題
Q: 連接池中的連接超時 / 配置連接池
-
原因:可能是由于網絡不穩定或服務器響應慢導致的。
-
解決方法
import okhttp3.ConnectionPool;
ConnectionPool pool = new ConnectionPool(5, 5, TimeUnit.MINUTES);
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(pool)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.build();
Q: 連接池的默認配置
- 連接池默認配置:
- 每個地址的空閑連接數為 5個,每個空閑連接的存活時間為 5分鐘
package okhttp3;
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
Q: 過期連接的處理策略?
- 連接池每次添加一個新的連接時,都會先清理當前連接池中過期的連接,通過清理線程池
executor執行清理任務cleanupRunnable。
Q: 連接池中的連接數過多
-
原因:可能是由于客戶端請求過于頻繁,導致連接池中的連接數超過了設定的最大值。
-
解決方案:
ConnectionPool pool = new ConnectionPool(100, 5, TimeUnit.MINUTES);
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(pool)
.build();
Q: 連接池中的連接無法釋放
-
原因:可能是由于某些請求未正確關閉連接導致的。
-
解決方案:
確保每次請求后都正確關閉響應體:
Response response = client.newCall(request).execute();
try {
// 處理響應
} finally {
response.close();
}
- 綜合案例
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import java.util.concurrent.TimeUnit;
public class OkHttpExample {
public static void main(String[] args) {
ConnectionPool pool = new ConnectionPool(100, 5, TimeUnit.MINUTES);
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(pool)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.build();
// 使用 client 進行網絡請求
}
}
通過合理配置和使用 OkHttp ConnectionPool,可以有效提升網絡請求的性能和穩定性。
參考文獻
綜合應用
案例1:封裝OkHttp單例模式 : OkHttpUtils(自定義)
public class OkHttpUtils {
/**
* 單例模式
*/
private static OkHttpUtils okHttpUtils = null;
private OkHttpUtils() {
}
public static OkHttpUtils getInstance() {
//雙層判斷,同步鎖
if (okHttpUtils == null) {
synchronized (OkHttpUtils.class) {
if(okHttpUtils == null){
okHttpUtils = new OkHttpUtils();
}
}
}
return okHttpUtils;
}
/**
* 單例模式
* 封裝OkhHttp
* synchronized同步方法
*/
private static OkHttpClient okHttpClient = null;
private static synchronized OkHttpClient getOkHttpClient() {
if (okHttpClient == null) {
//攔截器
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
//攔截日志消息
Log.i("henry", "log: " + message);
}
});
//設置日志攔截器模式
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
okHttpClient = new OkHttpClient.Builder()
//日志攔截器
.addInterceptor(interceptor)
//應用攔截器
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.addHeader("source", "android")
.build();
return chain.proceed(request);
}
})
.build();
}
return okHttpClient;
}
/**
* doGet
*/
public void doGet(String url, Callback callback) {
//創建okhttp
OkHttpClient okHttpClient = getOkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
okHttpClient.newCall(request).enqueue(callback);
}
/**
* doPost
*/
public void doPost(String url, Map<String, String> params, Callback callback) {
OkHttpClient okHttpClient = getOkHttpClient();
//請求體
FormBody.Builder formBody = new FormBody.Builder();
for (String key : params.keySet()) {
//遍歷map集合
formBody.add(key, params.get(key));
}
Request request = new Request.Builder()
.url(url)
.post(formBody.build())
.build();
okHttpClient.newCall(request).enqueue(callback);
}
}
實現了單例模式來確保
OkHttpUtils和OkHttpClient實例的唯一性
案例2:配置https的自簽證書和信任所有證書
HTTPS / SSL 證書
HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網絡協議,要比http協議安全。
一般支持
https的網站,都是CA(Certificate Authority)機構頒發的證書,但是一般該機構頒發的證書需要提供費用且有使用時間的限制,到期需要續費。
否則,默認該鏈接是不信任的,通過okHttp無法直接訪問。

但是我們可以使用自簽的方式,通過
JDK自帶的keytool.exe生成一個自己的證書,然后使用該證書內容。
雖然也是會出現提示“不安全”,但是我們可以通過okhttp訪問鏈接。
使用自簽證書
- 將證書文件放置在
assets目錄(也可以放置在其他目錄下,只要能正確讀取到該文件),在創建OkhttpClient對象時sslSocketFactory()將該證書信息添加。
private SSLContext getSLLContext() {
SSLContext sslContext = null;
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
InputStream certificate = mContext.getAssets().open("gdroot-g2.crt");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
String certificateAlias = Integer.toString(0);
keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
sslContext = SSLContext.getInstance("TLS");
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
} catch (CertificateException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return sslContext;
}
信任所有證書
- 通過添加證書的形式,可以實現客戶端訪問Https服務端的功能,但是如果服務端更換證書內容,那么客戶端需要相應的更換https證書,否則無法正常交互獲取不到數據,我們可以通過自定義
X509TrustManager的形式實現來規避所有的證書檢測,實現信任所有證書的目的。
private OkHttpClient getHttpsClient() {
OkHttpClient.Builder okhttpClient = new OkHttpClient().newBuilder();
//信任所有服務器地址
okhttpClient.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//設置為true
return true;
}
});
//創建管理器
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[] {};
}
} };
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
//為OkHttpClient設置sslSocketFactory
okhttpClient.sslSocketFactory(sslContext.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
return okhttpClient.build();
}
- 創建
X509TrustManager對象,并實現其中的方法。由于X509TrustManager是通用證書格式,只需要拿到該格式就行。最后init該安全協議,將其放入okhttp的sslSocketFactory中。- 由于
Retrofit只是對Okhttp網絡接口的封裝。因此實際使用中,該方法同樣適用于Retrofit中。
原理與架構
okhttp 請求過程

FAQ for okhttp
面試可能會問到的問題:
- 簡單說一下okhttp
- okhttp的核心類有哪些?
- okhttp對于網絡請求做了哪些優化,如何實現的?
- okhttp架構中用到了哪些設計模式?
- okhttp攔截器的執行順序
Q: OkHttp3 超時設置(callTimeout/connectTimeout/readTimeout)?
okhttp:3.14.9
場景導入: Socket closed , SocketTimeoutException
假設應用程序使用okhttp3框架時,報如下錯誤,考慮下如何解決?
[TID: xxx.100.xxxx01] [ds-service] [system] [2025/03/13 09:56:44.512] [DEBUG] [http-nio-9527-exec-6] [AbstractQuery] logDatabaseQuerySQL:113 ElasticSearchQuery | database final query sql: select xxx from xxx #other-info# [pageSql]
[TID: xxx.100.xxxx01] [ds-service] [system] [2025/03/13 09:56:45.515] [ERROR] [http-nio-9527-exec-6] [ElasticSearchConnector] post:387 Fail to execute for send post request cause that the exception!url:http://127.0.0.1:9200/_opendistro/_sql?format=json, requestJson:`select xxx from xxx`,exception:
java.net.SocketTimeoutException: timeout
at okio.Okio$4.newTimeoutException(Okio.java:232) ~[okio-1.17.2.jar!/:?]
at okio.AsyncTimeout.exit(AsyncTimeout.java:286) ~[okio-1.17.2.jar!/:?]
at okio.AsyncTimeout$2.read(AsyncTimeout.java:241) ~[okio-1.17.2.jar!/:?]
at okio.RealBufferedSource.indexOf(RealBufferedSource.java:358) ~[okio-1.17.2.jar!/:?]
at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:230) ~[okio-1.17.2.jar!/:?]
at okhttp3.internal.http1.Http1ExchangeCodec.readHeaderLine(Http1ExchangeCodec.java:242) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.java:213) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.java:115) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.java:94) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:43) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:94) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:88) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.RealCall.getResponseWithInterceptorChain$original$BnZzvgYR(RealCall.java:229) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.RealCall.getResponseWithInterceptorChain$original$BnZzvgYR$accessor$VWw5Xe8t(RealCall.java) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.RealCall$auxiliary$aiLFzmy0.call(Unknown Source) ~[okhttp-3.14.9.jar!/:?]
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86) ~[skywalking-agent.jar:8.9.0]
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.RealCall.execute$original$BnZzvgYR(RealCall.java:81) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.RealCall.execute$original$BnZzvgYR$accessor$VWw5Xe8t(RealCall.java) ~[okhttp-3.14.9.jar!/:?]
at okhttp3.RealCall$auxiliary$JZ7wpldV.call(Unknown Source) ~[okhttp-3.14.9.jar!/:?]
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86) ~[skywalking-agent.jar:8.9.0]
at okhttp3.RealCall.execute(RealCall.java) ~[okhttp-3.14.9.jar!/:?]
at com.xxx.common.connector.elasticsearch.ElasticSearchConnector.post(ElasticSearchConnector.java:385) [ds-service-common-1.1.24-SNAPSHOT.jar!/:?]
at com.xxx.common.connector.elasticsearch.ElasticSearchConnector.post(ElasticSearchConnector.java:328) [ds-service-common-1.1.24-SNAPSHOT.jar!/:?]
at com.xxx.common.query.ElasticSearchQuery.post(ElasticSearchQuery.java:116) [ds-service-common-1.1.24-SNAPSHOT.jar!/:?]
at com.xxx.common.query.ElasticSearchQuery.autoPagingQuery(ElasticSearchQuery.java:101) [ds-service-common-1.1.24-SNAPSHOT.jar!/:?]
at com.xxx.common.query.QueryFactory.query(QueryFactory.java:88) [ds-service-common-1.1.24-SNAPSHOT.jar!/:?]
at com.xxx.biz.ds.service.impl.CommonSearchBizServiceImpl.queryDatasourceData(CommonSearchBizServiceImpl.java:398) [classes!/:?]
at com.xxx.biz.ds.service.impl.CommonSearchBizServiceImpl.executeQuery(CommonSearchBizServiceImpl.java:160) [classes!/:?]
at com.xxx.biz.ds.service.impl.CommonSearchBizServiceImpl.executeQuery(CommonSearchBizServiceImpl.java:337) [classes!/:?]
at com.xxx.biz.ds.controller.v2.DataSearchController.datasetQuery$original$Ezz0kp0w(DataSearchController.java:158) [classes!/:?]
at com.xxx.biz.ds.controller.v2.DataSearchController.datasetQuery$original$Ezz0kp0w$accessor$8fPZVSws(DataSearchController.java) [classes!/:?]
at com.xxx.biz.ds.controller.v2.DataSearchController$auxiliary$XARzqMqE.call(Unknown Source) [classes!/:?]
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86) [skywalking-agent.jar:8.9.0]
at com.xxx.biz.ds.controller.v2.DataSearchController.datasetQuery(DataSearchController.java) [classes!/:?]
at com.xxx.biz.ds.controller.v2.DataSearchController$$FastClassBySpringCGLIB$$6bfcdaf9.invoke(<generated>) [classes!/:?]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) [spring-core-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779) [spring-aop-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) [spring-aop-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) [spring-aop-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:119) [spring-context-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) [spring-aop-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) [spring-aop-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) [spring-aop-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at com.xxx.biz.ds.controller.v2.DataSearchController$$EnhancerBySpringCGLIB$$957c5021.datasetQuery(<generated>) [classes!/:?]
at sun.reflect.GeneratedMethodAccessor257.invoke(Unknown Source) ~[?:?]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_422]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_422]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:665) [javax.servlet-api-4.0.1.jar!/:4.0.1]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) [spring-webmvc-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:750) [javax.servlet-api-4.0.1.jar!/:4.0.1]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) [tomcat-embed-websocket-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.skywalking.apm.agent.core.context.TraceMdcWebFilter.doFilter(TraceMdcWebFilter.java:58) [classes!/:8.9.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar!/:?]
at com.xxx.biz.common.filter.AccessPathWebFilter.doFilter(AccessPathWebFilter.java:92) [classes!/:?]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar!/:?]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar!/:?]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar!/:?]
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:97) [spring-boot-actuator-2.3.12.RELEASE.jar!/:2.3.12.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar!/:?]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.15.RELEASE.jar!/:5.2.15.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.StandardHostValve.invoke$original$q8sTk9iR(StandardHostValve.java:143) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.StandardHostValve.invoke$original$q8sTk9iR$accessor$N646g7Eq(StandardHostValve.java) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.StandardHostValve$auxiliary$MLBge40B.call(Unknown Source) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86) [skywalking-agent.jar:8.9.0]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:764) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) [tomcat-embed-core-9.0.46.jar!/:?]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.46.jar!/:?]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_422]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_422]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.46.jar!/:?]
at java.lang.Thread.run(Thread.java:750) [?:1.8.0_422]
Caused by: java.net.SocketException: Socket closed
at java.net.SocketInputStream.read(SocketInputStream.java:204) ~[?:1.8.0_422]
at java.net.SocketInputStream.read(SocketInputStream.java:141) ~[?:1.8.0_422]
at okio.Okio$2.read(Okio.java:140) ~[okio-1.17.2.jar!/:?]
at okio.AsyncTimeout$2.read(AsyncTimeout.java:237) ~[okio-1.17.2.jar!/:?]
... 115 more
OkHttpClient 配置解釋
OkHttpClient httpClient = new OkHttpClient.Builder()
.callTimeout(CALL_TIMEOUT, TimeUnit.MILLISECONDS)
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) //連接超時
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) //讀取超時
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) //寫超時
//.pingInterval(1000, TimeUnit.MILLISECONDS) //此配置項,會定時的向服務器發送一個消息來保持長連接; 只有 http2 和 webSocket 中有使用
.protocols( protocols ) // Protocol.HTTP_1_1 / ...
.retryOnConnectionFailure(true)//連接失敗時是否重試
.followRedirects(true)//重定向
.connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.MILLISECONDS))
//.addNetworkInterceptor(new EncryptInterceptor())
//.addInterceptor( new MyOkHttpRetryInterceptor(builder) )//本重置攔截器,尚不可靠
//.addInterceptor(new CommonHeaderInterceptor())
//.addInterceptor(new CacheInterceptor())
//.addInterceptor(new HttpLoggerInterceptor())
.build();
- 超時參數
- connectTimeout
指的是建立連接所用的時間,適用于網絡狀況正常的情況下,兩端連接所用的時間。
通過跟源碼發現這個值用在了socket.connect(address, connectTimeout);即:最終設置給了socket (確切的說應該是rawSocket)
- readTimeout 最終設置給了rawSocket 以及 在socket基礎上創建的BufferedSource
- writeTimeout 最終設置給了在socket基礎上創建的BufferedSink
- okhttp => http / socket => tcp
okhttp底層基于socket, 故 Timeout 自然也是設置給Socket的 connect / read / write
而socket是對于傳輸層的抽象。我們這里討論的是http, 所以對socket設置各種timeout 其實也就是對于TCP的timeout進行配置
TCP協議(握手/揮手/發包/丟包重傳/滑動窗口/擁塞控制等細節)以及socket屬于前置知識
源碼探究
okhttp采用了責任鏈的設計模式
- 用一條抽象的 Chain 將一堆 Interceptor 串起來
- 從發出 request 到接收 response 的路徑類似于 node.js 中 koa2 的“洋蔥模型”(圖1),而 okhttp 的 Interceptor 作用就相當于koa2中的 middleware

圖1: koa洋蔥模型
“洋蔥”的每一層都是一個
Interceptor,每一層都專注于自己的事情(單一職責),比如日志、mock api、弱網模擬、統一header、APP層緩存、通訊加密等,功能拆分,互不影響,從框架層面來講也是對AOP思想的具體實踐。(AOP可不僅僅是傳統意義上的字節碼插樁)

圖2: okhttp中的洋蔥模型
okhttp本身已提供了幾個Interceptor的默認實現
CacheInterceptor: 對于 http1.1 緩存機制的具體實現(cache-controll等)ConnectInterceptor: 專門負責創建/復用TCP連接, 里面的 ConnectionPool 就是對 http1.1 中 keep-alive(TCP連接復用)和 pipline機制(用多條TCP連接實現并發請求)的具體實現,而超時相關的設置也是從這里切入。
okhttp3.internal.connection.Transmitter
okhttp3.RealCall#newRealCall + okhttp3.internal.connection.Transmitter : 配置 callTimeout

- okhttp3.RealCall
package okhttp3;
final class RealCall implements Call {
...
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
}
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.transmitter = new Transmitter(client, call);
return call;
}
...
}
- okhttp3.internal.connection.Transmitter
package okhttp3.internal.connection;
public final class Transmitter {
private final OkHttpClient client;
private final RealConnectionPool connectionPool;
private final Call call;
private final EventListener eventListener;
private final AsyncTimeout timeout = new AsyncTimeout() {
@Override protected void timedOut() {
cancel();//超時將 cancel 請求
}
};
// ...
public Transmitter(OkHttpClient client, Call call) {
this.client = client;
this.connectionPool = Internal.instance.realConnectionPool(client.connectionPool());
this.call = call;
this.eventListener = client.eventListenerFactory().create(call);
this.timeout.timeout(client.callTimeoutMillis(), MILLISECONDS);//callTimeout
}
public void timeoutEnter() {
timeout.enter();
}
private @Nullable IOException timeoutExit(@Nullable IOException cause) {
if (timeoutEarlyExit) return cause;
if (!timeout.exit()) return cause;
InterruptedIOException e = new InterruptedIOException("timeout");//留意此行,InterruptedIOException
if (cause != null) e.initCause(cause);
return e;
}
public void cancel() {//取消請求 (如:請求超時時)
Exchange exchangeToCancel;
RealConnection connectionToCancel;
synchronized (connectionPool) {
canceled = true;
exchangeToCancel = exchange;
connectionToCancel = exchangeFinder != null && exchangeFinder.connectingConnection() != null
? exchangeFinder.connectingConnection()
: connection;
}
if (exchangeToCancel != null) {
exchangeToCancel.cancel();
} else if (connectionToCancel != null) {
connectionToCancel.cancel();
}
}
Realcall : 內置 ConnectInterceptor / CallServerInterceptor 等阻攔器
okhttp3.RealCall.AsyncCall#execute
okhttp3.RealCall: 每次調用
okhttp3.RealCall.AsyncCall#execute
@Override protected void execute() {
boolean signalledCallback = false;
transmitter.timeoutEnter();
try {
Response response = getResponseWithInterceptorChain();//關鍵行 : 通過攔截器鏈式調用 的方式 獲取 response
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} catch (Throwable t) {
cancel();
if (!signalledCallback) {
IOException canceledException = new IOException("canceled due to " + t);
canceledException.addSuppressed(t);
responseCallback.onFailure(RealCall.this, canceledException);
}
throw t;
} finally {
client.dispatcher().finished(this);
}
}
okhttp3.RealCall#getResponseWithInterceptorChain
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(new RetryAndFollowUpInterceptor(client));
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));//內置了對 ConnectInterceptor 的引用
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
Response response = chain.proceed(originalRequest);
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
ConnectInterceptor
okhttp3.internal.connection.ConnectInterceptor
/** Opens a connection to the target server and proceeds to the next interceptor. */
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;
public ConnectInterceptor(OkHttpClient client) {
this.client = client;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
// 入口在 newStream 方法
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
上面的 StreamAllocation#newStream 方法就做了2件事
public HttpCodec newStream(OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
// 這里的chain就是RealInterceptorChain,它里面的各種timeout值都是通過我們創建HttpClient時原封不動賦給它的,下面只是它的一些get方法;
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
//簡化后的代碼
...
// 3.1 findHealthyConnection 會調用 findConnection
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
// 3.2
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
return resultCodec;
}
okio.AsyncTimeout
package okio;
public class AsyncTimeout extends Timeout {
...
}
callTimeout,readTimeout,writeTimeout和okio的
AsyncTimeout有著密不可分的關系,其內部維護了一個靜態內部類Watchdog: 單獨開一個線程死循環判斷是否超時
okio.AsyncTimeout.Watchdog
private static final class Watchdog extends Thread {
Watchdog() {
super("Okio Watchdog");
setDaemon(true);
}
public void run() {
while (true) {
try {
AsyncTimeout timedOut;
synchronized (AsyncTimeout.class) {
timedOut = awaitTimeout();
// Didn't find a node to interrupt. Try again.
if (timedOut == null) continue;
// The queue is completely empty. Let this thread exit and let another watchdog thread
// get created on the next call to scheduleTimeout().
if (timedOut == head) {
head = null;
return;
}
}
// Close the timed out node.
timedOut.timedOut();
} catch (InterruptedException ignored) {
}
}
}
}
參考文獻
Y 推薦資源
- [網絡/HTTPS/Java] PKI公鑰基礎設施體系:數字證書(X.509)、CA機構 | 含:證書管理工具(jdk keytool / openssl) - 博客園/千千寰宇
- [Linux/Bash/Shell] curl & wget - 博客園/千千寰宇
X 參考文獻
本文鏈接: http://www.rzrgm.cn/johnnyzen
關于博文:評論和私信會在第一時間回復,或直接私信我。
版權聲明:本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協議。轉載請注明出處!
日常交流:大數據與軟件開發-QQ交流群: 774386015 【入群二維碼】參見左下角。您的支持、鼓勵是博主技術寫作的重要動力!

浙公網安備 33010602011771號