HttpClient 教程 (二)

第二章 連接管理

HttpClient有一個(gè)對(duì)連接初始化和終止,還有在活動(dòng)連接上I/O操作的完整控制。而連接操作的很多方面可以使用一些參數(shù)來(lái)控制。

2.1 連接參數(shù)

這些參數(shù)可以影響連接操作:

'http.socket.timeout':定義了套接字的毫秒級(jí)超時(shí)時(shí)間(SO_TIMEOUT),這就是等待數(shù)據(jù),換句話(huà)說(shuō),在兩個(gè)連續(xù)的數(shù)據(jù)包之間最大的閑置時(shí)間。如果超時(shí)時(shí)間是0就解釋為是一個(gè)無(wú)限大的超時(shí)時(shí)間。這個(gè)參數(shù)期望得到一個(gè)java.lang.Integer類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么讀取操作就不會(huì)超時(shí)(無(wú)限大的超時(shí)時(shí)間)。

'http.tcp.nodelay':決定了是否使用Nagle算法。Nagle算法視圖通過(guò)最小化發(fā)送的分組數(shù)量來(lái)節(jié)省帶寬。當(dāng)應(yīng)用程序希望降低網(wǎng)絡(luò)延遲并提高性能時(shí),它們可以關(guān)閉Nagle算法(也就是開(kāi)啟TCP_NODELAY)。數(shù)據(jù)將會(huì)更早發(fā)送,增加了帶寬消耗的成文。這個(gè)參數(shù)期望得到一個(gè)java.lang.Boolean類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么TCP_NODELAY就會(huì)開(kāi)啟(無(wú)延遲)。

'http.socket.buffer-size':決定了內(nèi)部套接字緩沖使用的大小,來(lái)緩沖數(shù)據(jù)同時(shí)接收/傳輸HTTP報(bào)文。這個(gè)參數(shù)期望得到一個(gè)java.lang.Integer類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么HttpClient將會(huì)分配8192字節(jié)的套接字緩存。

'http.socket.linger':使用指定的秒數(shù)拖延時(shí)間來(lái)設(shè)置SO_LINGER。最大的連接超時(shí)值是平臺(tái)指定的。值0暗示了這個(gè)選項(xiàng)是關(guān)閉的。值-1暗示了使用了JRE默認(rèn)的。這個(gè)設(shè)置僅僅影響套接字關(guān)閉操作。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么就假設(shè)值為-1(JRE默認(rèn))。

'http.connection.timeout':決定了直到連接建立時(shí)的毫秒級(jí)超時(shí)時(shí)間。超時(shí)時(shí)間的值為0解釋為一個(gè)無(wú)限大的時(shí)間。這個(gè)參數(shù)期望得到一個(gè)java.lang.Integer類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,連接操作將不會(huì)超時(shí)(無(wú)限大的超時(shí)時(shí)間)。

'http.connection.stalecheck':決定了是否使用舊的連接檢查。當(dāng)在一個(gè)連接之上執(zhí)行一個(gè)請(qǐng)求而服務(wù)器端的連接已經(jīng)關(guān)閉時(shí),關(guān)閉舊的連接檢查可能導(dǎo)致在獲得一個(gè)I/O錯(cuò)誤風(fēng)險(xiǎn)時(shí)顯著的性能提升(對(duì)于每一個(gè)請(qǐng)求,檢查時(shí)間可以達(dá)到30毫秒)。這個(gè)參數(shù)期望得到一個(gè)java.lang.Boolean類(lèi)型的值。出于性能的關(guān)鍵操作,檢查應(yīng)該被關(guān)閉。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么舊的連接將會(huì)在每個(gè)請(qǐng)求執(zhí)行之前執(zhí)行。

'http.connection.max-line-length':決定了最大請(qǐng)求行長(zhǎng)度的限制。如果設(shè)置為一個(gè)正數(shù),任何HTTP請(qǐng)求行超過(guò)這個(gè)限制將會(huì)引發(fā)java.io.IOException異常。負(fù)數(shù)或零將會(huì)關(guān)閉這個(gè)檢查。這個(gè)參數(shù)期望得到一個(gè)java.lang.Integer類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么就不強(qiáng)制進(jìn)行限制了。

