第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 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是一个更复杂的实现。它管理一个client连接池。并为线程的连接请求提供服务。连接都被汇集在每一个路由基础上。对于一个路由请求,假设管理器在池里已有一个可用的持续连接,则不会创建一个新的,而是租用池里的这个连接。PoolingHttpClientConnectionManager维护着在每一个路由基础上连接数目的上限。每一个默认的实现不会创建超过2个并行连接,每一个指定的路由总共不会超过20个连接。对于很多现实的应用来说,这些限制可能会过于约束。尤其是当他们为他们的server使用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 线程请求运行
当配备一个池连接管理器后,如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堵塞模式有一个基本的缺点。就是当I/O操作被堵塞时,网络socket仅仅对I/O事件影响。当一个连接被释放回管理器,它会保持活跃。然而它不会监听socket的状态和不论什么I/O事件。假设这个连接在server端被关闭,client的连接在连接状态(和因为结束时正在关闭而作出的适当响应)下不会检測出这个改变。
HttpClient通过測试连接是否为“陈腐的”而尝试去缓解这个问题,“陈腐的”是指不再是有效的。由于它会被server端关闭掉,并会在这之前为了正运行中的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规范没有明白指定一个持续连接最多能够保持活跃有多久。一些HTTPserver会使用一个非标准的Keep-Alive(保持活跃)标头来告诉client,他们计划在server端保持连接活跃的时间(以秒为单位)。假设能够获得的话。HttpClient就会使用这些信息。假设Keep-Alive标头没有出如今应答里,HttpClient会假定这个连接能够无限期地保持活跃。然而,很多HTTPserver一般会在一段不活跃时期后被配置成放弃持续连接,为了保存系统,这常常不会通知client。默认的策略似乎太过于乐观了,你可能会想提供一个自己定义保持活跃策略。
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; //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 安全的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还能选择性地检验是否目标主机名与存放在server上的X.509证书相匹配。
这个检验能够为server相信材料的可靠性提供额外的保证。
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和火狐浏览器里。主机名必需要么匹配第一个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 HttpClient代理server配置
虽然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