說明
在項目中對第三方服務的調用,使用了OkHttp進行http請求,這當中踩了許多坑。本篇博文將對OkHttp使用過程遇到的問題進行總結記錄。
正文
同步請求SyncRequest 異步請求AsyncRequest
通過簡單示例瞭解OkHttp如何進行http請求:
SyncRequest:
private static void syncRequest(String url) throws IOException {
Request request = new Request.Builder().url(url).build();
Response response = okHttpClient.newCall(request).execute();
System.out.println(response.body().string());
}
AsyncRequest
private static void asyncRequest(String url) {
Request request = new Request.Builder().url(url).build();
okHttpClient.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 RuntimeException("Unexpected code " + response);
} else {
System.out.println(response.body().string());
}
}
});
}
在上示代碼中,okHttpClient調用newCall方法生成Call對象,再調用不同的方法進行不同的請求。爲什麼不直接請求,而是通過Call對象,該對象的作用是什麼?通過官方文檔 Calls 進行了解。
在文檔中說明了OkHttp將對請求和響應做以下操作:
- 爲了請求的準確性和效率,OkHttp在進行實際請求前會重寫發送者的請求(Rewriting Requests)。
- 當使用透明傳時或者使用緩存時,OkHttp將重寫響應(Rewriting Responses)。
- 當請求地址改變,響應302時,OkHttp將進行重定向的後續請求(Follow-up Requests)。
- 當連接失敗時,OkHttp將重新嘗試建立連接(Retrying Requests)。
發送者的簡單請求可能會產生以上的多箇中間請求和響應,OkHttp使用Call來進行任務,實現中間的多個請求響應。Call的執行有兩種方式:
- 同步 請求線程會阻塞直到響應可讀
- 異步 請求進隊列,通過另一個線程獲取回調讀取結果
Call的執行可以在任何線程中取消,這會導致未完成的請求失敗並拋出IOException。
請求轉發Dispatch
對於同步請求,我們需要控制同時請求的線程數量,太多同時請求的連接會浪費資源,太少請求則會增加延遲。
對於異步請求,OkHttp使用Dispathcer類依據某種策略實現對同時請求數的控制。你可以通過參數maxRequestPreHost設置每個服務端最多的同時請求數,默認值爲5,還可以通過參數maxRequests設置okHttpClient最多可以同時請求的數量,默認值是64。
在瞭解了OkHttp的同步異步請求是通過Call完成的後,再看OkHttp的連接池和線程池。
連接池ConnectionPool 線程池ThreadPool
在之前提到,在創建OkHttpClient對象時,創建了ConnecitonPool對象,但是該對象中並沒有設置連接池的大小,而只是設置了最大空閒連接數和空閒存活時長,同時瞭解OkHttp發送請求分爲同步、異步兩種方式,並且都是通過Call這個類完成的,在請求轉發時又通過了Dispatcher這個類。
這裏我們通過源碼來了解其背後的原理。
同步請求
public Response execute() throws IOException {
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
Response var2;
try {
this.client.dispatcher().executed(this); // 將本次請求放入Dispatcher類中的正在運行的同步請求隊列 runningSyncCalls
Response result = this.getResponseWithInterceptorChain(); // 攔截鏈處理並進行請求,獲取響應
if (result == null) {
throw new IOException("Canceled");
}
var2 = result;
} finally {
this.client.dispatcher().finished(this); //將執行完的請求從runingSyncCalls隊列刪除,並統計所有在執行請求的數量
}
return var2;
}
繼續跟蹤getResponseWithInterceptorChain()方法,瞭解如何實現同步請求。
private Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList();
interceptors.addAll(this.client.interceptors());
interceptors.add(this.retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(this.client.cookieJar()));
interceptors.add(new CacheInterceptor(this.client.internalCache()));
interceptors.add(new ConnectInterceptor(this.client));
if (!this.retryAndFollowUpInterceptor.isForWebSocket()) {
interceptors.addAll(this.client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(this.retryAndFollowUpInterceptor.isForWebSocket()));
Chain chain = new RealInterceptorChain(interceptors, (StreamAllocation)null, (HttpStream)null, (Connection)null, 0, this.originalRequest);
return chain.proceed(this.originalRequest);
}
通過以上代碼,我們可以看到除了我們在創建client對象時配置的攔截器外,還會自動添加五個攔截器,分別是:RetryAndFollwUpInterceptor, BridgeInterceptor, CacheInterceptor, ConnectInterceptor和CallServerInterceptor。
在生成攔截鏈Chain後,調用proceed方法處理請求。該方法在攔截鏈執行過程中被遞歸調用。
經過斷點追蹤,發現這五個攔截器的執行順序爲 RetryAndFollwUpInterceptor -> BridgeInterceptor -> CacheInterceptor -> ConnectInterceptor -> CallServerInterceptor。每個攔截器的作用這裏簡單說明下,更詳細的內容感興趣的同學可以自己追蹤下源碼。
RetryAndFollwUpInterceptor 處理連接失敗重試和重定向的後續請求。當沒有中斷cancel請求時,會循環調用proceed方法。
BridgeInterceptor 處理請求頭參數和響應頭參數,當沒有設置時,會添加Host, Connetion, Accept-Encoding, User-Agent請求頭參數,這裏我們可以看到OkHttp建立連接時默認是長連接 Keep-Alive。
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
CacheInterceptor 根據緩存策略處理響應結果。
ConnectInterceptor 連接攔截器, 從StreamAllocation獲取連接。
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain)chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpStream httpStream = streamAllocation.newStream(this.client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpStream, connection);
}
可以看到StreamAllocatio獲取連接前,調用newStream創建了HttpStream對象。追蹤其源碼可以看到獲取連接的源碼:
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException {
ConnectionPool var6 = this.connectionPool;
Route selectedRoute;
synchronized(this.connectionPool) {
if (this.released) {
throw new IllegalStateException("released");
}
if (this.stream != null) {
throw new IllegalStateException("stream != null");
}
if (this.canceled) {
throw new IOException("Canceled");
}
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
RealConnection pooledConnection = Internal.instance.get(this.connectionPool, this.address, this); // 根據Adress從連接池獲取連接
if (pooledConnection != null) {
this.connection = pooledConnection;
return pooledConnection;
}
selectedRoute = this.route; // 若連接池沒有該Adress的連接,則選擇Route嘗試建立連接
}
if (selectedRoute == null) {
selectedRoute = this.routeSelector.next();
var6 = this.connectionPool;
synchronized(this.connectionPool) {
this.route = selectedRoute;
this.refusedStreamCount = 0;
}
}
RealConnection newConnection = new RealConnection(selectedRoute);
this.acquire(newConnection);
ConnectionPool var16 = this.connectionPool;
synchronized(this.connectionPool) {
Internal.instance.put(this.connectionPool, newConnection); // 將新連接添加到連接池
this.connection = newConnection;
if (this.canceled) {
throw new IOException("Canceled");
}
}
newConnection.connect(connectTimeout, readTimeout, writeTimeout, this.address.connectionSpecs(), connectionRetryEnabled); // RealConnection的connect創建連接
this.routeDatabase().connected(newConnection.route());
return newConnection;
}
在connect方法中使用Route創建連接:
if (this.route.requiresTunnel()) {
this.buildTunneledConnection(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
} else {
this.buildConnection(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
}
以上源碼證明了之前說明的OkHttp建立連接的過程。同時也證明了,對於同步請求,要控制連接池的大小,需要請求發送者控制同時請求的數量。
CallServerInterceptor 在該攔截器中完成請求獲取響應,並且在請求結束後根據請求頭和響應頭的頭部參數Connection判斷是否關閉連接。
if ("close".equalsIgnoreCase(response.request().header("Connection")) || "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
通過ConnectInterceptor源碼,我們已經知道OkHttp默認使用長連接,但是這裏也會判斷服務端的響應頭Connection參數值,所以證明了在解決connection reset所說的客戶端的服務端要一致,要麼都用長連接,要麼都用短連接。
異步請求
public void enqueue(Callback responseCallback) {
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
可以看到在進行異步請求時,根據回調函數又創建了RealCall內部類AsynCall對象。接着調用了Dispatcher對象的enqueue方法,在該方法中對請求數量根據之前的配置進行了控制。
synchronized void enqueue(AsyncCall call) {
if (this.runningAsyncCalls.size() < this.maxRequests && this.runningCallsForHost(call) < this.maxRequestsPerHost) {
this.runningAsyncCalls.add(call); // 放入正在異步請求的隊列 方便統計所有的請求
this.executorService().execute(call); // 使用線程池處理請求
} else {
this.readyAsyncCalls.add(call); // 超過請求數限制,放入異步請求隊列
}
}
根據源碼可以看到根據之前配置的maxRequests和maxRequestsPerHost參數值對請求進行了限制,若不超過則使用了線程池處理請求,否則添加到異步請求隊列中。
針對線程池的配置,我們也可以從源碼看到,線程池最大線程數爲2147483647。
public synchronized ExecutorService executorService() {
if (this.executorService == null) {
this.executorService = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
}
return this.executorService;
}
在執行異步請求時,調用AsyncCall的execute()方法:
protected void execute() {
boolean signalledCallback = false;
try {
Response response = RealCall.this.getResponseWithInterceptorChain();
if (RealCall.this.retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
this.responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
this.responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException var6) {
if (signalledCallback) {
Platform.get().log(4, "Callback failure for " + RealCall.this.toLoggableString(), var6);
} else {
this.responseCallback.onFailure(RealCall.this, var6);
}
} finally {
RealCall.this.client.dispatcher().finished(this);
}
}
該方法進行請求獲取響應與同步請求一樣,都是通過getResponseWithInterceptorChain()方法,這裏不再贅述。同樣的在請求完成後,調用了Dispatcher的finished方法。但是該方法與同步請求不同,該方法的promoteCalls參數值設置爲了true。
void finished(AsyncCall call) {
this.finished(this.runningAsyncCalls, call, true);
}
finished方法:
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized(this) {
if (!calls.remove(call)) {
throw new AssertionError("Call wasn't in-flight!");
}
if (promoteCalls) {
this.promoteCalls(); // 將準備執行的異步請求放到線程池處理
}
runningCallsCount = this.runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
可以看到,異步請求在調用finished方法時,會調用promoteCalls()方法。在該方法中,會判斷如果當前進行的請求數小於maxRequests並且請求的host上的請求數小於masRequestsPerHost,則會處理請求。
private void promoteCalls() {
if (this.runningAsyncCalls.size() < this.maxRequests) { // 判斷正在進行的異步請求數是否小於總數限制
if (!this.readyAsyncCalls.isEmpty()) {
Iterator i = this.readyAsyncCalls.iterator();
do {
if (!i.hasNext()) {
return;
}
AsyncCall call = (AsyncCall)i.next();
if (this.runningCallsForHost(call) < this.maxRequestsPerHost) { // 判斷請求的host上的正在進行的請求數是否小於限制
i.remove();
this.runningAsyncCalls.add(call);
this.executorService().execute(call); // 使用線程池處理請求
}
} while(this.runningAsyncCalls.size() < this.maxRequests);
}
}
}
至此,通過以上源碼我們可以看到,對於異步請求,想要控制連接池和線程池的大小,需要發送者設置恰當的maxRequests和maxRequestsPerHost參數值。
本篇文章從client單例, 長連接,同步異步請求,連接池 線程池幾個方面瞭解了OkHttp,其他方面更多內容請查看官方文檔。
參考資料:
https://square.github.io/okhttp/connections/
https://blog.insightdatascience.com/learning-about-the-http-connection-keep-alive-header-7ebe0efa209d?gi=a5b2d74099c7
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive
https://tomcat.apache.org/connectors-doc/common_howto/timeouts.html
https://stackoverflow.com/questions/33281810/java-okhttp-reuse-keep-alive-connection
https://github.com/square/okhttp/issues/2031
https://www.jianshu.com/p/da5c303d1df4?tdsourcetag=s_pcqq_aiomsg
https://www.vogella.com/tutorials/JavaLibrary-OkHttp/article.html
https://juejin.im/post/5e156c80f265da5d3c6de72a
https://mp.weixin.qq.com/s/fy84edOix5tGgcvdFkJi2w
https://stackoverflow.com/questions/49069297/okhttpclient-connection-pool-size-dilemma/49070993#49070993