'http.connection.max-header-count':決定了允許的最大HTTP頭部信息數(shù)量。如果設(shè)置為一個(gè)正數(shù),從數(shù)據(jù)流中獲得的HTTP頭部信息數(shù)量超過(guò)這個(gè)限制就會(huì)引發(fā)java.io.IOException異常。負(fù)數(shù)或零將會(huì)關(guān)閉這個(gè)檢查。這個(gè)參數(shù)期望得到一個(gè)java.lang.Integer類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么就不

強(qiáng)制進(jìn)行限制了。

'http.connection.max-status-line-garbage':決定了在期望得到HTTP響應(yīng)狀態(tài)行之前可忽略請(qǐng)求行的最大數(shù)量。使用HTTP/1.1持久性連接,這個(gè)問(wèn)題產(chǎn)生的破碎的腳本將會(huì)返回一個(gè)錯(cuò)誤的Content-Length(有比指定的字節(jié)更多的發(fā)送)。不幸的是,在某些情況下,這個(gè)不能在錯(cuò)誤響應(yīng)后來(lái)偵測(cè),只能在下一次之前。所以HttpClient必須以這種方式跳過(guò)那些多余的行。這個(gè)參數(shù)期望得到一個(gè)java.lang.Integer類(lèi)型的值。0是不允許在狀態(tài)行之前的所有垃圾/空行。使用java.lang.Integer#MAX_VALUE來(lái)設(shè)置不限制的數(shù)字。如果這個(gè)參數(shù)沒(méi)有被設(shè)置那就假設(shè)是不限制的。

2.2 持久連接

從一個(gè)主機(jī)向另外一個(gè)建立連接的過(guò)程是相當(dāng)復(fù)雜的,而且包含了兩個(gè)終端之間的很多包的交換,它是相當(dāng)費(fèi)時(shí)的。連接握手的開(kāi)銷(xiāo)是很重要的,特別是對(duì)小量的HTTP報(bào)文。如果打開(kāi)的連接可以被重用來(lái)執(zhí)行多次請(qǐng)求,那么就可以達(dá)到很高的數(shù)據(jù)吞吐量。

HTTP/1.1強(qiáng)調(diào)HTTP連接默認(rèn)情況可以被重用于多次請(qǐng)求。HTTP/1.0兼容的終端也可以使用相似的機(jī)制來(lái)明確地交流它們的偏好來(lái)保證連接處于活動(dòng)狀態(tài),也使用它來(lái)處理多個(gè)請(qǐng)求。HTTP代理也可以保持空閑連接處于一段時(shí)間的活動(dòng)狀態(tài),防止對(duì)相同目標(biāo)主機(jī)的一個(gè)連接也許對(duì)隨后的請(qǐng)求需要。保持連接活動(dòng)的能力通常被稱(chēng)作持久性連接。HttpClient完全支持持久性連接。

2.3 HTTP連接路由

HttpClient能夠直接或通過(guò)路由建立連接到目標(biāo)主機(jī),這會(huì)涉及多個(gè)中間連接,也被稱(chēng)為跳。HttpClient區(qū)分路由和普通連接,通道和分層。通道連接到目標(biāo)主機(jī)的多個(gè)中間代理的使用也稱(chēng)作是代理鏈。

普通路由由連接到目標(biāo)或僅第一次的代理來(lái)創(chuàng)建。通道路由通過(guò)代理鏈到目標(biāo)連接到第一通道來(lái)建立。沒(méi)有代理的路由不是通道的,分層路由通過(guò)已存在連接的分層協(xié)議來(lái)建立。協(xié)議僅僅可以在到目標(biāo)的通道上或在沒(méi)有代理的直接連接上分層。

2.3.1 路由計(jì)算

