轉-HttpClient4.3 連接管理


 轉 http://www.yeetrack.com/?p=782

 

2.1.持久連接

兩個主機建立連接的過程是很復雜的一個過程,涉及到多個數據包的交換,並且也很耗時間。Http連接需要的三次握手開銷很大,這一開銷對於比較小的http消息來說更大。但是如果我們直接使用已經建立好的http連接,這樣花費就比較小,吞吐率更大。

HTTP/1.1默認就支持Http連接復用。兼容HTTP/1.0的終端也可以通過聲明來保持連接,實現連接復用。HTTP代理也可以在一定時間內保持連接不釋放,方便后續向這個主機發送http請求。這種保持連接不釋放的情況實際上是建立的持久連接。HttpClient也支持持久連接。

2.2.HTTP連接路由

HttpClient既可以直接、又可以通過多個中轉路由(hops)和目標服務器建立連接。HttpClient把路由分為三種plain(明文 ),tunneled(隧道)和layered(分層)。隧道連接中使用的多個中間代理被稱作代理鏈。

客戶端直接連接到目標主機或者只通過了一個中間代理,這種就是Plain路由。客戶端通過第一個代理建立連接,通過代理鏈tunnelling,這種情況就是Tunneled路由。不通過中間代理的路由不可能時tunneled路由。客戶端在一個已經存在的連接上進行協議分層,這樣建立起來的路由就是layered路由。協議只能在隧道—>目標主機,或者直接連接(沒有代理),這兩種鏈路上進行分層。

2.2.1.路由計算

RouteInfo接口包含了數據包發送到目標主機過程中,經過的路由信息。HttpRoute類繼承了RouteInfo接口,是RouteInfo的具體實現,這個類是不允許修改的。HttpTracker類也實現了RouteInfo接口,它是可變的,HttpClient會在內部使用這個類來探測到目標主機的剩余路由。HttpRouteDirector是個輔助類,可以幫助計算數據包的下一步路由信息。這個類也是在HttpClient內部使用的。

HttpRoutePlanner接口可以用來表示基於http上下文情況下,客戶端到服務器的路由計算策略。HttpClient有兩個HttpRoutePlanner的實現類。SystemDefaultRoutePlanner這個類基於java.net.ProxySelector,它默認使用jvm的代理配置信息,這個配置信息一般來自系統配置或者瀏覽器配置。DefaultProxyRoutePlanner這個類既不使用java本身的配置,也不使用系統或者瀏覽器的配置。它通常通過默認代理來計算路由信息。

2.2.2. 安全的HTTP連接

為了防止通過Http消息傳遞的信息不被未授權的第三方獲取、截獲,Http可以使用SSL/TLS協議來保證http傳輸安全,這個協議是當前使用最廣的。當然也可以使用其他的加密技術。但是通常情況下,Http信息會在加密的SSL/TLS連接上進行傳輸。

2.3. HTTP連接管理器

2.3.1. 管理連接和連接管理器

Http連接是復雜,有狀態的,線程不安全的對象,所以它必須被妥善管理。一個Http連接在同一時間只能被一個線程訪問。HttpClient使用一個叫做Http連接管理器的特殊實體類來管理Http連接,這個實體類要實現HttpClientConnectionManager接口。Http連接管理器在新建http連接時,作為工廠類;管理持久http連接的生命周期;同步持久連接(確保線程安全,即一個http連接同一時間只能被一個線程訪問)。Http連接管理器和ManagedHttpClientConnection的實例類一起發揮作用,ManagedHttpClientConnection實體類可以看做http連接的一個代理服務器,管理着I/O操作。如果一個Http連接被釋放或者被它的消費者明確表示要關閉,那么底層的連接就會和它的代理進行分離,並且該連接會被交還給連接管理器。這是,即使服務消費者仍然持有代理的引用,它也不能再執行I/O操作,或者更改Http連接的狀態。

下面的代碼展示了如何從連接管理器中取得一個http連接:

    HttpClientContext context = HttpClientContext.create();
    HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
    HttpRoute route = new HttpRoute(new HttpHost("www.yeetrack.com", 80));
    // 獲取新的連接. 這里可能耗費很多時間
    ConnectionRequest connRequest = connMrg.requestConnection(route, null);
    // 10秒超時
    HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
    try {
        // 如果創建連接失敗
        if (!conn.isOpen()) {
            // establish connection based on its route info
            connMrg.connect(conn, route, 1000, context);
            // and mark it as route complete
            connMrg.routeComplete(conn, route, context);
        }
        // 進行自己的操作.
    } finally {
        connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
    }

