HttpClient4.5教程-第二章-連線管理
2.1 連線持久化
在兩個主機之間建立連線的過程複雜並且可能相當耗時,這一過程涉及到多個數據包交換,,連線(特別是短連線)握手的開銷會非常的大,我們可以通過多個request重用HTTP 連線來達到高吞吐資料量避免這一問題。
HTTP/1.1 預設HTTP連線可以被多個請求重用。HTTP/1.0標準的終端可以使用某些機制去顯示的表達他們要想重用連線的意圖。HTTP代理也能夠保持一段時間內的空閒連線從而提供給同一主機的連續請求使用。保持連線的能力通常叫做連線持久化,HttpClient對連線持久化提供了全面的支援。
2.2 HTTP 路由
HttpClient可以直接跟目標主機建立連線,也可以通過路由來建立多個連線--我們稱之為“跳”。HttpClient將連線劃分為plain(直連),tunneled(隧道),layered(分層連線),多箇中間代理的隧道連線被稱之為代理鏈。
直接到目標或者第一個並且是唯一一個代理的連線稱之為直連,與目標之間的多箇中間代理建立的連線稱之為隧道路由,隧道路由不能脫離代理而存在,通過已有連線的分層協議的路由則稱之為分層路由,分層協議只能基於隧道或者直連。
2.2.1 路由計算
RouteInfo介面代表了到目標主機的一系列明確的路由資訊,HttpRoute是RouteInfo的一個具體實現,該類是不可修改的。
HttpTracker是一個可變的RouteInfo的實現,用於HttpClient內部來追蹤到最終目標主機的剩餘路由,HttpTracker可以在進行完一個成功的路由之後被更新。
HttpRouteDirector則是用於計算到下一步路由的幫助類,該類也是在HttpClient內部使用。
HttpRoutePlanner基於執行上下文來計算到目標主機的路由策略,HttpClient附帶兩個預設的HttpRoutePlanner實現,
SystemDefaultRoutePlanner基於java.net.ProxySelector,它預設從JVM,系統屬性或者瀏覽器中提取代理配置。
DefaultProxyRoutePlanner總是通過預設的策略來計算路由。
2.2.2 HTTP安全連線
如果兩個主機之間傳遞的資訊不能被未授權的第三方讀取或者篡改,那麼這個連線就認為是安全的,SSL/TLS是使用最廣泛的HTTP安全傳輸協議,雖然其他加密技術也可以很好的實現安全傳輸,但是HTTP傳輸則普遍使用SSL/TLS。
2.3 HTTP 連線管理器
2.3.1 連線管理與連線管理器
HTTP連線是複雜的,無狀態的,非執行緒安全的,需要妥善的管理以保證正確工作,一個HTTP連線同一時間只能被一個執行緒執行,HttpClient使用HttpClientConnectionManager介面作為連線管理器去管理HTTP連線,HTTP連線管理器的目標是
1 成為建立HTTP新連線的工廠
2 管理持久化連線的生命週期
3 同步持久化連線的訪問,保證同一時間只有一個執行緒可以訪問同一個連線。
內部的HTTP連線管理器與ManagedHttpClientConnection例項協同工作,能夠實現代理連線,管理連線狀態,控制執行I/O操作。如果一個連線被釋放或者被消費端明確的關閉了,底層連線將會從其代理分離出來交還連線管理器,即便是消費端仍然持有代理例項的引用,它也沒辦法做任何I/O操作或者是改變真實連線的狀態。
下面是從連線管理器獲取HTTP連線的一個例子:
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#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 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();
2.3.4 連線池關閉
當HttpClient例項不再使用並且將要離開作用域時,需要關閉連線管理器,確保所有的連線都被關閉,系統資源被正確釋放,這一點非常重要。
CloseableHttpClient httpClient = <...>
httpClient.close();
2.4 多執行緒request執行
當使用如PoolingClientConnectionManager的連線池管理器時,HttpClient可以通過多個執行執行緒同時執行多個請求。
PoolingClientConnectionManager將會根據其配置分配連線,如果某個路由的所有連線都已經被分配出去了,新進來的請求將會阻塞直到某個連線被釋放回連線池,你可以通過配置http.conn-manager.timeout 這個引數來配置新的請求進來時阻塞的超時時間,從而避免無限期等待,如果在給定時間內連線沒有被獲取到,那麼將會丟擲ConnectionPoolTimeoutException異常。
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
// URIs to perform GETs on
String[] urisToGet = {
"http://www.domain1.com/",
"http://www.domain2.com/",
"http://www.domain3.com/",
"http://www.domain4.com/"
};
// create a thread for each 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();
}
// join the threads
for (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模型的一個主要缺點就是網路socket只有在I/O操作阻塞的情況下才會對I/O事件作出反應。當連線釋放回管理器時,它雖然能夠保持存活,但是它無法監控socket的狀態也無法對任何I/O事件作出反應。如果連線在Server端被關閉,client端連線無法偵測到連線狀態的改變並且作出適當的迴應。
HttpClient嘗試通過測試連線是否'stale'來緩解這個問題,stale的意思是連線不再有效因為在執行Http請求之前其已經被服務端關閉。過期連線檢查不是100%可靠的,唯一可行解決方案是提供一個專用監控執行緒用於回收那些長時間內不活動的連線,而該解決方案不會影響到一個socket一個執行緒的空閒連線模型。監控執行緒可以定期呼叫 ClientConnectionManager#closeExpiredConnections()方法來關閉所有過期的連線,同時從連線池中回收已經被關閉的連線,也可以有選擇性的呼叫 ClientConnectionManager#closeIdleConnection()方法去關閉那些在給定時間範圍內空閒的連線。
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-Aliveheader去同客戶端溝通他們想要在server端保持多久的連線,HttpClient也會利用這個資訊,如果Keep-Alive訊息頭在response中不存在,HttpClient假設連線可以被無限期的保持存活,但是很多HTTP服務端通常配置為在持久化連線一段時間內不活動時就丟棄以節省系統資源,而丟棄時往往不通知客戶端,在這種情況下,預設的策略會變得過於樂觀,你可能想要提供一個自定義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.7 連線套接字工廠
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("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 安全套接字層
LayeredConnectionSocketFactory是ConnectionSocketFactory介面的一個擴充套件,分層套接字工廠能夠在已存在的普通socket上建立分層socket,Socket分層主要用於通過代理建立安全socket,HttpClient附帶的SSLSocketFactory實現了SSL/TLS層協議,請注意HttpClient不會使用任何定製的加密功能,它完全依賴JCE(標準JAVA加密)和JSEE(安全套接字)擴充套件。
2.7.2 同連線管理器整合
自定義連線套接字工廠可以跟特定的協議(如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()
.loadTrustMaterial(myTrustStore)
.build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
定製SSLConnectionSocketFactory意味著對SSL/TLS協議有一定程度的瞭解,其詳細說明已經超出本文件的範圍,請檢視http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html
來獲取javax.net.ssl.SSLContext和其相關工具的詳細說明。
2.7.4 主機名驗證
除了信任驗證和客戶端身份驗證在SSL/TLS協議層進行之外,HttpClient可以有選擇的驗證目標主機名是否跟服務端儲存在X.509認證裡的一致,一旦連線已經建立,這種驗證可以為伺服器認證提供額外的保障,javax.net.ssl.HostnameVerifier 介面代表了主機名驗證的一種策略,HttpClient附帶了兩中javax.net.ssl.HostnameVerifier的實現,注意:不要把主機名驗證跟SSL信任驗證混淆
DefaultHostnameVerifier: HttpClient使用的預設實現,與RFC2818相容,主機名必須匹配證書指定的任何可替換的名稱,或者沒有可替換名稱下證書主體中指定的具體的CN,CN和可替換名稱中都可能有萬用字元。
NoopHostnameVerifier: 這個主機名驗證器基本上就是把主機名驗證關閉了,它接受任何有效的SSL會話來匹配目標主機。
預設HttpClient使用DefaultHostnameVerifier實現,如果有需要的話你可以指定一個不同的主機名驗證器
SSLContext sslContext = SSLContexts.createSystemDefault();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE);
HttpClient4.4使用Mozilla基金會維護的公共字尾列表去確保SSL證書的萬用字元不會被多個通用頂級域名誤用,HttpClient會附帶一個該列表的最新的拷貝,最新的修正版在https://publicsuffix.org/list/,強烈建議從源資料每天更新一次並且保持一份本地拷貝。
PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.load(
PublicSuffixMatcher.class.getResource("my-copy-effective_tld_names.dat"));
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);
你可以通過使用null匹配來關閉公共字尾列表驗證
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(null);
2.8 HttpClient 代理配置
儘管HttpClient已經有複雜路由計算和代理量,他也只支援簡單的直連和單跳代理連線。
讓HttpClient通過代理連線目標主機最簡單的方式是配置預設的代理引數:
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();
}
}