RouteInfo接口代表關(guān)于最終涉及一個(gè)或多個(gè)中間步驟或跳的目標(biāo)主機(jī)路由的信息。HttpRoute是RouteInfo的具體實(shí)現(xiàn),這是不能改變的(是不變的)。HttpTracker是可變的RouteInfo實(shí)現(xiàn),由HttpClient在內(nèi)部使用來(lái)跟蹤到最大路由目標(biāo)的剩余跳數(shù)。HttpTracker可以在成功執(zhí)行向路由目標(biāo)的下一跳之后更新。HttpRouteDirector是一個(gè)幫助類(lèi),可以用來(lái)計(jì)算路由中的下一跳。這個(gè)類(lèi)由HttpClient在內(nèi)部使用。

HttpRoutePlanner是一個(gè)代表計(jì)算到基于執(zhí)行上下文到給定目標(biāo)完整路由策略的接口。HttpClient附帶兩個(gè)默認(rèn)的HttpRoutePlanner實(shí)現(xiàn)。ProxySelectorRoutePlanner是基于java.net.ProxySelector的。默認(rèn)情況下,它會(huì)從系統(tǒng)屬性中或從運(yùn)行應(yīng)用程序的瀏覽器中選取JVM的代理設(shè)置。DefaultHttpRoutePlanner實(shí)現(xiàn)既不使用任何Java系統(tǒng)屬性,也不使用系統(tǒng)或?yàn)g覽器的代理設(shè)置。它只基于HTTP如下面描述的參數(shù)計(jì)算路由。

2.3.2 安全HTTP連接

如果信息在兩個(gè)不能由非認(rèn)證的第三方進(jìn)行讀取或修改的終端之間傳輸,HTTP連接可以被認(rèn)為是安全的。SSL/TLS協(xié)議是用來(lái)保證HTTP傳輸安全使用最廣泛的技術(shù)。而其它加密技術(shù)也可以被使用。通常來(lái)說(shuō),HTTP傳輸是在SSL/TLS加密連接之上分層的。

2.4 HTTP路由參數(shù)

這些參數(shù)可以影響路由計(jì)算:

'http.route.default-proxy':定義可以被不使用JRE設(shè)置的默認(rèn)路由規(guī)劃者使用的代理主機(jī)。這個(gè)參數(shù)期望得到一個(gè)HttpHost類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么就會(huì)嘗試直接連接到目標(biāo)。

'http.route.local-address':定義一個(gè)本地地址由所有默認(rèn)路由規(guī)劃者來(lái)使用。有多個(gè)網(wǎng)絡(luò)接口的機(jī)器中,這個(gè)參數(shù)可以被用于從連接源中選擇網(wǎng)絡(luò)接口。這個(gè)參數(shù)期望得到一個(gè)java.net.InetAddress類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,將會(huì)自動(dòng)使用本地地址。

'http.route.forced-route':定義一個(gè)由所有默認(rèn)路由規(guī)劃者使用的強(qiáng)制路由。代替了計(jì)算路由,給定的強(qiáng)制路由將會(huì)被返回,盡管它指向一個(gè)完全不同的目標(biāo)主機(jī)。這個(gè)參數(shù)期望得到一個(gè)HttpRoute類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,那么就使用默認(rèn)的規(guī)則建立連接到目標(biāo)服務(wù)器。

2.5 套接字工廠(chǎng)

LayeredSocketFactory是SocketFactory接口的擴(kuò)展。分層的套接字工廠(chǎng)可HTTP連接內(nèi)部使用java.net.Socket對(duì)象來(lái)處理數(shù)據(jù)在線(xiàn)路上的傳輸。它們依賴(lài)SocketFactory接口來(lái)創(chuàng)建,初始化和連接套接字。這會(huì)使得HttpClient的用戶(hù)可以提供在運(yùn)行時(shí)指定套接字初始化代碼的應(yīng)用程序。PlainSocketFactory是創(chuàng)建和初始化普通的(不加密的)套接字的默認(rèn)工廠(chǎng)。

創(chuàng)建套接字的過(guò)程和連接到主機(jī)的過(guò)程是不成對(duì)的,所以套接字在連接操作封鎖時(shí)可以被關(guān)閉。

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

2.5.1 安全套接字分層

