HTTP連線池

AskHarries發表於2018-05-07

1.為什麼要用Http連線池

1、降低延遲:如果不採用連線池,每次連線發起Http請求的時候都會重新建立TCP連線(經歷3次握手),用完就會關閉連線(4次揮手),如果採用連線池則減少了這部分時間損耗,別小看這幾次握手,本人經過測試發現,基本上3倍的時間延遲

2、支援更大的併發:如果不採用連線池,每次連線都會開啟一個埠,在大併發的情況下系統的埠資源很快就會被用完,導致無法建立新的連線

2.簡單連線管理器

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

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();複製程式碼

4.關閉連線管理器

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

    CloseableHttpClient httpClient = <...>
    httpClient.close();複製程式碼

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();
            }
        }

    }複製程式碼

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();複製程式碼


HTTP連線池


相關文章