異步httpclient(httpasyncclient)的使用與總結

1. 前言

應用層的網絡模型有同步與異步。同步意味當前線程是阻塞的,只有本次請求完成後才能進行下一次請求;異步意味着所有的請求可以同時塞入緩衝區,不阻塞當前的線程;

httpclient在4.x之後開始提供基於nio的異步版本httpasyncclient,httpasyncclient藉助了Java併發庫和nio進行封裝(雖說NIO是同步非阻塞IO,但是HttpAsyncClient提供了回調的機制,與netty類似,所以可以模擬類似於AIO的效果),其調用方式非常便捷,但是其中也有許多需要注意的地方。

2. pom文件

本文依賴4.1.2,當前最新的客戶端版本是4.1.3maven repository 地址

        <dependency>  
            <groupId>org.apache.httpcomponents</groupId>  
            <artifactId>httpclient</artifactId>  
            <version>4.5.2</version>  
        </dependency>  

        <dependency>  
            <groupId>org.apache.httpcomponents</groupId>  
            <artifactId>httpcore</artifactId>  
            <version>4.4.5</version>  
        </dependency>  

        <dependency>  
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore-nio</artifactId>
            <version>4.4.5</version>
        </dependency>  

        <dependency>  
            <groupId>org.apache.httpcomponents</groupId>  
            <artifactId>httpasyncclient</artifactId>  
            <version>4.1.2</version>  
        </dependency>

3. 簡單的實例