LayeredSocketFactory是SocketFactory接口的擴(kuò)展。分層的套接字工廠(chǎng)可以創(chuàng)建在已經(jīng)存在的普通套接字之上的分層套接字。套接字分層主要通過(guò)代理來(lái)創(chuàng)建安全的套接字。HttpClient附帶實(shí)現(xiàn)了SSL/TLS分層的SSLSocketFactory。請(qǐng)注意HttpClient不使用任何自定義加密功能。它完全依賴(lài)于標(biāo)準(zhǔn)的Java密碼學(xué)(JCE)和安全套接字(JSEE)擴(kuò)展。

2.5.2 SSL/TLS的定制

HttpClient使用SSLSocketFactory來(lái)創(chuàng)建SSL連接。SSLSocketFactory允許高度定制。它可以使用javax.net.ssl.SSLContext的實(shí)例作為參數(shù),并使用它來(lái)創(chuàng)建定制SSL連接。

TrustManager easyTrustManager = new X509TrustManager() {

@Override

public void checkClientTrusted(X509Certificate[] chain,

String authType) throws CertificateException {

// 哦,這很簡(jiǎn)單!

}

@Override

public void checkServerTrusted(X509Certificate[] chain,

String authType) throws CertificateException {

//哦,這很簡(jiǎn)單!

}

@Override

public 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);

SSLSocketFactory的定制暗示出一定程度SSL/TLS協(xié)議概念的熟悉,這個(gè)詳細(xì)的解釋超出了本文檔的范圍。請(qǐng)參考Java的安全套接字?jǐn)U展[http://java.sun.com/j2se/1.5.0/docs/guide/

security/jsse/JSSERefGuide.html],這是javax.net.ssl.SSLContext和相關(guān)工具的詳細(xì)描述。

2.5.3 主機(jī)名驗(yàn)證

除了信任驗(yàn)證和客戶(hù)端認(rèn)證在SSL/TLS協(xié)議級(jí)上進(jìn)行,一旦連接建立之后,HttpClient能可選地驗(yàn)證目標(biāo)主機(jī)名匹配存儲(chǔ)在服務(wù)器的X.509認(rèn)證中的名字。這個(gè)認(rèn)證可以提供額外的服務(wù)器信任材料的真實(shí)保證。X509主機(jī)名驗(yàn)證接口代表了主機(jī)名驗(yàn)證的策略。HttpClient附帶了3個(gè)X509主機(jī)名驗(yàn)證器。很重要的一點(diǎn)是:主機(jī)名驗(yàn)證不應(yīng)該混淆SSL信任驗(yàn)證。

StrictHostnameVerifier:嚴(yán)格的主機(jī)名驗(yàn)證在Sun Java 1.4,Sun Java 5和Sun Java 6中是相同的。而且也非常接近IE6。這個(gè)實(shí)現(xiàn)似乎是兼容RFC 2818處理通配符的。主機(jī)名必須匹配第一個(gè)CN或任意的subject-alt。在CN和其它任意的subject-alt中可能會(huì)出現(xiàn)通配符。

BrowserCompatHostnameVerifier:主機(jī)名驗(yàn)證器和Curl和Firefox的工作方式是相同的。主機(jī)名必須匹配第一個(gè)CN或任意的subject-alt。在CN和其它任意的subject-alt中可能會(huì)出現(xiàn)通配符。BrowserCompatHostnameVerifier和StrictHostnameVerifier的唯一不同是使用BrowserCompatHostnameVerifier匹配所有子域的通配符(比如”*.foo.com”),包括”a.b.foo.com”。

AllowAllHostnameVerifier:這個(gè)主機(jī)名驗(yàn)證器基本上是關(guān)閉主機(jī)名驗(yàn)證的。這個(gè)實(shí)現(xiàn)是一個(gè)空操作,而且不會(huì)拋出javax.net.ssl.SSLException異常。

每一個(gè)默認(rèn)的HttpClient使用BrowserCompatHostnameVerifier的實(shí)現(xiàn)。如果需要的話(huà),它可以指定不同的主機(jī)名驗(yàn)證器實(shí)現(xiàn)。

SSLSocketFactory sf = new SSLSocketFactory(SSLContext.getInstance("TLS"));

sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

2.6 協(xié)議模式

