0

我正在尝试在我的 java 代码中实现 http 连接池,当我尝试使用它时,我得到了 Handshake 异常。如果我取出设置连接管理器的那一行,它就可以工作。这对我来说毫无意义。我正在使用这些 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)

我究竟做错了什么?谢谢

4

1 回答 1

1

我为 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。

** **

可能是您在主要方法中使用它,因为它正在产生问题。

**

*此连接管理器实现应在 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 Cryptography (JCE) 和 Secure Sockets (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 使用的默认实现应符合 RFC 2818。主机名必须与证书指定的任何替代名称匹配,或者在没有替代名称的情况下,使用证书主题的最具体 CN。通配符可以出现在 CN 中,也可以出现在任何 subject-alts 中。

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

可以使用空匹配器禁用对公共 suffic 列表的验证。

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();
    }
}
于 2018-07-19T01:52:56.483 回答