在实现PoolingHttpClientConnectionManager时遇到问题

时间:2018-07-19 00:39:40

标签: http ssl connection-pooling ssl-client-authentication

我正在尝试在Java代码中实现http连接池,当我尝试使用它时,出现了握手异常。如果我取出设置连接管理器的那一行,它将起作用。这对我来说毫无意义。我正在使用以下jar文件: httpclient-4.5.2.jar httpcore-4.4.4.jar

已建立连接池:

RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(10000)
        .setConnectionRequestTimeout(10000)
        .setSocketTimeout(5000)
        .build();

SSLContext sslContext = SSLContexts.custom()
        .loadKeyMaterial(readStore(), KEYSTOREPASS) 
        .build();

HttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager();

httpClient = HttpClients.custom()
        .setConnectionManager(poolingConnManager)
        .setDefaultRequestConfig(requestConfig)
        .setSSLContext(sslContext)
        .build();

引发收到致命警报:handshake_failure 例外:

main, RECV TLSv1.2 ALERT:  fatal, handshake_failure
%% Invalidated:  [Session-1, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
%% Invalidated:  [Session-2, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
main, called closeSocket()
main, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
main, called close()
main, called closeInternal(true)
00:22:51,523 ERROR TestHttps:155 - Received fatal alert: handshake_failure

已注释掉连接池:

RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(10000)
        .setConnectionRequestTimeout(10000)
        .setSocketTimeout(5000)
        .build();

SSLContext sslContext = SSLContexts.custom()
        .loadKeyMaterial(readStore(), KEYSTOREPASS) // use null as second param if you don't have a separate key password
        .build();

HttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager();

httpClient = HttpClients.custom()
        //.setConnectionManager(poolingConnManager)
        .setDefaultRequestConfig(requestConfig)
        .setSSLContext(sslContext)
        .build();

它成功运行并返回我的值(在此处混淆):

main, READ: TLSv1.2 Application Data, length = 40
Padded plaintext after DECRYPTION:  len = 16
0000: xx xx xx xx xx xx xx xx   xx xx xx xx xx xx xx xx  1234567890
main, called close()
main, called closeInternal(true)
main, SEND TLSv1.2 ALERT:  warning, description = close_notify
Padded plaintext before ENCRYPTION:  len = 2
0000: 01 00                                              ..
main, WRITE: TLSv1.2 Alert, length = 26
[Raw write]: length = 31
0000: 15 03 03 00 1A 00 00 00   00 00 00 00 01 2C 7D 6E  .............,.n
0010: 66 04 BA 1D FF 4A EB 54   0F 60 C7 A4 41 4A 68     f....J.T.`..AJh
main, called closeSocket(true)

我在做什么错?谢谢

1 个答案:

答案 0 :(得分:0)

我发现此文档是httpconnection Manager的标准实现的

**

  

*此连接管理器实现应在EJB容器内使用。可能是因为这个原因,并且您在主要方法**

中使用了此方法

HTTP连接管理器 2.3.1。托管连接和连接管理器 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()方法中阻塞的线程。

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

** **

  

可能是因为您正在main方法中使用它   造成问题。

**

  

*此连接管理器实现应在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。多线程请求执行 如果配备了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操作中阻塞时,网络套接字才能对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。连接保持活力的策略 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。连接插座工厂 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。安全插座分层 LayeredConnectionSocketFactory是ConnectionSocketFactory接口的扩展。分层套接字工厂能够创建在现有普通套接字上分层的套接字。套接字分层主要用于通过代理创建安全套接字。 HttpClient与实现SSL / TLS分层的SSLSocketFactory一起提供。请注意,HttpClient不使用任何自定义加密功能。它完全依赖于标准Java加密(JCE)和安全套接字(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协议的概念有一定程度的了解,对此的详细说明不在本文档的讨论范围之内。有关javax.net.ssl.SSLContext和相关工具的详细说明,请参考Java™安全套接字扩展(JSSE)参考指南。

主机名验证 建立连接后,除了在SSL / TLS协议级别执行的信任验证和客户端身份验证外,HttpClient还可选择验证目标主机名是否与服务器X.509证书中存储的名称匹配。该验证可以为服务器信任材料的真实性提供其他保证。 javax.net.ssl.HostnameVerifier接口表示用于主机名验证的策略。 HttpClient附带了两个javax.net.ssl.HostnameVerifier实现。重要提示:请勿将主机名验证与SSL信任验证相混淆。

DefaultHostnameVerifier:HttpClient使用的默认实现应符合RFC2818。主机名必须与证书指定的任何备用名称匹配,或者在没有给备用名称指定证书主题的最特定CN的情况下。通配符可以出现在CN以及任何主题替换中。

NoopH​​ostnameVerifier:该主机名验证程序实质上关闭了主机名验证。它接受任何有效且匹配目标主机的SSL会话。

每个默认HttpClient使用DefaultHostnameVerifier实现。如果需要,可以指定其他主机名验证程序实现

SSLContext sslContext = SSLContexts.createSystemDefault();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslContext,
        NoopHostnameVerifier.INSTANCE);

从4.4版开始,HttpClient使用Mozilla Foundation友好维护的公共后缀列表,以确保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);

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();
    }
}