Scheme類(lèi)代表了一個(gè)協(xié)議模式,比如“http”或“https”同時(shí)包含一些協(xié)議屬性,比如默認(rèn)端口,用來(lái)為給定協(xié)議創(chuàng)建java.net.Socket實(shí)例的套接字工廠(chǎng)。SchemeRegistry類(lèi)用來(lái)維持一組Scheme,當(dāng)去通過(guò)請(qǐng)求URI建立連接時(shí),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);

2.7 HttpClient代理配置

盡管HttpClient了解復(fù)雜的路由模式和代理鏈,它僅支持簡(jiǎn)單直接的或開(kāi)箱的跳式代理連接。

告訴HttpClient通過(guò)代理去連接到目標(biāo)主機(jī)的最簡(jiǎn)單方式是通過(guò)設(shè)置默認(rèn)的代理參數(shù):

DefaultHttpClient httpclient = new DefaultHttpClient();

HttpHost proxy = new HttpHost("someproxy", 8080);

httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);

也可以構(gòu)建HttpClient使用標(biāo)準(zhǔn)的JRE代理選擇器來(lái)獲得代理信息:

DefaultHttpClient httpclient = new DefaultHttpClient();

ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(

httpclient.getConnectionManager().getSchemeRegistry(),

ProxySelector.getDefault());

httpclient.setRoutePlanner(routePlanner);

另外一種選擇,可以提供一個(gè)定制的RoutePlanner實(shí)現(xiàn)來(lái)獲得HTTP路由計(jì)算處理上的復(fù)雜的控制:

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

}

});

2.8 HTTP連接管理器

2.8.1 連接操作器

連接操作是客戶(hù)端的低層套接字或可以通過(guò)外部實(shí)體,通常稱(chēng)為連接操作的被操作的狀態(tài)的連接。OperatedClientConnection接口擴(kuò)展了HttpClientConnection接口而且定義了額外的控制連接套接字的方法。ClientConnectionOperator接口代表了創(chuàng)建實(shí)例和更新那些對(duì)象低層套接字的策略。實(shí)現(xiàn)類(lèi)最有可能利用SocketFactory來(lái)創(chuàng)建java.net.Socket實(shí)例。ClientConnectionOperator接口可以讓HttpClient的用戶(hù)提供一個(gè)連接操作的定制策略和提供可選實(shí)現(xiàn)OperatedClientConnection接口的能力。

2.8.2 管理連接和連接管理器

HTTP連接是復(fù)雜的,有狀態(tài)的,線(xiàn)程不安全的對(duì)象需要正確的管理以便正確地執(zhí)行功能。HTTP連接在同一時(shí)間僅僅只能由一個(gè)執(zhí)行線(xiàn)程來(lái)使用。HttpClient采用一個(gè)特殊實(shí)體來(lái)管理訪(fǎng)問(wèn)HTTP連接,這被稱(chēng)為HTTP連接管理器,代表了ClientConnectionManager接口。一個(gè)HTTP連接管理器的目的是作為工廠(chǎng)服務(wù)于新的HTTP連接,管理持久連接和同步訪(fǎng)問(wèn)持久連接來(lái)確保同一時(shí)間僅有一個(gè)線(xiàn)程可以訪(fǎng)問(wèn)一個(gè)連接。

內(nèi)部的HTTP連接管理器和OperatedClientConnection實(shí)例一起工作,但是它們?yōu)榉?wù)消耗器ManagedClientConnection提供實(shí)例。ManagedClientConnection扮演連接之上管理狀態(tài)控制所有I/O操作的OperatedClientConnection實(shí)例的包裝器。它也抽象套接字操作,提供打開(kāi)和更新去創(chuàng)建路由套接字便利的方法。ManagedClientConnection實(shí)例了解產(chǎn)生它們到連接管理器的鏈接,而且基于這個(gè)事實(shí),當(dāng)不再被使用時(shí),它們必須返回到管理器。ManagedClientConnection類(lèi)也實(shí)現(xiàn)了ConnectionReleaseTrigger接口,可以被用來(lái)觸發(fā)釋放連接返回給管理器。一旦釋放連接操作被觸發(fā)了,被包裝的連接從ManagedClientConnection包裝器中脫離,OperatedClientConnection實(shí)例被返回給管理器。盡管服務(wù)消耗器仍然持有ManagedClientConnection實(shí)例的引用,它也不再去執(zhí)行任何I/O操作或有意無(wú)意地改變的OperatedClientConnection狀態(tài)。

這里有一個(gè)從連接管理器中獲取連接的示例:

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

// 請(qǐng)求新連接。這可能是一個(gè)很長(zhǎng)的過(guò)程。

ClientConnectionRequest connRequest = connMrg.requestConnection(

new HttpRoute(new HttpHost("localhost", 80)), null);

// 等待連接10秒

ManagedClientConnection conn = connRequest.getConnection(10, TimeUnit.SECONDS);

try {

// 用連接在做有用的事情。當(dāng)完成時(shí)釋放連接。

conn.releaseConnection();

} catch (IOException ex) {

// 在I/O error之上終止連接。

conn.abortConnection();

throw ex;

}

如果需要,連接請(qǐng)求可以通過(guò)調(diào)用來(lái)ClientConnectionRequest#abortRequest()方法過(guò)早地中斷。這會(huì)解鎖在ClientConnectionRequest#getConnection()方法中被阻止的線(xiàn)程。

一旦響應(yīng)內(nèi)容被完全消耗后,BasicManagedEntity包裝器類(lèi)可以用來(lái)保證自動(dòng)釋放低層的連接。HttpClient內(nèi)部使用這個(gè)機(jī)制來(lái)實(shí)現(xiàn)透明地對(duì)所有從HttpClient#execute()方法中獲得響應(yīng)釋放連接:

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

// 替換實(shí)體

response.setEntity(managedEntity);

}

// 使用響應(yīng)對(duì)象做有用的事情。當(dāng)響應(yīng)內(nèi)容被消耗后這個(gè)連接將會(huì)自動(dòng)釋放。

} catch (IOException ex) {

//在I/O error之上終止連接。

conn.abortConnection();

throw ex;

}

2.8.3 簡(jiǎn)單連接管理器

SingleClientConnManager是一個(gè)簡(jiǎn)單的連接管理器,在同一時(shí)間它僅僅維護(hù)一個(gè)連接。盡管這個(gè)類(lèi)是線(xiàn)程安全的,但它應(yīng)該被用于一個(gè)執(zhí)行線(xiàn)程。SingleClientConnManager對(duì)于同一路由的后續(xù)請(qǐng)求會(huì)盡量重用連接。而如果持久連接的路由不匹配連接請(qǐng)求的話(huà),它也會(huì)關(guān)閉存在的連接之后對(duì)給定路由再打開(kāi)一個(gè)新的。如果連接已經(jīng)被分配,將會(huì)拋出java.lang.IllegalStateException異常。

對(duì)于每個(gè)默認(rèn)連接,HttpClient使用SingleClientConnManager。

2.8.4 連接池管理器

ThreadSafeClientConnManager是一個(gè)復(fù)雜的實(shí)現(xiàn)來(lái)管理客戶(hù)端連接池,它也可以從多個(gè)執(zhí)行線(xiàn)程中服務(wù)連接請(qǐng)求。對(duì)每個(gè)基本的路由,連接都是池管理的。對(duì)于路由的請(qǐng)求,管理器在池中有可用的持久性連接,將被從池中租賃連接服務(wù),而不是創(chuàng)建一個(gè)新的連接。

ThreadSafeClientConnManager維護(hù)每個(gè)基本路由的最大連接限制。每個(gè)默認(rèn)的實(shí)現(xiàn)對(duì)每個(gè)給定路由將會(huì)創(chuàng)建不超過(guò)兩個(gè)的并發(fā)連接,而總共也不會(huì)超過(guò)20個(gè)連接。對(duì)于很多真實(shí)的應(yīng)用程序,這個(gè)限制也證明很大的制約,特別是他們?cè)诜?wù)中使用HTTP作為傳輸協(xié)議。連接限制,也可以使用HTTP參數(shù)來(lái)進(jìn)行調(diào)整。

