1. 程式人生 > >HttpClient 超時設定(包括https)

HttpClient 超時設定(包括https)

本文基於httpClient4.5版本

maven依賴

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

本文不詳細展開對httpclient的詳細講解, 只展示具體用法

httpclient 4.5版本設定超時有三種

1connectTimeOut

建立連線超時時間, 眾所周知 http的三次握手機智, 在正式進行資料傳輸之前需要先完成三次握手. connectTimeOut就是指在進行三次握手行為時所設定的超時. 例如在文末例子中,我訪問了一個虛假的域名,http://74.125.203.100, 因為域名本身就不存在, 所以三次握手行為肯定是失敗的, 所以連結肯定是無法建立的. 根據如下程式碼, 設定的時間是5000毫秒 所以執行程式會在五秒之後丟擲連結超時異常

2socketTimeOut

建立連結成功資料傳輸導致的超時時間, 當三次握手行為成功後, 即可通過所建立的http通道進行資料傳輸, 此時如果超過設定時間並沒有獲取到對應的資料包就會丟擲超時異常, 此處有個需要注意點是socketTimeOut所處理的超時時間是指相鄰兩個資料包傳輸之間所經歷的時間. 例如連結建立成功後 由於資料過大 服務端每隔1秒傳送一個數據包給客戶端, 此時設定的超時時間為3秒,一共傳送了10個數據包,總共耗時10秒, 請求總共花費10秒, 但是並不會報超時異常, 是因為每次資料包傳輸之間的時間都不超過3秒,所以不會丟擲異常, 總結一下 socketTimeOut是指連結建立成功後,資料包傳輸之間時間超時限制.

3 connectionRequestTimeOut

從httpclient連線池獲取連線超時限制 這個超時沒有測試, 以後詳細解釋

時間單位是毫秒

.setConnectionRequestTimeout(1000) // 從資料庫連線池獲取連線超時時間設定

.setSocketTimeout(1000)   // socket連線建立成功, 資料傳輸響應超時

.setConnectTimeout(5000) // 建立socket連結超時時間

https訪問超時無效問題

由於我在專案中既要訪問http介面也要訪問https介面,設定超時後發現https不起作用,經過排查跟https 對應的HttpClient物件有關,下面貼上新舊程式碼

初步排查是建立httpsClient時候導致的超時不生效(此時是https介面訪問生效, https介面訪問不生效), 目前尚未發現是什麼原因,待以後詳細檢視後再解決

舊程式碼

// 構造方法
   public SSLClient() throws Exception{
       // 呼叫父類構造方法
       super();  
       SSLContext ctx = SSLContext.getInstance("TLS");  
       X509TrustManager tm = new X509TrustManager() {  
               @Override  
               public void checkClientTrusted(X509Certificate[] chain,  
                       String authType) throws CertificateException {  
               }  
               @Override  
               public void checkServerTrusted(X509Certificate[] chain,  
                       String authType) throws CertificateException {  
               }  
               @Override  
               public X509Certificate[] getAcceptedIssuers() {  
                   return null;  
               }  
       };  
       ctx.init(null, new TrustManager[]{tm}, null);  
       SSLSocketFactory ssf = new SSLSocketFactory(ctx,SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);  
       ClientConnectionManager ccm = this.getConnectionManager();  
       SchemeRegistry sr = ccm.getSchemeRegistry();  
       sr.register(new Scheme("https", 443, ssf));  
   }

新程式碼

public static CloseableHttpClient createSSLClientDefault(CookieStore cookieStore) {

    SSLContext sslContext;
    try {
        sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
            //信任所有
            @Override
            public boolean isTrusted(X509Certificate[] xcs, String string){
                return true;
            }
        }).build();

        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
        if(cookieStore == null){
            return HttpClients.custom().setSSLSocketFactory(sslsf).build();
        }else{
            return HttpClients.custom().setSSLSocketFactory(sslsf).setDefaultCookieStore(cookieStore).build();
        }
    } catch (KeyStoreException ex) {
        logger.info(ex);
    } catch (NoSuchAlgorithmException ex) {
        logger.info(ex);
    } catch (KeyManagementException ex) {
        logger.info(ex);
    }

    return HttpClients.createDefault();
}

