OkHttp使用踩坑記錄總結(二):OkHttp同步異步請求和連接池線程池

說明

在項目中對第三方服務的調用,使用了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將對請求和響應做以下操作:

  1. 爲了請求的準確性和效率,OkHttp在進行實際請求前會重寫發送者的請求(Rewriting Requests)。
  2. 當使用透明傳時或者使用緩存時,OkHttp將重寫響應(Rewriting Responses)。
  3. 當請求地址改變,響應302時,OkHttp將進行重定向的後續請求(Follow-up Requests)。
  4. 當連接失敗時,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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章