如果要終止連接,可以調用ConnectionRequestcancel()方法。這個方法會解鎖被ConnectionRequestget()方法阻塞的線程。

2.3.2.簡單連接管理器

BasicHttpClientConnectionManager是個簡單的連接管理器,它一次只能管理一個連接。盡管這個類是線程安全的,它在同一時間也只能被一個線程使用。BasicHttpClientConnectionManager會盡量重用舊的連接來發送后續的請求,並且使用相同的路由。如果后續請求的路由和舊連接中的路由不匹配,BasicHttpClientConnectionManager就會關閉當前連接,使用請求中的路由重新建立連接。如果當前的連接正在被占用,會拋出java.lang.IllegalStateException異常。

2.3.3.連接池管理器

相對BasicHttpClientConnectionManager來說,PoolingHttpClientConnectionManager是個更復雜的類,它管理着連接池,可以同時為很多線程提供http連接請求。Connections are pooled on a per route basis.當請求一個新的連接時,如果連接池有有可用的持久連接,連接管理器就會使用其中的一個,而不是再創建一個新的連接。

PoolingHttpClientConnectionManager維護的連接數在每個路由基礎和總數上都有限制。默認,每個路由基礎上的連接不超過2個,總連接數不能超過20。在實際應用中,這個限制可能會太小了,尤其是當服務器也使用Http協議時。

下面的例子演示了如果調整連接池的參數:

    PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
    // 將最大連接數增加到200
    cm.setMaxTotal(200);
    // 將每個路由基礎的連接增加到20
    cm.setDefaultMaxPerRoute(20);
    //將目標主機的最大連接數增加到50
    HttpHost localhost = new HttpHost("www.yeetrack.com", 80);
    cm.setMaxPerRoute(new HttpRoute(localhost), 50);

    CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(cm)
            .build();
 
        

2.3.4.關閉連接管理器

當一個HttpClient的實例不在使用,或者已經脫離它的作用范圍,我們需要關掉它的連接管理器,來關閉掉所有的連接,釋放掉這些連接占用的系統資源。

    CloseableHttpClient httpClient = <...>
    httpClient.close();

 

2.4.多線程請求執行

當使用了請求連接池管理器(比如PoolingClientConnectionManager)后,HttpClient就可以同時執行多個線程的請求了。

PoolingClientConnectionManager會根據它的配置來分配請求連接。如果連接池中的所有連接都被占用了,那么后續的請求就會被阻塞,直到有連接被釋放回連接池中。為了防止永遠阻塞的情況發生,我們可以把http.conn-manager.timeout的值設置成一個整數。如果在超時時間內,沒有可用連接,就會拋出ConnectionPoolTimeoutException異常。

    PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
    CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(cm)
            .build();

    // URL列表數組
    String[] urisToGet = {
        "http://www.domain1.com/",
        "http://www.domain2.com/",
        "http://www.domain3.com/",
        "http://www.domain4.com/"
    };

    // 為每個url創建一個線程,GetThread是自定義的類
    GetThread[] threads = new GetThread[urisToGet.length];
    for (int i = 0; i < threads.length; i++) {
        HttpGet httpget = new HttpGet(urisToGet[i]);
        threads[i] = new GetThread(httpClient, httpget);
    }

    // 啟動線程
    for (int j = 0; j < threads.length; j++) {
        threads[j].start();
    }

    // join the threads
    for (int j = 0; j < threads.length; j++) {
        threads[j].join();
    }

即使HttpClient的實例是線程安全的,可以被多個線程共享訪問,但是仍舊推薦每個線程都要有自己專用實例的HttpContext。

下面是GetThread類的定義:

    static class GetThread extends Thread {

        private final CloseableHttpClient httpClient;
        private final HttpContext context;
        private final HttpGet httpget;

        public GetThread(CloseableHttpClient httpClient, HttpGet httpget) {
            this.httpClient = httpClient;
            this.context = HttpClientContext.create();
            this.httpget = httpget;
        }

        @Override
        public void run() {
            try {
                CloseableHttpResponse response = httpClient.execute(
                        httpget, context);
                try {
                    HttpEntity entity = response.getEntity();
                } finally {
                    response.close();
                }
            } catch (ClientProtocolException ex) {
                // Handle protocol errors
            } catch (IOException ex) {
                // Handle I/O errors
            }
        }

    }

 