超時設定不生效問題

在檢視相關文件的時候,發現有朋友說這個問題, 但是自己沒有遇到過,所以暫時先不研究, 待研究後再寫

測試完整程式碼, 不包括https

httpclient客戶端訪問測試程式碼

import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * SocketTimeout  服務端連結成功後之間響應超時時間設定
 * ConnectTimeout 建立socket連結超時設定
 */
public class TimeoutTestControllerTest {

    public static void main(String[] args) {
        try {
            //new TimeoutTestControllerTest().connectionTimeout();
            //new TimeoutTestControllerTest().socketTimeout();
            //new TimeoutTestControllerTest().socketTimeoutNo();
            new TimeoutTestControllerTest().connectionRequestTimeoutWithPoolingConnectionManager();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 1.ConnectTimeout:IP無法連結,連結超時 連結無法建立時間限制
     *
     * @throws Exception
     */
    public void connectionTimeout() throws Exception {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://74.125.203.100");
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(1000)
                .setSocketTimeout(1000) // 服務端相應超時
                .setConnectTimeout(5000) // 建立socket連結超時時間
                .build();
        httpGet.setConfig(requestConfig);
        long start = 0;
        try {
            start = System.currentTimeMillis();
            httpclient.execute(httpGet);
            System.out.println("連結成功");
        } catch (ConnectTimeoutException exception) {
            System.out.println("連結成失敗");
            exception.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("花費時間:" + (end-start)/1000 + "秒");
    }

    /**
     * 2.socketTimeout測試,服務端沒有指定時間內任何響應,會超時
     *
     * @throws Exception
     */
    public void socketTimeout() throws Exception {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://127.0.0.1:9999/testHttpClientTimeOutController/socket_timeout.do");
        RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(4000).build();
        httpGet.setConfig(requestConfig);
        long start = 0;
        try {
            start = System.currentTimeMillis();
            httpclient.execute(httpGet);
        } catch (SocketTimeoutException exception) {
            exception.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("花費時間:" + (end-start)/1000 + "秒");
    }

    /**
     * 3.socketTimeout測試:服務端隔800ms返回一點資料,不會超時
     *
     * @throws Exception
     */
    public void socketTimeoutNo() {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://127.0.0.1:9999/testHttpClientTimeOutController/socket_timeout_2.do");
        RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(100)
                .setSocketTimeout(900).setConnectTimeout(100).build();
        httpGet.setConfig(requestConfig);

        long start = 0;
        try {
            start = System.currentTimeMillis();
            httpclient.execute(httpGet);
            CloseableHttpResponse response = httpclient.execute(httpGet);
            System.out.println(String.format("socketTimeoutNo, %s", EntityUtils.toString(response.getEntity())));
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("花費時間:" + (end-start)/1000 + "秒");

    }

    /**
     * 4.connectionRequestTimeout測試:指從連線管理器(例如連線池)中拿到連線的超時時間
     *
     * @throws Exception
     */
    public void connectionRequestTimeoutWithPoolingConnectionManager() throws Exception {
        PoolingHttpClientConnectionManager conMgr = new PoolingHttpClientConnectionManager();
        conMgr.setMaxTotal(2);

        final CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(conMgr).build();
        final HttpGet httpGet = new HttpGet("http://127.0.0.1:9999/testHttpClientTimeOutController/connection_request_timeout.do");

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

        // 如下多執行緒佔滿連線池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        CloseableHttpResponse response = httpclient.execute(httpGet);
                        System.out.println(String.format("connectionRequestTimeoutTest: %s",
                                EntityUtils.toString(response.getEntity())));
                    } catch (SocketTimeoutException exception) {
                        System.out.println(exception.getMessage());
                    } catch (ClientProtocolException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        // 在連線池佔滿的情況下,拿不到就會拋異常
        long start = 0;
        try {
            start = System.currentTimeMillis();
            CloseableHttpResponse response = httpclient.execute(httpGet);
            System.out.println(String.format("connectionRequestTimeoutTest: %s",
                    EntityUtils.toString(response.getEntity())));
        } catch (Exception exception) {
            // 異常是從連線池拿到連線超時
            System.out.println(exception.getMessage());
        }
        long end = System.currentTimeMillis();
        System.out.println("花費時間:" + (end-start)/1000 + "秒");
    }

    /**
     * 5.connectionRequestTimeout測試,指從連線管理器中拿到連線的超時時間,由於使用基本的連線管理器,連結被佔用時,直接無法分配連結
     * connectionRequestTimeout並未生效,目前看來該引數只在連線池奏效.
     * 該連結管理器(BasicHttpClientConnectionManager)是單執行緒情況下可以使用,多執行緒情況下不要使用。
     *
     * @throws Exception
     */
    public void connectionRequestTimeoutWithBasicConnectionManager() throws Exception {

        BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager();
        final CloseableHttpClient httpclient = HttpClients.custom()
                .setConnectionManager(connManager).setMaxConnPerRoute(1).build();
        final HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/test/connection_request_timeout");

        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(100000)
                .setConnectionRequestTimeout(1000000).setSocketTimeout(1000000).build();
        httpGet.setConfig(requestConfig);

        // 如下多執行緒佔滿連線
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    CloseableHttpResponse response = null;
                    try {
                        response = httpclient.execute(httpGet);
                        System.out.println(String.format("connectionRequestTimeoutTest: %s",
                                EntityUtils.toString(response.getEntity())));
                    } catch (Exception exception) {
                        exception.printStackTrace();
                    } finally {
                        if (response != null) {
                            try {
                                response.close();
                                httpclient.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }

                    }
                }
            });
        }
        System.out.println(new Date());

        // 在連線池佔滿的情況下,拿不到就會拋異常
        try {
            CloseableHttpResponse response = httpclient.execute(httpGet);
            System.out.println(String.format("connectionRequestTimeoutTest: %s",
                    EntityUtils.toString(response.getEntity())));
        } catch (Exception exception) {
            System.out.println(new Date());
            exception.printStackTrace();
            // 異常是從連線池拿到連線超時
            System.out.println(exception.getMessage());
        }

    }

}

服務端程式碼  用於測試長時間響應

import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 測試HttpClient連結超時機制
 */
@Controller
@RequestMapping("testHttpClientTimeOutController")
public class TestHttpClientTimeOutController {

    private static final Logger logger = Logger.getLogger(TestHttpClientTimeOutController.class);

    /**
     * 1.測試socketOutTimeout,三秒後返回資料
     *
     * @return
     * @throws InterruptedException
     */
    @RequestMapping(value = {"/socket_timeout"}, method = {RequestMethod.GET})
    @ResponseBody
    String socketTimeout() throws InterruptedException {
        logger.info("socket_timeout");
        Thread.sleep(3000);
        return "socket_timeout";
    }

    /**
     * 2.測試socketOutTimeout,每隔0.8秒返回資料
     *
     * @return
     * @throws InterruptedException
     */
    @RequestMapping(value = {"/socket_timeout_2"}, method = {RequestMethod.GET})
    void socketTimeout2(HttpServletResponse response) throws InterruptedException, IOException {
        logger.info("socket_timeout_2");
        for (int i = 0; i < 10; i++) {
            logger.info("{}" + i);
            response.getWriter().println("" + i);
            response.flushBuffer();
            Thread.sleep(800);
        }
    }

    /**
     * 3.測試connectionRequestTimeout用的服務,三秒後返回資料
     *
     * @param request
     * @return
     * @throws InterruptedException
     */
    @RequestMapping(value = {"/connection_request_timeout"}, method = {RequestMethod.GET})
    @ResponseBody
    String connectionRequestTimeout(HttpServletRequest request) throws InterruptedException {
        logger.info(request.getRequestURI());
        Thread.sleep(3000);
        return "connectionRequestTimeout";
    }
}