轉 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); }
如果要終止連接,可以調用ConnectionRequest
的cancel()
方法。這個方法會解鎖被ConnectionRequest
類get()
方法阻塞的線程。
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分層
LayeredConnectionSocketFactory
是ConnectionSocketFactory
的拓展接口。分層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.StrictHostnameVerifier
和BrowserCompatHostnameVerifier
方式唯一不同的地方就是,帶有通配符的域名(比如*.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