您的位置:首页 > 理论基础 > 计算机网络

HTTPClient4.5.2学习笔记(二):连接管理(Connection management)

2017-05-11 17:09 393 查看
其实在第一篇基础篇的时候在介绍entity的时候就已经,在说三种类型区分的时候就已经提到过连接管理的概念,但是就我个人而言其实对于HTTPClient的连接管理不是很清楚。

2.1. 连接的持久性(Connection persistence)

建立从一个主机到另一个主机的连接的过程是相当复杂的,并且涉及两个端点之间的多个分组交换,这可能是相当耗时的。连接握手的开销可能很大,特别是对于小型HTTP消息。如果可以重新使用开放式连接来执行多个请求,则可以实现更高的数据吞吐量。

HTTP / 1.1表示每个默认情况下HTTP连接可以重复使用多个请求。符合HTTP / 1.0标准的端点也可以使用一种机制来明确地传达其优先级,以保持连接的活跃性并将其用于多个请求。HTTP代理还可以在一段时间内保持空闲连接的活动,以防后续请求需要连接到同一目标主机。保持连接活动的能力通常被称为连接持久性。HttpClient完全支持连接持久性。

2.2. HTTP的连接路由( connection routing)

HttpClient能够直接或经由可能涉及多个中间连接(也称为跳)的路由建立与目标主机的连接。HttpClient将路由的连接区分为普通,隧道和分层。使用多个中间代理将连接隧道传送到目标主机称为代理链接。

普通路由是直接与目标主机建立连接或者仅仅和第一个代理主机建立连接。

隧道路由是通过连接到第一个隧道并通过代理链隧道进行目标建立的。不使用代理服务器的路由不能使用隧道路由。

通过在现有连接上分层协议来建立分层路由。协议只能通过隧道到目标,或通过无代理的直接连接。(这种路由方式不是很明白)

2.2.1. 路由计算

 
RouteInfo接口
表示关于涉及一个或多个中间步骤或跳的目标主机的确定路由的信息。

HttpRoute是RouteInfo的具体实现,不能更改(不可变)。

HttpTracker是一个可变的RouteInfo实现,由HttpClient内部使用,用于跟踪终端路由目标的剩余跳数。HttpTracker可以在成功执行路由目标的下一跳后进行更新。HttpRouteDirector是一个帮助类,可用于计算路由中的下一步。这个类由HttpClient内部使用。

HttpRoutePlanner是一个表示基于执行上下文计算到给定目标的完整路由的策略的接口。HttpClient附带两个默认的HttpRoutePlanner实现

SystemDefaultRoutePlanner基于java.net.ProxySelector。默认情况下,它将从系统属性或运行应用程序的浏览器中选取JVM的代理设置。

DefaultProxyRoutePlanner实现不会使用任何Java系统属性,也不使用任何系统或浏览器代理设置。它总是通过相同的默认代理计算路由。

2.2.2. HTTPS

如果在两个连接端点之间传输的信息无法被未授权的第三方读取或篡改,则HTTP连接可以被认为是安全的。SSL / TLS协议是确保HTTP传输安全性最广泛使用的技术。然而,也可以采用其他加密技术。
通常,HTTP传输是通过SSL / TLS加密连接分层的。

2.3. HTTP连接管理器( connection managers)

2.3.1. 管理连接和连接管理器

HTTP连接是复杂,有状态,线程不安全的对象,需要正确管理才能正常工作。HTTP连接一次只能由一个执行线程使用。HttpClient采用一个特殊实体来管理被称为HTTP连接管理器的HTTP连接,并由HttpClientConnectionManager接口表示。HTTP连接管理器的目的是用作新的HTTP连接的工厂,以管理持久连接的生命周期并同步对持久连接的访问,确保只有一个线程可以一次访问连接。内部HTTP连接管理器与ManagedHttpClientConnection的实例一起工作,作为管理连接状态并控制I
/ O操作执行的实际连接的代理。如果一个http连接被释放或者consumer关闭了,底层的connection 将与 proxy 分离,重新交给 manager.即使服务使用者仍然持有对代理实例的引用,但是它不再能够执行任何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. 简单连接管理器( connection manager)

BasicHttpClientConnectionManager是一个简单的连接管理器,一次只维护一个连接。即使这个类是线程安全的,它只能被一个执行线程使用。BasicHttpClientConnectionManager将努力重新使用具有相同路由的后续请求的连接。但是,如果持久连接的路由与连接请求的路由不匹配,则会关闭现有连接并重新打开给定路由。如果已经分配了连接,则抛出java.lang.IllegalStateException。

应该在EJB容器内使用此连接管理器实现。

2.3.3. Pooling 连接管理器

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. 执行多线程请求(并发执行多个请求更为合理)

当配备池池连接管理器(如PoolingClientConnectionManager)时,HttpClient可以用于同时使用多个执行线程执行多个请求。

PoolingClientConnectionManager将根据其配置分配连接。如果给定路由的所有连接已经出租,则连接请求将被阻止,直到连接释放回池为止。通过将“http.conn-manager.timeout”设置为正值,可以确保连接管理器不会无限期地阻止连接请求操作。如果连接请求在给定的时间段内无法处理,则ConnectionPoolTimeoutException将被抛出。

虽然HttpClient实例是线程安全的,可以在多个执行线程之间共享,但强烈建议每个线程都维护自己的专用HttpContext实例。