這個(gè)示例展示了連接池參數(shù)是如何來(lái)調(diào)整的:

HttpParams params = new BasicHttpParams();

// 增加最大連接到200

ConnManagerParams.setMaxTotalConnections(params, 200);

// 增加每個(gè)路由的默認(rèn)最大連接到20

ConnPerRouteBean connPerRoute = new ConnPerRouteBean(20);

// 對(duì)localhost:80增加最大連接到50

HttpHost 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);

2.8.5 連接管理器關(guān)閉

當(dāng)一個(gè)HttpClient實(shí)例不再需要時(shí),而且即將走出使用范圍,那么關(guān)閉連接管理器來(lái)保證由管理器保持活動(dòng)的所有連接被關(guān)閉,由連接分配的系統(tǒng)資源被釋放是很重要的。

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

2.9 連接管理參數(shù)

這些是可以用于定制標(biāo)準(zhǔn)HTTP連接管理器實(shí)現(xiàn)的參數(shù):

'http.conn-manager.timeout':定義了當(dāng)從ClientConnectionManager中檢索ManagedClientConnection實(shí)例時(shí)使用的毫秒級(jí)的超時(shí)時(shí)間。這個(gè)參數(shù)期望得到一個(gè)java.lang.Long類(lèi)型的值。如果這個(gè)參數(shù)沒(méi)有被設(shè)置,連接請(qǐng)求就不會(huì)超時(shí)(無(wú)限大的超時(shí)時(shí)間)。

'http.conn-manager.max-per-route':定義了每個(gè)路由連接的最大數(shù)量。這個(gè)限制由客戶(hù)端連接管理器來(lái)解釋?zhuān)覒?yīng)用于獨(dú)立的管理器實(shí)例。這個(gè)參數(shù)期望得到一個(gè)ConnPerRoute類(lèi)型的值。

'http.conn-manager.max-total':定義了總共連接的最大數(shù)目。這個(gè)限制由客戶(hù)端連接管理器來(lái)解釋?zhuān)覒?yīng)用于獨(dú)立的管理器實(shí)例。這個(gè)參數(shù)期望得到一個(gè)java.lang.Integer類(lèi)型的值。

2.10 多線(xiàn)程執(zhí)行請(qǐng)求

當(dāng)配備連接池管理器時(shí),比如ThreadSafeClientConnManager,HttpClient可以同時(shí)被用來(lái)執(zhí)行多個(gè)請(qǐng)求,使用多線(xiàn)程執(zhí)行。

ThreadSafeClientConnManager將會(huì)分配基于它的配置的連接。如果對(duì)于給定路由的所有連接都被租出了,那么連接的請(qǐng)求將會(huì)阻塞,直到一個(gè)連接被釋放回連接池。它可以通過(guò)設(shè)置'http.conn-manager.timeout'為一個(gè)正數(shù)來(lái)保證連接管理器不會(huì)在連接請(qǐng)求執(zhí)行時(shí)無(wú)限期的被阻塞。如果連接請(qǐng)求不能在給定的時(shí)間周期內(nèi)被響應(yīng),將會(huì)拋出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);

// 執(zhí)行GET方法的URI

String[] urisToGet = {

"http://www.domain1.com/",

"http://www.domain2.com/",

"http://www.domain3.com/",

"http://www.domain4.com/"

};

// 為每個(gè)URI創(chuàng)建一個(gè)線(xiàn)程

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

}

// 開(kāi)始執(zhí)行線(xiàn)程

for (int j = 0; j < threads.length; j++) {

threads[j].start();

}

// 合并線(xiàn)程

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;

}

@Override

public void run() {

try {

HttpResponse response = this.httpClient.execute(this.httpget, this.context);

HttpEntity entity = response.getEntity();

if (entity != null) {

// 對(duì)實(shí)體做些有用的事情...

// 保證連接能釋放回管理器

entity.consumeContent();

}

} catch (Exception ex) {

this.httpget.abort();

}

}

}

2.11 連接收回策略

