HttpClient 4.3.6教程 第2章 串連管理 【翻譯】,httpclient4.3.6
第2章 串連管理2.1 持久串連 一個主機與另一端建立串連是十分複雜的,並且兩個終端間要交換多個資訊包,這會耗費不少時間。對於低級的HTTP訊息來說握手串連是尤其重要的。如果在執行多個請求時重複使用公用的串連,那麼就能大大提高資料吞吐率。
HTTP/1.1預設允許HTTP串連可以被多個請求複用。HTTP/1.0也相容終端為了多個請求去使用一個明確的機制來優先保持活躍的串連。HTTP代理也能在一定的同期時間裡保持活躍的空閑串連,以免同樣的目標主機隨後還要請求。這種保持活躍串連的能力通常都會涉及持久性串連。HttpClient完全支援“持久性串連”。
2.2 Http連線路由 HttpClient可以建立串連給主機或路由[包含複雜的中間串連——也被稱為hops(彈跳)]。HttpClient會區分不同的路由串連(平坦、路徑和分層)。使用多個中間代理服務去打通目標主機串連的方式稱為代理連結。
正在串連中、第一次串連或只用代理串連都會建立“平坦路由”。第一次串連和通過代理連結都會建立“通道路由”。路由離開代理是不能產生路徑的。當一個分層協議結束一個存在的串連就會建立“分層路由”。當結束一個目標路徑或結束一個不再代理的串連後,協議就會建立分層。
2.2.1 路由計算 RouteInfo介面代表一個確定的目標主機路徑的資訊,涉及一個或更多的中間步驟或hops(彈跳)。HttpRoute是一個具體的RouteInfo實現,它是不能被改變的(是不可變的)。HttpTracker是一個可變的RouteInfo執行情況,用於HttpClient在內部追蹤剩餘的指向最終路由目標的hops(彈跳)。如果下一次向著目標的hop(彈跳)執行成功,HttpTracker會被更新。HttpRouteDirector是一個協助類,它可以用來計算路由的下一個步驟。這個類在HttpClient內部被使用。
HttpRoutePlanner是一個介面代表著一個策略,用於計算一個基於執行內容的完整的路線。HttpClient包含了兩種預設HttpRoutePlanner的實現。SystemDefaultRoutePlanner是基於java.net.ProxySelector的。預設情況下,他會接載JVM的代理設定(會在系統特性或應用上啟動並執行瀏覽器選擇其中一個設定)。DefaultProxyRoutePlanner實現不會利用任何Java系統特性,也不會使用任何系統或瀏覽器的代理設定。它總是通過相同的預設代理服務來計算路由。
2.2.2 安全的HTTP串連 如果兩個終端間正在傳輸著的資訊不能被未授權的人讀取或篡改,那麼HTTP串連就被認為是安全的。SSL/TLS協議被廣泛用在HTTP傳輸安全上。然而,其他的加密手段也有被使用。通常,HTTP傳輸在SSL/TLS加密串連上是被分層的。
2.3 HTTP串連管理2.3.1 管理串連和串連管理者 HTTP串連是複雜的、狀態性強的、線程不安全的,它需要適當地去管理。HTTP串連每次只能被一個執行線程使用。HttpClientConnectionManager 介面是HttpClient用來管理HTTP串連的特別實體。HTTP連線管理員的目的是為建立HTTP串連充當一個工廠,以管理持續串連的生命週期和同步入口以持續串連,確保每次只有一個線程可以進入一個串連。內部HTTP連線管理員與ManagedHttpClientConnection執行個體一起工作,為一個真實串連去充當一個代理服務,以管理串連狀態和控制執行I/O製作。如何一個管理串連被消費者釋放或被明確地關閉了,底層串連會從代理服務裡分離,並返回給管理器。儘管這個服務消費者會保持代理服務執行個體的引用,但是不再允許執行任何I/O操作,也不會有意或無意得去改變真實串連的狀態。
這是從連線管理員裡獲得一個串連的例子:
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 secHttpClientConnection 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#cancel()提早中止。這將使得在ConnectionRequest#get()方法內會解除線程阻塞。
2.3.2 簡單的串連管理 BasicHttpClientConnectionManager是一個簡單的連線管理員,它每次只能保持一個串連。儘管這個類是安全執行緒的,但它只能被一個執行的線程使用。BasicHttpClientConnectionManager會為隨後的同樣路由的請求嘗試重用這個串連。如果這個持續串連的路由與串連請求不匹配,它會為了指定的路由而關閉現有的串連並重新開啟它。如何這個串連已經被分配,就會拋出java.lang.IllegalStateException異常。
連線管理員的實現應該在一個EJB容器內使用。
2.3.3 池連線管理員 PoolingHttpClientConnectionManager是一個更複雜的實現,它管理一個用戶端串連池,並為線程的串連請求提供服務。串連都被彙集在每個路由基礎上。對於一個路由請求,如果管理器在池裡已有一個可用的持續串連,則不會建立一個新的,而是租用池裡的這個串連。
PoolingHttpClientConnectionManager維護著在每個路由基礎上串連數目的上限。每個預設的實現不會建立超過2個並行串連,每個指定的路由總共不會超過20個串連。對於許多現實的應用來說,這些限制可能會過於約束,尤其是當他們為他們的伺服器使用HTTP傳輸協議時。
這個例子示範了如何調整串連池參數:
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();// Increase max total connection to 200cm.setMaxTotal(200);// Increase default max connection per route to 20cm.setDefaultMaxPerRoute(20);// Increase max connections for localhost:80 to 50HttpHost localhost = new HttpHost("locahost", 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();// URIs to perform GETs onString[] urisToGet = { "http://www.domain1.com/", "http://www.domain2.com/", "http://www.domain3.com/", "http://www.domain4.com/"};// create a thread for each URIGetThread[] 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 threadsfor (int j = 0; j < threads.length; j++) { threads[j].start();}// join the threadsfor (int j = 0; j < threads.length; j++) { threads[j].join();}
HttpClient執行個體是安全執行緒的並且可以在執行著的多線程間共用,並強烈推薦每個線程維護自己的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 { 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事件。如果這個串連在伺服器端被關閉,用戶端的串連在串連狀態(和由於結束時正在關閉而作出的適當響應)下不會檢測出這個改變。
HttpClient通過測試連接是否為“陳腐的”而嘗試去緩解這個問題,“陳腐的”是指不再是有效,因為它會被伺服器端關閉掉,並會在這之前為了正執行中的HTTP請求去使用串連。“陳腐的”串連檢測不是百分之百有效,並且會給每個請求執行增加10到10毫秒。為了空閑串連,唯一有效解決辦法是在每個socket模型裡不包含一個線程,有一個專門的監聽線程是被用來驅逐已到期的不活躍的長串連的。這個監聽線程會周期性地調用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); // 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.6 串連保持活躍策略 HTTP規範沒有明確指定一個持續串連最多可以保持活躍有多久。一些HTTP伺服器會使用一個非標準的Keep-Alive(保持活躍)標題來告訴用戶端,他們計劃在伺服器端保持串連活躍的時間(以秒為單位)。如果可以獲得的話,HttpClient就會使用這些資訊。如果Keep-Alive標題沒有出現在應答裡,HttpClient會假定這個串連可以無限期地保持活躍。然而,許多HTTP伺服器通常會在一段不活躍時期後被配置成放棄持續串連,為了儲存系統,這經常不會通知用戶端。預設的策略似乎太過於樂觀了,你可能會想提供一個自訂保持活躍策略。
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對象去處理從電線傳輸過來的資料,然而他們依靠介面去建立、初始化和串連socket。在運行時允許HttpClient使用者裝備指定的socket初始化代碼。PlainConnectionSocketFactory是一個預設的工作,用於建立和初始化平坦(未加密的)socket。
建立socket的過程和將它串連去一個主機是脫鉤的,所以當正在一個串連操作裡阻塞的時候,應該閉關掉socket。
HttpClientContext clientContext = HttpClientContext.create();PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory();Socket socket = sf.createSocket(clientContext);int timeout = 1000; //msHttpHost target = new HttpHost("localhost");InetSocketAddress remoteAddress = new InetSocketAddress( InetAddress.getByAddress(new byte[] {127,0,0,1}), 80);sf.connectSocket(timeout, socket, target, remoteAddress, null, clientContext);
2.7.1 安全的socket分層
LayeredConnectionSocketFactory 是一個ConnectionSocketFactory 介面的擴充。分層的socket工廠有能力在一個現存的平坦socket上建立分層的socket。分層的socket會首先會被代理服務使用來建立安全的socket。HttpClient包含了SSLSocketFactory,以實現SSL/TLS分層。請注意,HttpClient不會使用任何自訂的加密功能。它是完全依賴於標準的Java Cryptography (JCE) and Secure Sockets (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利用SSLConnectionSocketFactory來建立SSL串連。SSLConnectionSocketFactory允許高度的定製。它可以把javax.net.ssl.SSLContext的執行個體看作是一個參數,並用它來建立自訂的SSL串連配置。
KeyStore myTrustStore = <...>SSLContext sslContext = SSLContexts.custom() .useTLS() .loadTrustMaterial(myTrustStore) .build();SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
定製SSLConnectionSocketFactory要對SSL/TLS協議有更深入的掌握,這已超出了本文檔的說明範圍。javax.net.ssl.SSLContext詳細的說明和相關的工具使用,請參考Java Secure Socket Extension(連結:http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html)。
2.7.4 主機名稱檢驗
除了在SSL/TLS協議級上託管檢驗和客戶身份評鑑外,一旦串連被建立,HttpClient還能選擇性地檢驗是否目標主機名稱與存放在伺服器上的X.509認證相匹配。這個檢驗可以為伺服器相信材料的可靠性提供額外的保證。X509HostnameVerifier 介面代表一個用於主機名稱檢驗的策略。HttpClient包含了三個 X509HostnameVerifier實現。注意:主機名稱檢驗不要被SSL託管檢驗給搞混淆了。
StrictHostnameVerifier(精確主機名稱的檢驗器):精確的主機名稱檢驗器工作在類似於Sun Java 1.4, Sun Java 5, Sun Java 6裡。它與IE6的關係也相當緊密。這個實現符合RFC 2818,因為要處理萬用字元。主機名稱必須要麼匹配第一個CN,要麼匹配任意的subject-alts。萬用字元可以出現在CN裡,和任意的subject-alts裡。
BrowserCompatHostnameVerifier(瀏覽器安全色主機名稱的檢驗器):這個主機名稱檢驗器工作在類似於Curl和Firefox瀏覽器裡。主機名稱必須要麼匹配第一個CN,要麼匹配任意的subject-alts。萬用字元可以出現在CN裡,和任意的subject-alts裡。BrowserCompatHostnameVerifier和 StrictHostnameVerifier唯一的不同是,BrowserCompatHostnameVerifier的萬用字元(如"*.foo.com")會匹配所有的子域,包括"a.b.foo.com"。
AllowAllHostnameVerifier(充許所有主機名稱的檢驗器):這個主機名稱檢驗器在本質上會關掉主機檢驗。這個實現是無操作的(no-op),並且永遠會拋出javax.net.ssl.SSLException異常。
預設的HttpClient使用BrowserCompatHostnameVerifier實現。如要需要,你可以指定不同的主機名稱檢驗器。
SSLContext sslContext = SSLContexts.createSystemDefault();SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslContext, SSLConnectionSocketFactory.STRICT_HOSTNAME_VERIFIER);
2.8 HttpClientProxy 伺服器配置
儘管HttpClient知道複雜的路由體系和代理服務鏈結接,但它只支援簡單的定位或一個離開的跳躍(hop)代理串連。
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(); }}
譯者:lianghongge