public class TestHttpClient {
    public static void main(String[] args){

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(50000)
                .setSocketTimeout(50000)
                .setConnectionRequestTimeout(1000)
                .build();

        //配置io線程
        IOReactorConfig ioReactorConfig = IOReactorConfig.custom().
                setIoThreadCount(Runtime.getRuntime().availableProcessors())
                .setSoKeepAlive(true)
                .build();
        //設置連接池大小
        ConnectingIOReactor ioReactor=null;
        try {
            ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
        } catch (IOReactorException e) {
            e.printStackTrace();
        }
        PoolingNHttpClientConnectionManager connManager = new PoolingNHttpClientConnectionManager(ioReactor);
        connManager.setMaxTotal(100);
        connManager.setDefaultMaxPerRoute(100);


        final CloseableHttpAsyncClient client = HttpAsyncClients.custom().
                setConnectionManager(connManager)
                .setDefaultRequestConfig(requestConfig)
                .build();


        //構造請求
        String url = "http://127.0.0.1:9200/_bulk";
        HttpPost httpPost = new HttpPost(url);
        StringEntity entity = null;
        try {
            String a = "{ \"index\": { \"_index\": \"test\", \"_type\": \"test\"} }\n" +
                    "{\"name\": \"上海\",\"age\":33}\n";
            entity = new StringEntity(a);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        httpPost.setEntity(entity);

        //start
        client.start();

        //異步請求
        client.execute(httpPost, new Back());

        while(true){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Back implements FutureCallback<HttpResponse>{

        private long start = System.currentTimeMillis();
        Back(){
        }

        public void completed(HttpResponse httpResponse) {
            try {
                System.out.println("cost is:"+(System.currentTimeMillis()-start)+":"+EntityUtils.toString(httpResponse.getEntity()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void failed(Exception e) {
            System.err.println(" cost is:"+(System.currentTimeMillis()-start)+":"+e);
        }

        public void cancelled() {

        }
    }
}

4. 幾個重要的參數

4.1 TimeOut(3個)的設置

這裏寫圖片描述

ConnectTimeout : 連接超時,連接建立時間,三次握手完成時間。
SocketTimeout : 請求超時,數據傳輸過程中數據包之間間隔的最大時間。
ConnectionRequestTimeout : 使用連接池來管理連接,從連接池獲取連接的超時時間。

在實際項目開發過程中,這三個值可根據具體情況設置。

(1) 下面針對ConnectionRequestTimeout的情況進行分析

實驗條件:設置連接池最大連接數爲1,每一個異步請求從開始到回調的執行時間在100ms以上;

實驗過程:連續發送2次請求

public class TestHttpClient {
    public static void main(String[] args){

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(50000)
                .setSocketTimeout(50000)
                .setConnectionRequestTimeout(10)//設置爲10ms
                .build();

        //配置io線程
        IOReactorConfig ioReactorConfig = IOReactorConfig.custom().
                setIoThreadCount(Runtime.getRuntime().availableProcessors())
                .setSoKeepAlive(true)
                .build();
        //設置連接池大小
        ConnectingIOReactor ioReactor=null;
        try {
            ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
        } catch (IOReactorException e) {
            e.printStackTrace();
        }
        PoolingNHttpClientConnectionManager connManager = new PoolingNHttpClientConnectionManager(ioReactor);
        connManager.setMaxTotal(1);//最大連接數設置1
        connManager.setDefaultMaxPerRoute(1);//per route最大連接數設置1


        final CloseableHttpAsyncClient client = HttpAsyncClients.custom().
                setConnectionManager(connManager)
                .setDefaultRequestConfig(requestConfig)
                .build();


        //構造請求
        String url = "http://127.0.0.1:9200/_bulk";
        List<HttpPost> list = new ArrayList<HttpPost>();
        for(int i=0;i<2;i++){
            HttpPost httpPost = new HttpPost(url);
            StringEntity entity = null;
            try {
                String a = "{ \"index\": { \"_index\": \"test\", \"_type\": \"test\"} }\n" +
                        "{\"name\": \"上海\",\"age\":33}\n";
                entity = new StringEntity(a);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            httpPost.setEntity(entity);
            list.add(httpPost);
        }

        client.start();

        for(int i=0;i<2;i++){
            client.execute(list.get(i), new Back());
        }

        while(true){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Back implements FutureCallback<HttpResponse>{

        private long start = System.currentTimeMillis();
        Back(){
        }

        public void completed(HttpResponse httpResponse) {
            try {
                System.out.println("cost is:"+(System.currentTimeMillis()-start)+":"+EntityUtils.toString(httpResponse.getEntity()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void failed(Exception e) {
            e.printStackTrace();
            System.err.println(" cost is:"+(System.currentTimeMillis()-start)+":"+e);
        }

        public void cancelled() {

        }
    }
}

實驗結果 :
第一次請求執行時間在200ms左右
第二請求回調直接拋出TimeOutException


java.util.concurrent.TimeoutException
    at org.apache.http.nio.pool.AbstractNIOConnPool.processPendingRequest(AbstractNIOConnPool.java:364)
    at org.apache.http.nio.pool.AbstractNIOConnPool.processNextPendingRequest(AbstractNIOConnPool.java:344)
    at org.apache.http.nio.pool.AbstractNIOConnPool.release(AbstractNIOConnPool.java:318)
    at org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager.releaseConnection(PoolingNHttpClientConnectionManager.java:303)
    at org.apache.http.impl.nio.client.AbstractClientExchangeHandler.releaseConnection(AbstractClientExchangeHandler.java:239)
    at org.apache.http.impl.nio.client.MainClientExec.responseCompleted(MainClientExec.java:387)
    at org.apache.http.impl.nio.client.DefaultClientExchangeHandlerImpl.responseCompleted(DefaultClientExchangeHandlerImpl.java:168)
    at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.processResponse(HttpAsyncRequestExecutor.java:436)
    at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.inputReady(HttpAsyncRequestExecutor.java:326)
    at org.apache.http.impl.nio.DefaultNHttpClientConnection.consumeInput(DefaultNHttpClientConnection.java:265)
    at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:81)
    at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:39)
    at org.apache.http.impl.nio.reactor.AbstractIODispatch.inputReady(AbstractIODispatch.java:114)
    at org.apache.http.impl.nio.reactor.BaseIOReactor.readable(BaseIOReactor.java:162)
    at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvent(AbstractIOReactor.java:337)
    at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvents(AbstractIOReactor.java:315)
    at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:276)
    at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:104)
    at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:588)
    at java.lang.Thread.run(Thread.java:745)

結果分析:由於連接池大小是1,第一次請求執行後連接被佔用(時間在100ms),第二次請求在規定的時間內無法獲取連接,於是直接連接獲取的TimeOutException

(2) 修改ConnectionRequestTimeout

RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(50000)
                .setSocketTimeout(50000)
                .setConnectionRequestTimeout(1000)//設置爲1000ms
                .build();

上述兩次請求正常執行。

下面進一步看一下代碼中拋異常的地方:

這裏寫圖片描述

這裏寫圖片描述

從上面的代碼中可以看到如果要設置永不ConnectionRequestTimeout,只需要將ConnectionRequestTimeout設置爲小於0即可,當然後這種設置一定要慎用, 如果處理不當,請求堆積會導致OOM。

4.2 連接池大小的設置

這裏寫圖片描述

ConnTotal:連接池中最大連接數;
ConnPerRoute(1000):分配給同一個route(路由)最大的併發連接數,route爲運行環境機器到目標機器的一條線路,舉例來說,我們使用HttpClient的實現來分別請求 www.baidu.com 的資源和 www.bing.com 的資源那麼他就會產生兩個route;

對於上述的實驗,在一定程度上可以通過增大最大連接數來解決ConnectionRequestTimeout的問題!

後續:本文重點在於使用,後續會對源碼進行分析與解讀

參考文檔:
1.官網httpcomponents-asyncclient-4.1.x

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