1. 程式人生 > >Java HttpClient(二:連線與狀態管理、認證與cache)

Java HttpClient(二:連線與狀態管理、認證與cache)

參考文獻:http://hc.apache.org/httpcomponents-client-ga/tutorial/html/

文章目錄

2.連線管理

從一臺主機到另一臺主機建立連結的過程是非常複雜的,其中在兩個端點之間有許多的包交換過程,而該過程非常耗時。建立連線時的握手產生的開銷是比較明顯的,特別是對於小的Http訊息。如果你能夠複用你建立的連線,那麼這對提升整個系統的吞吐量是有很大幫助的。在HTTP/1.1 規範中,預設情況下,Http連線是可以複用的。對於使用HTTP/1.0的端點來說,可以通過keep-alive 機制來複用連線。

保持連線可用通常來說需要保證連線的持久化。HttpClient完全支援連線的持久化。

HttpClient能夠直接和目標主機建立連線,同時也可以通過route(這裡是指路由還是代理?)和目標主機建立連線,但這會建立許多的中間連線(也可被成為中間跳)。

2.1 Http連線路由

route型別連線的分類
HttpClient將route型別的連線分為簡單的(plain)、使用隧道包裝的(tunneled)、分層的(layer)連線。在tunnel connection 中使用的多箇中間代理被稱為代理鏈。

  1. plian route是通過直接和目標主機或是代理(和第一個也是唯一的一個代理)建立起來的;
  2. tunnelled route 是通過和第一個代理通訊,訊息通過代理鏈進行傳輸(Tunnelled routes are established by connecting to the first and tunnelling through a chain of proxies to the target.);如果不存在代理,則無法tunnelled( 隧道化包裝?)
  3. Layered routes是指在已有的連線之上,在封裝一層協議( Layered routes are established by layering a protocol over an existing connection. Protocols can only be layered over a tunnel to the target, or over a direct connection without proxies.)(常用的是ssl/tls協議)

2.2 Http連線管理

Http連線是有狀態的(不同於協議規範中的http conection是無狀態的,實際中的連線希望有狀態,儲存連線的資訊,以提供更好的服務,如session,context等)、非執行緒安全的。HttpClient通過HttpClientConnectionManager 介面來進行連線管理。

HttpClient Connection Manager 的作用是:作為一個Http Connection的工廠,建立新的連線、管理持久化連線的生命週期、對持久化連線的訪問做同步控制,確保同一時刻,只有一個執行緒能夠訪問持久化的連線(簡而言之,一是建立連線;二是管理持久化連線,包括生命週期管理,同步訪問管理)。

在http connection manager內部,mananger通過ManagedHttpClientConnection例項、代理機制來實現。當實際的連線例項物件被釋放、或是關閉了,則該例項物件就會和它繫結的代理解綁,然後返回給manager。即使connection使用者 依然持有代理物件的例項,connection也無法執行任何io請求,或是改變狀態了。

mananger物件使用的範例如下:

HttpClientContext context = HttpClientContext.create();
HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
HttpRoute route = new HttpRoute(new HttpHost("localhost", 80));
// Request new connection. This can be a long process
ConnectionRequest connRequest = connMrg.requestConnection(route, null);
// Wait for connection up to 10 sec
HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
try {
    // If not open
    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);
    }
    // Do useful things with the connection.
} finally {
    connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
}

在連線請求( ConnectionRequest.get() )可以被 ConnectionRequest.cancel()提前中斷,中斷後,阻塞在 ConnectionRequest.get() 上的執行緒會解除阻塞狀態。

2.2.1 BasicHttpClientConnectionManager

BasicHttpClientConnectionManager 是簡單的連線管理器,同一時刻,只維持一個連線。雖然這個類是執行緒安全的,但是它應該只能被一個執行執行緒呼叫。BasicHttpClientConnectionManager 會重新利用之前的連線(使用相同的route)。另外,它可能也會關閉之前的連線,然後重新連線。

2.2.2 PoolingHttpClientConnectionManager