2.5. 連接回收策略

經典阻塞I/O模型的一個主要缺點就是只有當組側I/O時,socket才能對I/O事件做出反應。當連接被管理器收回后,這個連接仍然存活,但是卻無法監控socket的狀態,也無法對I/O事件做出反饋。如果連接被服務器端關閉了,客戶端監測不到連接的狀態變化(也就無法根據連接狀態的變化,關閉本地的socket)。

HttpClient為了緩解這一問題造成的影響,會在使用某個連接前,監測這個連接是否已經過時,如果服務器端關閉了連接,那么連接就會失效。這種過時檢查並不是100%有效,並且會給每個請求增加10到30毫秒額外開銷。唯一一個可行的,且does not involve a one thread per socket model for idle connections的解決辦法,是建立一個監控線程,來專門回收由於長時間不活動而被判定為失效的連接。這個監控線程可以周期性的調用ClientConnectionManager類的closeExpiredConnections()方法來關閉過期的連接,回收連接池中被關閉的連接。它也可以選擇性的調用ClientConnectionManager類的closeIdleConnections()方法來關閉一段時間內不活動的連接。

    public static class IdleConnectionMonitorThread extends Thread {

        private final HttpClientConnectionManager connMgr;
        private volatile boolean shutdown;

        public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
            super();
            this.connMgr = connMgr;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(5000);
                        // 關閉失效的連接
                        connMgr.closeExpiredConnections();
                        // 可選的, 關閉30秒內不活動的連接
                        connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                    }
                }
            } catch (InterruptedException ex) {
                // terminate
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }

    }
 
        

2.6. 連接存活策略

Http規范沒有規定一個持久連接應該保持存活多久。有些Http服務器使用非標准的Keep-Alive頭消息和客戶端進行交互,服務器端會保持數秒時間內保持連接。HttpClient也會利用這個頭消息。如果服務器返回的響應中沒有包含Keep-Alive頭消息,HttpClient會認為這個連接可以永遠保持。然而,很多服務器都會在不通知客戶端的情況下,關閉一定時間內不活動的連接,來節省服務器資源。在某些情況下默認的策略顯得太樂觀,我們可能需要自定義連接存活策略。

    ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {

        public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
            // Honor 'keep-alive' header
            HeaderElementIterator it = new BasicHeaderElementIterator(
                    response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                String param = he.getName();
                String value = he.getValue();
                if (value != null && param.equalsIgnoreCase("timeout")) {
                    try {
                        return Long.parseLong(value) * 1000;
                    } catch(NumberFormatException ignore) {
                    }
                }
            }
            HttpHost target = (HttpHost) context.getAttribute(
                    HttpClientContext.HTTP_TARGET_HOST);
            if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
                // Keep alive for 5 seconds only
                return 5 * 1000;
            } else {
                // otherwise keep alive for 30 seconds
                return 30 * 1000;
            }
        }

    };
    CloseableHttpClient client = HttpClients.custom()
            .setKeepAliveStrategy(myStrategy)
            .build();

 

2.7.socket連接工廠

Http連接使用java.net.Socket類來傳輸數據。這依賴於ConnectionSocketFactory接口來創建、初始化和連接socket。這樣也就允許HttpClient的用戶在代碼運行時,指定socket初始化的代碼。PlainConnectionSocketFactory是默認的創建、初始化明文socket(不加密)的工廠類。

創建socket和使用socket連接到目標主機這兩個過程是分離的,所以我們可以在連接發生阻塞時,關閉socket連接。

    HttpClientContext clientContext = HttpClientContext.create();
    PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory();
    Socket socket = sf.createSocket(clientContext);
    int timeout = 1000; //ms
    HttpHost target = new HttpHost("www.yeetrack.com");
    InetSocketAddress remoteAddress = new InetSocketAddress(
        InetAddress.getByName("www.yeetrack.com", 80);
        //connectSocket源碼中,實際沒有用到target參數
        sf.connectSocket(timeout, socket, target, remoteAddress, null, clientContext);

 

2.7.1.安全SOCKET分層

LayeredConnectionSocketFactoryConnectionSocketFactory的拓展接口。分層socket工廠類可以在明文socket的基礎上創建socket連接。分層socket主要用於在代理服務器之間創建安全socket。HttpClient使用SSLSocketFactory這個類實現安全socket,SSLSocketFactory實現了SSL/TLS分層。請知曉,HttpClient沒有自定義任何加密算法。它完全依賴於Java加密標准(JCE)和安全套接字(JSEE)拓展。

2.7.2.集成連接管理器

自定義的socket工廠類可以和指定的協議(Http、Https)聯系起來,用來創建自定義的連接管理器。

    ConnectionSocketFactory plainsf = <...>
    LayeredConnectionSocketFactory sslsf = <...>
    Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create()
            .register("http", plainsf)
            .register("https", sslsf)
            .build();

    HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r);
    HttpClients.custom()
            .setConnectionManager(cm)
            .build();

 

