2.1 连接持久性
建立从一个主机到另一个主机的连接的过程相当复杂,并且涉及两个端点之间的多个分组交换,这可能相当耗时。连接握手的开销可能很大,特别是对于小型的HTTP消息。 如果可以重新使用开放连接来执行多个请求,则可以实现更高的数据吞吐量。
HTTP / 1.1规定HTTP连接可以重复用于多个请求。 符合HTTP / 1.0的端点还可以使用一种机制来显式传达它们的首选项,以保持连接的活动状态并将其用于多个请求。 HTTP代理还可以保持空闲连接在一段时间内保持活动状态,以防后续请求需要连接到同一个目标主机。 保持连接的能力通常被称为连接持久性。 HttpClient完全支持连接持久性。
2.2 HTTP连接路由
HttpClient能够直接或通过可能涉及多个中间连接(也称为中继)的路由建立到目标主机的连接。 HttpClient将路由的连接区分为普通,隧道和分层。 使用多个中间代理来隧道连接到目标主机被称为代理链接。
隧道路由是通过连接到第一个隧道并通过代理链隧道进行目标建立的。不使用代理服务器的路由不能使用隧道路由。通过在现有连接上分层协议来建立分层路由。协议只能通过隧道到目标,或通过无代理的直接连接。
2.2.1. 路由计算
RouteInfo接口表示关于到涉及一个或多个中间步骤或跳跃的目标主机的确定路由的信息。 HttpRoute是RouteInfo的具体实现,它不能被改变(是不可变的)。 HttpTracker是HttpClient内部使用的一个可变的RouteInfo实现,用于跟踪剩余的跳转到最终的路由目标。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连接一次只能由一个执行线程使用。 HttpClient使用一个特殊的实体来管理对HTTP连接的访问,这个HTTP连接被称为HTTP连接管理器,并由HttpClientConnectionManager接口表示。HTTP连接管理器的目的是作为新的HTTP连接的工厂,管理持久连接的生命周期,并同步对持久连接的访问,以确保一次只有一个线程可以访问连接。内部HTTP连接管理器与ManagedHttpClientConnection实例一起工作,作为管理连接状态和控制I/O操作执行的真实连接的代理。如果托管连接被释放或被其消费者明确关闭,则底层连接从其代理分离,并返回给管理器。即使服务消费者仍然持有对代理实例的引用,它不再有意或无意地执行任何I/O操作或改变真实连接的状态。
//创建HTTP上下文
HttpClientContext context=HttpClientContext.create();
//创建HTTP连接管理器
HttpClientConnectionManager connMrg=new BasicHttpClientConnectionManager();
//创建连接路由线路
HttpRoute route=new HttpRoute(new HttpHost("www.baidu.com",0, "http://"));
//请求一个新的Connection,这可能需要处理很长时间。
ConnectionRequest connRequest=connMrg.requestConnection(route, null);
//只等待10秒,有可能抛出InterruptedException, ExecutionException 异常
HttpClientConnection conn=connRequest.get(10, TimeUnit.SECONDS);
try {
if(conn.isOpen()) {
//根据Route info建立连接
connMrg.connect(conn, route, 1000, context);
//将其标记为路由已完成
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. 汇集连接管理器(Pooling connection manager)
PoolingHttpClientConnectionManager是一个更复杂的实现,它管理一个客户端连接池,并能够处理来自多个执行线程的连接请求。连接按照每个路线进行汇集。 对于管理器已经在池中具有持续连接的路由的请求将通过从池租用连接而不是创建全新的连接来服务。
PoolingHttpClientConnectionManager保持每个路由和总共连接的最大限制。 默认情况下,这个实现每个给定的路由创建不超过2个并发连接,总共不超过20个连接。 对于许多真实世界的应用程序来说,这些限制可能会被证明过于严格,特别是如果他们使用HTTP作为其服务的传输协议。
此示例显示连接池参数如何调整:
PoolingHttpClientConnectionManager cm=new PoolingHttpClientConnectionManager();
//设置最大连接数不超过200
cm.setMaxTotal(200);
//每个路由默认的连接数20
cm.setDefaultMaxPerRoute(20);
HttpHost locaHost=new HttpHost("localhost",80, "http://");
HttpRoute route=new HttpRoute(locaHost);
//路由最大连接数不超过50
cm.setMaxPerRoute(route, 50);
CloseableHttpClient httpclient=HttpClients.custom().setConnectionManager(cm).build();
2.3.4. 关闭连接管理器
当一个HttpClient实例不再需要并且即将离开作用域时,关闭其连接管理器以确保管理器保持活动的所有连接都关闭,并释放由这些连接分配的系统资源是非常重要的。
CloseableHttpClient httpClient = <...>
httpClient.close();
2.4. 多线程执行请求(Multithreaded request execution)
当配备PoolingClientConnectionManager等连接池管理器时,可以使用HttpClient同时使用多个执行线程执行多个请求。PoolingClientConnectionManager将根其配置分配连接。 如果给定路由的所有连接已经租用,连接请求将被阻塞,直到连接释放回池。 可以通过将 http.conn-manager.timeout 设置为正值来确保连接管理器不会在连接请求操作中无限期地阻塞。 如果连接请求在给定的时间内无法被服务,则抛出ConnectionPoolTimeoutException。
//创建PoolingHttpClientConnectionManager连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
//创建HttpClient实例
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
// 执行GET请求的URI
String[] 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();
}
// 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. 连接驱逐策略(Connection eviction policy)
经典阻塞I/O模型的主要缺点之一是网络套接字只有在I/O操作中被阻塞时才能对I/O事件做出反应。当一个连接释放回管理器时,它可以保持活动状态,但是它无法监视套接字的状态并对任何I/O事件做出反应。如果连接在服务器端被关闭,客户端连接将无法检测到连接状态的变化(并通过关闭套接字来适当地作出反应)。
HttpClient试图通过测试连接是否是“陈旧的”也就是不再有效的,因为它是在服务器端关闭的,在使用连接执行HTTP请求之前,以缓解这个问题。但是陈旧的连接检查不是100%可靠的。对于空闲连接,如果每个套接字模型不涉及一个线程的话,唯一可行解决方案是专用监视器线程,用于驱除由于长时间不活动而被认为过期的连接。监视线程可以定期调用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. 保持连接存在的策略(Connection keep alive strategy)
HTTP规范没有指定持续连接可能会保持多久,应该保持活动状态。一些HTTP服务器使用一个非标准的Keep-Alive标头来向客户端传达他们希望在服务器端保持连接的时间段(以秒为单位)。如果可用的话,HttpClient使用这个信息。如果响应中不存在Keep-Alive头,则HttpClient假定连接可以无限期地保持活动状态。 但是,通常使用的许多HTTP服务器被配置为在一段时间不活动之后丢弃持久连接,以节省系统资源,而通常不通知客户端。如果默认策略过于乐观,则可能需要提供自定义保活策略。
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. 连接套接字工厂(Connection socket factories)
HTTP连接在内部使用java.net.Socket对象来处理通过线路传输的数据。 但是他们依靠ConnectionSocketFactory接口来创建,初始化和连接套接字。 这使得HttpClient的用户可以在运行时提供特定于应用程序的套接字初始化代码。 PlainConnectionSocketFactory是创建和初始化普通(未加密)套接字的默认工厂。
创建套接字并将其连接到主机的过程是分离的,以便在连接操作中阻塞套接字。
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. SSL(Secure Sockets Layer 安全套接层)
LayeredConnectionSocketFactory是ConnectionSocketFactory接口的扩展。 分层的套接字工厂能够在现有的普通套接字上创建套接字。 套接字分层主要用于通过代理创建安全套接字。 HttpClient附带实现SSL / TLS分层的SSLSocketFactory。 请注意HttpClient不使用任何自定义加密功能。 它完全依赖于标准的Java加密(JCE)和安全套接字(JSEE)扩展。
2.7.2. 集成到连接管理器中(Integration with connection manager)
自定义连接套接字工厂可以与特定协议方案(如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定制(SSL/TLS customization)
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协议的概念有一定程度的熟悉,详细的解释不在本文的范围之内。 有关javax.net.ssl.SSLContext和相关工具的详细说明,请参阅Java™安全套接字扩展(JSSE)参考指南。
2.7.4. 主机名验证(Hostname verification)
除了在SSL / TLS协议级别上执行信任验证和客户端身份验证之外,HttpClient还可以选择验证目标主机名是否与存储在服务器X.509证书内的名称匹配。 该验证可以提供对服务器信任材料的真实性的附加保证。javax.net.ssl.HostnameVerifier接口表示主机名验证策略。HttpClient提供了两个javax.net.ssl.HostnameVerifier实现。 重要提示:主机名验证不应与SSL信任验证混淆。
- DefaultHostnameVerifier:HttpClient使用的默认实现符合RFC 2818的要求。主机名必须与证书指定的其他名称匹配,或者在没有其他名称给定证书主体的最具体CN的情况下。 通配符可以出现在CN和任何主题中。
- NoopHostnameVerifier:这个主机名验证者本质上关闭主机名验证。 它接受任何有效的SSL会话并匹配目标主机。
默认情况下,HttpClient使用DefaultHostnameVerifier实现。如果需要,可以指定一个不同的主机名验证器实现
SSLContext sslContext = SSLContexts.createSystemDefault();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE);
从版本4.4开始,HttpClient使用由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);
通过使用空匹配器,可以禁止对公众足迹进行验证。
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();
}
}