PoolingHttpClientConnectionManager 可以管理多個連線(管理連線池)。當用戶請求一個連線時,如果存在一個持久化的、相同的route的連線,則PoolingHttpClientConnectionManager 會返回一個之前的連線。

預設情況下,PoolingHttpClientConnectionManager 最大對於每個route維護2個連線,總共管理20個連線。可能這些限制太多了,你可以自己通過程式設計進行修改。

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();

// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);

// Increase max connections for localhost:80 to 50
HttpHost localhost = new HttpHost("locahost", 80);
cm.setMaxPerRoute(new HttpRoute(localhost), 50);

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

注意:當HttpClient不需要了後,一定要將Httpclient關閉(不僅僅將某個連線關閉),使得管理的connection manager關閉,同時關閉connection manager管理的連線。
(Httpclient 例項會繫結 connection manager,關閉httpclient時,會關閉繫結的connection manager, HttpClient connection manager預設實現是使用SimpleHttpConnectionManager。預設情況下,關閉連線不是真正的關閉連線,而是將連線還給 connectionmanager,該連線之後處於什麼狀態,由connectionmanager管理)。

關閉httpclient:

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

2.2.3 連線的分配管理


當使用PoolingClientConnectionManager後,則Httpclient可被多個執行緒使用,用於同時執行多個請求。PoolingClientConnectionManager會根據配置進行連線的分配,原則:

  1. 如果工作執行緒請求一個針對某個route的連線,且當前polingClientConnectionManager管理的該route點對應的連線都被租出去了,則該執行緒會阻塞,直到有對應的連線會還回來;
  2. 如果設定http.conn-manager.timeout為一個正數,則當沒有連線可用時,阻塞的最大時間為該值,如果到了這個時間依然沒有連線可用,則會丟擲ConnectionPoolTimeoutException 異常。

使用例項:

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

//get請求的url列表
String[] urisToGet = {
    "http://www.domain1.com/",
    "http://www.domain2.com/",
    "http://www.domain3.com/",
    "http://www.domain4.com/"
};

// 每個執行緒負責一個uri資源的訪問
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);
}

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

//等待所有執行緒任務執行結束
for (int j = 0; j < threads.length; j++) {
    threads[j].join();
}

2.2.4 多執行緒訪問httpclient, 每個執行緒維護自己的httpContext


注意:
雖然Httpclient物件是執行緒安全的,可以在多執行緒間共享,但是httpcontext不是執行緒安全的。建議每個執行緒維護自己的httpContext例項.

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 {
//在通過httpclient執行方法時,通過傳入當前執行緒的context物件,而不是使用預設的公共的httpcontext
            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.2.5 刪除過期的connection的最佳策略


當socket阻塞在io操作時,還是可以響應IO事件。但是,對於connection來說,即使connection被還回來了,狀態是還是活躍的,但是它不能監控socket的狀態,無法對IO事件做出反應。例如,當server端關閉了connection, client端的connection無法自身及時反饋這種變化(和及時的關閉client端關聯的socket)。

在Httpclient中,處理上述問題的最好的辦法是:新建一個獨立執行緒,該執行緒的任務就是週期性的呼叫connectionmanager的closeExpiredConnections來關閉過期的connection(server端已經管理的連線)、呼叫closeIdleConnections來關閉長時間不用的連線。(同時你可以在使用connection之前,檢查connection的stale狀態,但是該方法不是100%可靠。)

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);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections
                    // that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }
    
    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }
    
}

2.2.6 連線keep-alive配置


Http規範沒有明確規定一個持久化的connection應該存活多久。某些Http 伺服器不是使用標準的keep-alive 請求頭來保持連線的有效性;另外,有的server會關閉一段時間內不活躍的connection(且不會通知client) 來保證server的資源可用性。綜上所述,你可能需要自定義提供怎麼保持connection的可用性(keep-alive).

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.2.7 connection socket 工廠

httpconnection使用java.net.Socket物件來處理底層的資料傳輸。在Httpclient中,使用ConnectionSocketFactory 來負責socket的建立、初始化和socket連線。預設情況下,Httpclient使用的是PlainConnectionSocketFactory。