困扰了半个月时间终于找到连接池的问题,由于调用第三方有异常导致连接不能及时释放 所以写了一个定时扫描释放连接
监控连接池释放连接:
public static class IdleConnectionEvictor extends Thread {
private final HttpClientConnectionManager connectionManager;
private volatile boolean shutdown;
public IdleConnectionEvictor(HttpClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
//3s检查一次
wait(2000);
// 关闭失效的连接
connectionManager.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
//连接超过30秒空闲释放连接
connectionManager.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// 结束
ex.printStackTrace();
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
参照官方文档:
https://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html
第二章。连接管理
建立连接的过程从一个宿主传播到另一个相当复杂,涉及多个包两个端点之间的交流,这可能非常耗时。连接握手的开销会很大,特别是对于小HTTP消息。可以达到一个更高的数据吞吐量如果打开的连接可以执行多个请求重用。
HTTP / 1.1,HTTP连接可以重复用于多个请求/违约。HTTP / 1.0兼容端点也可以使用一种机制来明确他们的偏好进行沟通,保持连接活着,使用多个请求。HTTP代理还可以保持一段时间空闲连接在连接到同一个目标主机需要后续的请求。保持联系的能力通常是指连接持久性。HttpClient完全支持连接的持久性。
HttpClient能够建立连接到目标主机直接或通过一个路线,可能涉及多个中间连接——也称为啤酒花。HttpClient区分线路连接成平原,隧道和分层。使用多个中介代理隧道连接到目标主机被称为代理链接。
普通路线建立了连接到目标或第一个也��唯一一个代理。隧道线路建立连接到第一个和隧道通过一连串的代理到目标。没有代理不能挖过的路线。分层路由建立了分层协议在一个现有的连接。协议只能在隧道到目标分层,通过直接连接或没有代理。
的 RouteInfo
接口代表信息明确的路由到目标主机包括一个或多个中间步骤或跳。 HttpRoute
具体实现的吗 RouteInfo
不能改变(不可变的)。 HttpTracker
是一个可变的 RouteInfo
实现内部使用HttpClient跟踪剩下的啤酒花的终极目标。 HttpTracker
可以更新成功后执行下一跳向目标的路线。 HttpRouteDirector
是一个辅助类,它可以用来计算路线的下一步。这门课由HttpClient内部使用。
HttpRoutePlanner
是一个接口代表一��策略来计算一个完整的路线到一个给定的目标基于执行上下文。HttpClient附带两个默认 HttpRoutePlanner
实现。 SystemDefaultRoutePlanner
是基于 java.net.ProxySelector
。默认情况下,它将JVM的代理设置,从系统属性或从浏览器中运行应用程序。的 DefaultProxyRoutePlanner
实现不使用任何Java系统属性,也没有任何系统或浏览器代理设置。它总是通过相同的默认代理计算路线。
HTTP连接是复杂的,有状态的线程不安全的对象,需要适当地管理功能正确。所使用的HTTP连接只能一次一个执行线程。HttpClient雇佣了一个特殊的实体来管理访问HTTP连接称为HTTP连接管理器和所代表的 HttpClientConnectionManager
接口。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()
方法。
BasicHttpClientConnectionManager
是一个简单的连接管理器,它维护一次只有一个连接。尽管这类是线程安全的它应该只使用一个执行线程。 BasicHttpClientConnectionManager
将努力为后续请求重用连接相同的路线。然而,它将关闭现有连接并重新打开它为给定的路线,如果持久连接的线路不匹配的连接请求。如果连接已经被分配, java.lang.IllegalStateException
抛出。
应该使用这个连接管理器实现EJB容器内。
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();
当配备一个池连接管理器等 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 } } }
的一个主要缺点的经典阻塞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(); } } }
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();
利用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);
LayeredConnectionSocketFactory
是一个扩展的 ConnectionSocketFactory
接口。分层的套接字工厂可以创建套接字层在现有普通插座。套接字层主要用于创建通过代理安全套接字。HttpClient附带 SSLSocketFactory
实现SSL / TLS分层。请注意HttpClient不使用任何自定义加密功能。它完全依赖于标准的Java加密(JCE)和安全套接字(JSEE)扩展。
自定义连接套接字工厂可以关联到一个特定的协议方案作为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();
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协议的概念,详细解释这是超出了本文的范围。请参考Java™安全套接字扩展(JSSE)参考指南的详细描述 javax.net.ssl.SSLContext
和相关的工具。
除了信任验证和客户端身份验证SSL / TLS协议级别上执行,HttpClient可以验证目标主机是否匹配名称存储在服务器的X。509证书,一旦连接已经建立。这个验证可以提供额外的服务器的信任的真实性保证材料。的 javax.net.ssl.HostnameVerifier
接口代表主机名验证的策略。HttpClient附带两个 javax.net.ssl.HostnameVerifier
实现。重要:主机名验证SSL信任验证不应混淆。
-
DefaultHostnameVerifier
: 默认实现使用HttpClient有望符合RFC 2818。主机名必须匹配任何的替代名称指定的证书,或者如果没有替代的名字有证书的最具体的CN主题。一个通配符可以发生在CN,任何subject-alts。 -
NoopHostnameVerifier
: 这个主机名验证器本质上是主机名验证。它接受任何SSL会话有效,匹配的目标主机。
每违约HttpClient使用 DefaultHostnameVerifier
实现。你可以指定一个不同的主机名如果需要验证器实现
SSLContext sslContext = SSLContexts.createSystemDefault(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslContext, NoopHostnameVerifier.INSTANCE);
HttpClient使用4.4版本的公共后缀列表请由Mozilla基金会以确保通配符在SSL证书不能滥用申请多个域的通用顶级域名。HttpClient附带一份列表中检索时的释放。最新修订版本的列表可以发现https://publicsuffix.org/list/。是高度adviseable列表和下载列表的一个本地副本每天不超过一次从其原始位置。
PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.load( PublicSuffixMatcher.class.getResource("my-copy-effective_tld_names.dat")); DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);
可以禁用验证对公众suffic通��使用列表 null
匹配器。
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(null);
尽管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(); } }