一個(gè)經(jīng)典的阻塞I/O模型的主要缺點(diǎn)是網(wǎng)絡(luò)套接字僅當(dāng)I/O操作阻塞時(shí)才可以響應(yīng)I/O事件。當(dāng)一個(gè)連接被釋放返回管理器時(shí),它可以被保持活動(dòng)狀態(tài)而卻不能監(jiān)控套接字的狀態(tài)和響應(yīng)任何I/O事件。如果連接在服務(wù)器端關(guān)閉,那么客戶(hù)端連接也不能去偵測(cè)連接狀態(tài)中的變化和關(guān)閉本端的套接字去作出適當(dāng)響應(yīng)。

HttpClient通過(guò)測(cè)試連接是否是過(guò)時(shí)的來(lái)嘗試去減輕這個(gè)問(wèn)題,這已經(jīng)不再有效了,因?yàn)樗呀?jīng)在服務(wù)器端關(guān)閉了,之前使用執(zhí)行HTTP請(qǐng)求的連接。過(guò)時(shí)的連接檢查也并不是100%的穩(wěn)定,反而對(duì)每次請(qǐng)求執(zhí)行還要增加10到30毫秒的開(kāi)銷(xiāo)。唯一可行的而不涉及到每個(gè)對(duì)空閑連接的套接字模型線(xiàn)程解決方案,是使用專(zhuān)用的監(jiān)控線(xiàn)程來(lái)收回因?yàn)殚L(zhǎng)時(shí)間不活動(dòng)而被認(rèn)為是過(guò)期的連接。監(jiān)控線(xiàn)程可以周期地調(diào)用ClientConnectionManager#closeExpiredConnections()方法來(lái)關(guān)閉所有過(guò)期的連接,從連接池中收回關(guān)閉的連接。它也可以選擇性調(diào)用ClientConnectionManager#closeIdleConnections()方法來(lái)關(guān)閉所有已經(jīng)空閑超過(guò)給定時(shí)間周期的連接。

public static class IdleConnectionMonitorThread extends Thread {

private final ClientConnectionManager connMgr;

private volatile boolean shutdown;

public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {

super();

this.connMgr = connMgr;

}

@Override

public void run() {

try {

while (!shutdown) {

synchronized (this) {

wait(5000);

// 關(guān)閉過(guò)期連接

connMgr.closeExpiredConnections();

// 可選地,關(guān)閉空閑超過(guò)30秒的連接

connMgr.closeIdleConnections(30, TimeUnit.SECONDS);

}

}

} catch (InterruptedException ex) {

// 終止

}

}

public void shutdown() {

shutdown = true;

synchronized (this) {

notifyAll();

}

}

}

2.12 連接保持活動(dòng)的策略

HTTP規(guī)范沒(méi)有確定一個(gè)持久連接可能或應(yīng)該保持活動(dòng)多長(zhǎng)時(shí)間。一些HTTP服務(wù)器使用非標(biāo)準(zhǔn)的頭部信息Keep-Alive來(lái)告訴客戶(hù)端它們想在服務(wù)器端保持連接活動(dòng)的周期秒數(shù)。如果這個(gè)信息可用,HttClient就會(huì)利用這個(gè)它。如果頭部信息Keep-Alive在響應(yīng)中不存在,HttpClient假設(shè)連接無(wú)限期的保持活動(dòng)。然而許多現(xiàn)實(shí)中的HTTP服務(wù)器配置了在特定不活動(dòng)周期之后丟掉持久連接來(lái)保存系統(tǒng)資源,往往這是不通知客戶(hù)端的。如果默認(rèn)的策略證明是過(guò)于樂(lè)觀(guān)的,那么就會(huì)有人想提供一個(gè)定制的保持活動(dòng)策略。

DefaultHttpClient httpclient = new DefaultHttpClient();

httpclient.setKeepAliveStrategy(new ConnectionKeepAliveStrategy() {

public long getKeepAliveDuration(HttpResponse response, HttpContext context) {

// 兌現(xiàn)'keep-alive'頭部信息

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(

ExecutionContext.HTTP_TARGET_HOST);

if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {

// 只保持活動(dòng)5秒

return 5 * 1000;

} else {

// 否則保持活動(dòng)30秒

return 30 * 1000;

}

}

});

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容