PoolingHttpClientConnectionManager cm=new PoolingHttpClientConnectionManager();
//设置最大连接数不超过200
cm.setMaxTotal(200);
//每个路由默认的连接数20
cm.setDefaultMaxPerRoute(20);
CloseableHttpClient httpclient=HttpClients.custom()
.setConnectionManager(cm)
.build();
String[] urisToGet= {
"http://www.domain1.com/",
"http://www.domain2.com/",
"http://www.domain3.com/",
"http://www.domain4.com/"
};
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();
}
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. 连接踢出策略(eviction policy)

经典阻塞I / O模式的主要缺点之一是网络套接字只能在I / O操作中阻塞时对I / O事件做出反应。当连接释放回管理器时,它可以保持活动,但是它无法监视套接字的状态并对任何I
/ O事件做出反应。如果连接在服务器端关闭,则客户端连接无法检测到连接状态的变化(并通过关闭其端口上的套接字进行适当的响应)。

HttpClient尝试通过测试连接是否“过时”来解决问题,因为在使用连接执行HTTP请求之前,它不再有效,因为它在服务器端已关闭。stale的连接检查不是100%可靠

排除不包含每个无用连接的socket都是单线程的情况,解决“过时”问题的唯一解决方案就是设计一个专用的监听线程用来evict那些长期不活动被认为过期的
connections

该监听线程会被定时的调用,ClientConnectionManager#closeExpiredConnections()方法被用来关闭那些过期的 connections,并从pool中evict
已关闭的连接。它还可以选择调用ClientConnectionManager#closeIdleConnections()方法来关闭在给定时间段内空闲的所有连接。

import java.util.concurrent.TimeUnit;

import org.apache.http.conn.HttpClientConnectionManager;

public 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. 连接keep alive策略

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是创建和初始化普通(未加密)套接字的默认工厂。

创建套接字和将其连接到主机的过程是去耦合的,以便在连接操作中阻塞时可以关闭套接字。

HTTP连接在内部使用java.net.Socket对象来处理数据的传输。但是,它们依赖于ConnectionSocketFactory接口来创建,初始化和连接套接字。这使HttpClient的用户能够在运行时提供应用程序特定的套接字初始化代码。PlainConnectionSocketFactory是创建和初始化普通(未加密)套接字的默认工厂。

创建套接字和将其连接到主机的过程是去耦合的,以便在连接操作中阻塞时可以关闭套接字。

 HTTP连接在内部使用java.net.Socket对象来处理数据的传输。但是,它们依赖于ConnectionSocketFactory接口来创建,初始化和连接套接字。这使HttpClient的用户能够在运行时提供应用程序特定的套接字初始化代码。PlainConnectionSocketFactory是创建和初始化普通(未加密)套接字的默认工厂。
创建套接字和将其连接到主机的过程是去耦合的,以便在连接操作中阻塞时可以关闭套接字。
HttpClientContext clientContext=HttpClientContext.create();
PlainConnectionSocketFactory sf=PlainConnectionSocketFactory.getSocketFactory();
Socket socket=sf.createSocket(clientContext);
int timeout=1000;
HttpHost target=new HttpHost("localhost");
InetSocketAddress remoteSocketAddress=new InetSocketAddress(InetAddress.getByAddress(new byte[] {127,0,0,1}), 80);
sf.connectSocket(timeout, socket, target, remoteSocketAddress, null, clientContext);

2.7.1. SSL(Secure socket layering)

LayeredConnectionSocketFactory是ConnectionSocketFactory接口的扩展。 Layered socket factories
可以在普通socket上创建
layered .套接字分层主要用于通过代理创建安全套接字。HttpClient附带SSLSocketFactory,实现SSL
/ TLS分层。请注意HttpClient不使用任何自定义加密功能。 它完全依赖于标准Java加密(JCE)和安全套接字(JSE)扩展。

2.7.2. 集成到连接管理器中

自定义连接套接字工厂可以与特定的协议方案(如HTTP或HTTPS)相关联,然后用于创建自定义连接管理器。

ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
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协议的概念有一定程度的了解,该文档的详细说明超出了本文档的范围。 Please refer to theJava™ Secure Socket
Extension (JSSE) Reference Guide
 for a detailed description ofjavax.net.ssl.SSLContext and related tools.

//集成方案

public static CloseableHttpClient createSSLClientDefault(){
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
//信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
return HttpClients.custom().setSSLSocketFactory(sslsf).build();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
}
return HttpClients.createDefault();
}

2.7.4. Hostname 验证

除了信任证书和在SSL / TLS协议级别上执行的客户端认证之外,一旦建立连接,HttpClient可以可选地验证目标主机名是否与服务器的X.509证书中存储的名称匹配。此验证可以提供服务器信任资料的真实性的额外保证。javax.net.ssl.HostnameVerifier接口表示主机名验证策略。HttpClient附带两个javax.net.ssl.HostnameVerifier实现。重要提示:不要将主机名验证与SSL信任验证混淆。

· DefaultHostnameVerifier: HttpClient使用的默认期望实现将符合RFC 2818.主机名必须与证书指定的任何备用名称匹配,或者如果没有替代名称则需要提供指定的CN证书,如果CN证书中存在通配符,需要与通配符匹配。

NoopHostnameVerifier: 该主机名验证器基本上将主机名验证关闭。它接受任何SSL会话为有效并与目标主机匹配。

SSLContext sslContext = SSLContexts.createSystemDefault();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE);
· 默认情况下,HttpClient使用DefaultHostnameVerifier实现。如果需要,可以指定不同的主机名验证器实现。

从4.4版本开始,HttpClient使用由Mozilla Foundation维护的公共后缀列表,以确保SSL证书中的通配符不能被滥用以应用于具有公共顶级域的多个域。HttpClient附带了在发布时检索的列表副本。The latest revision of the list can found at
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 proxy 配置

即使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();
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: