轉自:http://www.cnblogs.com/loveyakamoz/archive/2011/07/21/2112832.htmlhtml
第二章 鏈接管理java
HttpClient有一個對鏈接初始化和終止,還有在活動鏈接上I/O操做的完整控制。而鏈接操做的不少方面可使用一些參數來控制。node
這些參數能夠影響鏈接操做:算法
從一個主機向另一個創建鏈接的過程是至關複雜的,並且包含了兩個終端之間的不少包的交換,它是至關費時的。鏈接握手的開銷是很重要的,特別是對小量的HTTP報文。若是打開的鏈接能夠被重用來執行屢次請求,那麼就能夠達到很高的數據吞吐量。瀏覽器
HTTP/1.1強調HTTP鏈接默認狀況能夠被重用於屢次請求。HTTP/1.0兼容的終端也可使用類似的機制來明確地交流它們的偏好來保證鏈接處於活動狀態,也使用它來處理多個請求。HTTP代理也能夠保持空閒鏈接處於一段時間的活動狀態,防止對相同目標主機的一個鏈接也許對隨後的請求須要。保持鏈接活動的能力一般被稱做持久性鏈接。HttpClient徹底支持持久性鏈接。緩存
HttpClient可以直接或經過路由創建鏈接到目標主機,這會涉及多箇中間鏈接,也被稱爲跳。HttpClient區分路由和普通鏈接,通道和分層。通道鏈接到目標主機的多箇中間代理的使用也稱做是代理鏈。安全
普通路由由鏈接到目標或僅第一次的代理來建立。通道路由經過代理鏈到目標鏈接到第一通道來創建。沒有代理的路由不是通道的,分層路由經過已存在鏈接的分層協議來創建。協議僅僅能夠在到目標的通道上或在沒有代理的直接鏈接上分層。服務器
RouteInfo接口表明關於最終涉及一個或多箇中間步驟或跳的目標主機路由的信息。HttpRoute是RouteInfo的具體實現,這是不能改變的(是不變的)。HttpTracker是可變的RouteInfo實現,由HttpClient在內部使用來跟蹤到最大路由目標的剩餘跳數。HttpTracker能夠在成功執行向路由目標的下一跳以後更新。HttpRouteDirector是一個幫助類,能夠用來計算路由中的下一跳。這個類由HttpClient在內部使用。網絡
HttpRoutePlanner是一個表明計算到基於執行上下文到給定目標完整路由策略的接口。HttpClient附帶兩個默認的HttpRoutePlanner實現。ProxySelectorRoutePlanner是基於java.net.ProxySelector的。默認狀況下,它會從系統屬性中或從運行應用程序的瀏覽器中選取JVM的代理設置。DefaultHttpRoutePlanner實現既不使用任何Java系統屬性,也不使用系統或瀏覽器的代理設置。它只基於HTTP以下面描述的參數計算路由。多線程
若是信息在兩個不能由非認證的第三方進行讀取或修改的終端之間傳輸,HTTP鏈接能夠被認爲是安全的。SSL/TLS協議是用來保證HTTP傳輸安全使用最普遍的技術。而其它加密技術也能夠被使用。一般來講,HTTP傳輸是在SSL/TLS加密鏈接之上分層的。
LayeredSocketFactory是SocketFactory接口的擴展。分層的套接字工廠可HTTP鏈接內部使用java.net.Socket對象來處理數據在線路上的傳輸。它們依賴SocketFactory接口來建立,初始化和鏈接套接字。這會使得HttpClient的用戶能夠提供在運行時指定套接字初始化代碼的應用程序。PlainSocketFactory是建立和初始化普通的(不加密的)套接字的默認工廠。
建立套接字的過程和鏈接到主機的過程是不成對的,因此套接字在鏈接操做封鎖時能夠被關閉。
PlainSocketFactory sf = PlainSocketFactory.getSocketFactory();Socket socket = sf.createSocket();HttpParams params = new BasicHttpParams();params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 1000L);sf.connectSocket(socket, "locahost", 8080, null, -1, params);
LayeredSocketFactory是SocketFactory接口的擴展。分層的套接字工廠能夠建立在已經存在的普通套接字之上的分層套接字。套接字分層主要經過代理來建立安全的套接字。HttpClient附帶實現了SSL/TLS分層的SSLSocketFactory。請注意HttpClient不使用任何自定義加密功能。它徹底依賴於標準的Java密碼學(JCE)和安全套接字(JSEE)擴展。
HttpClient使用SSLSocketFactory來建立SSL鏈接。SSLSocketFactory容許高度定製。它可使用javax.net.ssl.SSLContext的實例做爲參數,並使用它來建立定製SSL鏈接。
TrustManager easyTrustManager = new X509TrustManager() {@Overridepublic void checkClientTrusted(X509Certificate[] chain,String authType) throws CertificateException {// 哦,這很簡單!}@Overridepublic void checkServerTrusted(X509Certificate[] chain,String authType) throws CertificateException {//哦,這很簡單!}@Overridepublic X509Certificate[] getAcceptedIssuers() {return null;}};SSLContext sslcontext = SSLContext.getInstance("TLS");sslcontext.init(null, new TrustManager[] { easyTrustManager }, null);SSLSocketFactory sf = new SSLSocketFactory(sslcontext);SSLSocket socket = (SSLSocket) sf.createSocket();socket.setEnabledCipherSuites(new String[] { "SSL_RSA_WITH_RC4_128_MD5" });HttpParams params = new BasicHttpParams();params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 1000L);sf.connectSocket(socket, "locahost", 443, null, -1, params);
每個默認的HttpClient使用BrowserCompatHostnameVerifier的實現。若是須要的話,它能夠指定不一樣的主機名驗證器實現。
SSLSocketFactory sf = new SSLSocketFactory(SSLContext.getInstance("TLS"));sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
Scheme類表明了一個協議模式,好比「http」或「https」同時包含一些協議屬性,好比默認端口,用來爲給定協議建立java.net.Socket實例的套接字工廠。SchemeRegistry類用來維持一組Scheme,當去經過請求URI創建鏈接時,HttpClient能夠從中選擇:
Scheme http = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);SSLSocketFactory sf = new SSLSocketFactory(SSLContext.getInstance("TLS"));sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);Scheme https = new Scheme("https", sf, 443);SchemeRegistry sr = new SchemeRegistry();sr.register(http);sr.register(https);
儘管HttpClient瞭解複雜的路由模式和代理鏈,它僅支持簡單直接的或開箱的跳式代理鏈接。
告訴HttpClient經過代理去鏈接到目標主機的最簡單方式是經過設置默認的代理參數:
DefaultHttpClient httpclient = new DefaultHttpClient();HttpHost proxy = new HttpHost("someproxy", 8080);httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
也能夠構建HttpClient使用標準的JRE代理選擇器來得到代理信息:
DefaultHttpClient httpclient = new DefaultHttpClient();ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(httpclient.getConnectionManager().getSchemeRegistry(),ProxySelector.getDefault());httpclient.setRoutePlanner(routePlanner);
另一種選擇,能夠提供一個定製的RoutePlanner實現來得到HTTP路由計算處理上的複雜的控制:
DefaultHttpClient httpclient = new DefaultHttpClient();httpclient.setRoutePlanner(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()));}});
鏈接操做是客戶端的低層套接字或能夠經過外部實體,一般稱爲鏈接操做的被操做的狀態的鏈接。OperatedClientConnection接口擴展了HttpClientConnection接口並且定義了額外的控制鏈接套接字的方法。ClientConnectionOperator接口表明了建立實例和更新那些對象低層套接字的策略。實現類最有可能利用SocketFactory來建立java.net.Socket實例。ClientConnectionOperator接口可讓HttpClient的用戶提供一個鏈接操做的定製策略和提供可選實現OperatedClientConnection接口的能力。
HTTP鏈接是複雜的,有狀態的,線程不安全的對象須要正確的管理以便正確地執行功能。HTTP鏈接在同一時間僅僅只能由一個執行線程來使用。HttpClient採用一個特殊實體來管理訪問HTTP鏈接,這被稱爲HTTP鏈接管理器,表明了ClientConnectionManager接口。一個HTTP鏈接管理器的目的是做爲工廠服務於新的HTTP鏈接,管理持久鏈接和同步訪問持久鏈接來確保同一時間僅有一個線程能夠訪問一個鏈接。
內部的HTTP鏈接管理器和OperatedClientConnection實例一塊兒工做,可是它們爲服務消耗器ManagedClientConnection提供實例。ManagedClientConnection扮演鏈接之上管理狀態控制全部I/O操做的OperatedClientConnection實例的包裝器。它也抽象套接字操做,提供打開和更新去建立路由套接字便利的方法。ManagedClientConnection實例瞭解產生它們到鏈接管理器的連接,並且基於這個事實,當再也不被使用時,它們必須返回到管理器。ManagedClientConnection類也實現了ConnectionReleaseTrigger接口,能夠被用來觸發釋放鏈接返回給管理器。一旦釋放鏈接操做被觸發了,被包裝的鏈接從ManagedClientConnection包裝器中脫離,OperatedClientConnection實例被返回給管理器。儘管服務消耗器仍然持有ManagedClientConnection實例的引用,它也再也不去執行任何I/O操做或有意無心地改變的OperatedClientConnection狀態。
這裏有一個從鏈接管理器中獲取鏈接的示例:
HttpParams params = new BasicHttpParams();Scheme http = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);SchemeRegistry sr = new SchemeRegistry();sr.register(http);ClientConnectionManager connMrg = new SingleClientConnManager(params, sr);// 請求新鏈接。這多是一個很長的過程。ClientConnectionRequest connRequest = connMrg.requestConnection(new HttpRoute(new HttpHost("localhost", 80)), null);// 等待鏈接10秒ManagedClientConnection conn = connRequest.getConnection(10, TimeUnit.SECONDS);try {// 用鏈接在作有用的事情。當完成時釋放鏈接。conn.releaseConnection();} catch (IOException ex) {// 在I/O error之上終止鏈接。conn.abortConnection();throw ex;}
若是須要,鏈接請求能夠經過調用來ClientConnectionRequest#abortRequest()方法過早地中斷。這會解鎖在ClientConnectionRequest#getConnection()方法中被阻止的線程。
一旦響應內容被徹底消耗後,BasicManagedEntity包裝器類能夠用來保證自動釋放低層的鏈接。HttpClient內部使用這個機制來實現透明地對全部從HttpClient#execute()方法中得到響應釋放鏈接:
ClientConnectionRequest connRequest = connMrg.requestConnection(new HttpRoute(new HttpHost("localhost", 80)), null);ManagedClientConnection conn = connRequest.getConnection(10, TimeUnit.SECONDS);try {BasicHttpRequest request = new BasicHttpRequest("GET", "/");conn.sendRequestHeader(request);HttpResponse response = conn.receiveResponseHeader();conn.receiveResponseEntity(response);HttpEntity entity = response.getEntity();if (entity != null) {BasicManagedEntity managedEntity = new BasicManagedEntity(entity, conn, true);// 替換實體response.setEntity(managedEntity);}// 使用響應對象作有用的事情。當響應內容被消耗後這個鏈接將會自動釋放。} catch (IOException ex) {//在I/O error之上終止鏈接。conn.abortConnection();throw ex;}
SingleClientConnManager是一個簡單的鏈接管理器,在同一時間它僅僅維護一個鏈接。儘管這個類是線程安全的,但它應該被用於一個執行線程。SingleClientConnManager對於同一路由的後續請求會盡可能重用鏈接。而若是持久鏈接的路由不匹配鏈接請求的話,它也會關閉存在的鏈接以後對給定路由再打開一個新的。若是鏈接已經被分配,將會拋出java.lang.IllegalStateException異常。
對於每一個默認鏈接,HttpClient使用SingleClientConnManager。
ThreadSafeClientConnManager是一個複雜的實現來管理客戶端鏈接池,它也能夠從多個執行線程中服務鏈接請求。對每一個基本的路由,鏈接都是池管理的。對於路由的請求,管理器在池中有可用的持久性鏈接,將被從池中租賃鏈接服務,而不是建立一個新的鏈接。
ThreadSafeClientConnManager維護每一個基本路由的最大鏈接限制。每一個默認的實現對每一個給定路由將會建立不超過兩個的併發鏈接,而總共也不會超過20個鏈接。對於不少真實的應用程序,這個限制也證實很大的制約,特別是他們在服務中使用HTTP做爲傳輸協議。鏈接限制,也可使用HTTP參數來進行調整。
這個示例展現了鏈接池參數是如何來調整的:
HttpParams params = new BasicHttpParams();// 增長最大鏈接到200ConnManagerParams.setMaxTotalConnections(params, 200);// 增長每一個路由的默認最大鏈接到20ConnPerRouteBean connPerRoute = new ConnPerRouteBean(20);// 對localhost:80增長最大鏈接到50HttpHost localhost = new HttpHost("locahost", 80);connPerRoute.setMaxForRoute(new HttpRoute(localhost), 50);ConnManagerParams.setMaxConnectionsPerRoute(params, connPerRoute);SchemeRegistry schemeRegistry = new SchemeRegistry();schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);HttpClient httpClient = new DefaultHttpClient(cm, params);
當一個HttpClient實例再也不須要時,並且即將走出使用範圍,那麼關閉鏈接管理器來保證由管理器保持活動的全部鏈接被關閉,由鏈接分配的系統資源被釋放是很重要的。
DefaultHttpClient httpclient = new DefaultHttpClient();HttpGet httpget = new HttpGet("http://www.google.com/");HttpResponse response = httpclient.execute(httpget);HttpEntity entity = response.getEntity();System.out.println(response.getStatusLine());if (entity != null) {entity.consumeContent();}httpclient.getConnectionManager().shutdown();
當配備鏈接池管理器時,好比ThreadSafeClientConnManager,HttpClient能夠同時被用來執行多個請求,使用多線程執行。
ThreadSafeClientConnManager將會分配基於它的配置的鏈接。若是對於給定路由的全部鏈接都被租出了,那麼鏈接的請求將會阻塞,直到一個鏈接被釋放回鏈接池。它能夠經過設置'http.conn-manager.timeout'爲一個正數來保證鏈接管理器不會在鏈接請求執行時無限期的被阻塞。若是鏈接請求不能在給定的時間週期內被響應,將會拋出ConnectionPoolTimeoutException異常。
HttpParams params = new BasicHttpParams();SchemeRegistry schemeRegistry = new SchemeRegistry();schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);HttpClient httpClient = new DefaultHttpClient(cm, params);// 執行GET方法的URIString[] 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);}
// 開始執行線程for (int j = 0; j < threads.length; j++) {threads[j].start();}// 合併線程for (int j = 0; j < threads.length; j++) {threads[j].join();}static class GetThread extends Thread {private final HttpClient httpClient;private final HttpContext context;private final HttpGet httpget;public GetThread(HttpClient httpClient, HttpGet httpget) {this.httpClient = httpClient;this.context = new BasicHttpContext();this.httpget = httpget;}@Overridepublic void run() {try {HttpResponse response = this.httpClient.execute(this.httpget, this.context);HttpEntity entity = response.getEntity();if (entity != null) {// 對實體作些有用的事情...// 保證鏈接能釋放回管理器entity.consumeContent();}} catch (Exception ex) {this.httpget.abort();}}}
一個經典的阻塞I/O模型的主要缺點是網絡套接字僅當I/O操做阻塞時才能夠響應I/O事件。當一個鏈接被釋放返回管理器時,它能夠被保持活動狀態而卻不能監控套接字的狀態和響應任何I/O事件。若是鏈接在服務器端關閉,那麼客戶端鏈接也不能去偵測鏈接狀態中的變化和關閉本端的套接字去做出適當響應。
HttpClient經過測試鏈接是不是過期的來嘗試去減輕這個問題,這已經再也不有效了,由於它已經在服務器端關閉了,以前使用執行HTTP請求的鏈接。過期的鏈接檢查也並非100%的穩定,反而對每次請求執行還要增長10到30毫秒的開銷。惟一可行的而不涉及到每一個對空閒鏈接的套接字模型線程解決方案,是使用專用的監控線程來收回由於長時間不活動而被認爲是過時的鏈接。監控線程能夠週期地調用ClientConnectionManager#closeExpiredConnections()方法來關閉全部過時的鏈接,從鏈接池中收回關閉的鏈接。它也能夠選擇性調用ClientConnectionManager#closeIdleConnections()方法來關閉全部已經空閒超過給定時間週期的鏈接。
public static class IdleConnectionMonitorThread extends Thread {private final ClientConnectionManager connMgr;private volatile boolean shutdown;public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {super();this.connMgr = connMgr;}@Overridepublic void run() {try {while (!shutdown) {synchronized (this) {wait(5000);// 關閉過時鏈接connMgr.closeExpiredConnections();// 可選地,關閉空閒超過30秒的鏈接connMgr.closeIdleConnections(30, TimeUnit.SECONDS);}}} catch (InterruptedException ex) {// 終止}}public void shutdown() {shutdown = true;synchronized (this) {notifyAll();}}}
HTTP規範沒有肯定一個持久鏈接可能或應該保持活動多長時間。一些HTTP服務器使用非標準的頭部信息Keep-Alive來告訴客戶端它們想在服務器端保持鏈接活動的週期秒數。若是這個信息可用,HttClient就會利用這個它。若是頭部信息Keep-Alive在響應中不存在,HttpClient假設鏈接無限期的保持活動。然而許多現實中的HTTP服務器配置了在特定不活動週期以後丟掉持久鏈接來保存系統資源,每每這是不通知客戶端的。若是默認的策略證實是過於樂觀的,那麼就會有人想提供一個定製的保持活動策略。