2.7.3.SSL/TLS定制

HttpClient使用SSLSocketFactory來創建ssl連接。SSLSocketFactory允許用戶高度定制。它可以接受javax.net.ssl.SSLContext這個類的實例作為參數,來創建自定義的ssl連接。

    HttpClientContext clientContext = HttpClientContext.create();
    KeyStore myTrustStore = <...>
    SSLContext sslContext = SSLContexts.custom()
            .useTLS()
            .loadTrustMaterial(myTrustStore)
            .build();
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);

 

2.7.4.域名驗證

除了信任驗證和在ssl/tls協議層上進行客戶端認證,HttpClient一旦建立起連接,就可以選擇性驗證目標域名和存儲在X.509證書中的域名是否一致。這種驗證可以為服務器信任提供額外的保障。X509HostnameVerifier接口代表主機名驗證的策略。在HttpClient中,X509HostnameVerifier有三個實現類。重要提示:主機名有效性驗證不應該和ssl信任驗證混為一談。

  • StrictHostnameVerifier: 嚴格的主機名驗證方法和java 1.4,1.5,1.6驗證方法相同。和IE6的方式也大致相同。這種驗證方式符合RFC 2818通配符。The hostname must match either the first CN, or any of the subject-alts. A wildcard can occur in the CN, and in any of the subject-alts.
  • BrowserCompatHostnameVerifier: 這種驗證主機名的方法,和Curl及firefox一致。The hostname must match either the first CN, or any of the subject-alts. A wildcard can occur in the CN, and in any of the subject-alts.StrictHostnameVerifierBrowserCompatHostnameVerifier方式唯一不同的地方就是,帶有通配符的域名(比如*.yeetrack.com),BrowserCompatHostnameVerifier方式在匹配時會匹配所有的的子域名,包括 a.b.yeetrack.com .
  • AllowAllHostnameVerifier: 這種方式不對主機名進行驗證,驗證功能被關閉,是個空操作,所以它不會拋出javax.net.ssl.SSLException異常。HttpClient默認使用BrowserCompatHostnameVerifier的驗證方式。如果需要,我們可以手動執行驗證方式。
    SSLContext sslContext = SSLContexts.createSystemDefault();
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
            sslContext,
            SSLConnectionSocketFactory.STRICT_HOSTNAME_VERIFIER);
    

2.8.HttpClient代理服務器配置

盡管,HttpClient支持復雜的路由方案和代理鏈,它同樣也支持直接連接或者只通過一跳的連接。

使用代理服務器最簡單的方式就是,指定一個默認的proxy參數。

    HttpHost proxy = new HttpHost("someproxy", 8080);
    DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
    CloseableHttpClient httpclient = HttpClients.custom()
            .setRoutePlanner(routePlanner)
            .build();

我們也可以讓HttpClient去使用jre的代理服務器。

    SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(
        ProxySelector.getDefault());
    CloseableHttpClient httpclient = HttpClients.custom()
            .setRoutePlanner(routePlanner)
            .build();

又或者,我們也可以手動配置RoutePlanner,這樣就可以完全控制Http路由的過程。

    HttpRoutePlanner routePlanner = new HttpRoutePlanner() {

        public HttpRoute determineRoute(
                HttpHost target,
                HttpRequest request,
                HttpContext context) throws HttpException {
            return new HttpRoute(target, null,  new HttpHost("someproxy", 8080),
                    "https".equalsIgnoreCase(target.getSchemeName()));
        }

    };
    CloseableHttpClient httpclient = HttpClients.custom()
            .setRoutePlanner(routePlanner)
            .build();
        }
    }

 

基本概念

http://www.yeetrack.com/?p=773

Http狀態管理

http://www.yeetrack.com/?p=822

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM