HttpClient 中文官方教程----第二章连接管理-只收录,未测试
博客专区 > 诺岚 的博客 > 博客详情
HttpClient 中文官方教程----第二章连接管理-只收录,未测试
诺岚 发表于3个月前
HttpClient 中文官方教程----第二章连接管理-只收录,未测试
  • 发表于 3个月前
  • 阅读 5
  • 收藏 0
  • 点赞 0
  • 评论 0

 


第二章连接管理

2.1。连接持久性

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

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

2.2。HTTP连接路由

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

平原路线是通过连接到目标或第一个也是唯一的代理建立的。隧道路线是通过连接到第一个隧道并通过代理链隧道进行目标建立的。没有代理的路由不能被隧道掘进。通过在现有连接上分层协议来建立分层路由。协议只能通过隧道分层到目标,或通过无代理的直接连接分层。

2.2.1。路线计算

RouteInfo接口表示关于涉及一个或多个中间步骤或跳的目标主机的确定路由的信息。HttpRoute是一个具体的实现,RouteInfo不能改变(是不可变的)。HttpTrackerRouteInfoHttpClient内部使用的可变实现,用于跟踪终端路由目标的剩余跳数。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连接的访问​​,并由该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。多线程请求执行

当配备了一个池化连接管理器,如 PoolingClientConnectionManagerHttpClient可以同时使用多个执行线程执行多个请求。

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。安全套接字层

LayeredConnectionSocketFactoryConnectionSocketFactory接口的扩展。分层插座工厂能够在现有的普通套接字上创建套接字。套接字分层主要用于通过代理创建安全套接字。HttpClient附带SSLSocketFactory实现SSL / TLS分层。请注意HttpClient不使用任何自定义加密功能。它完全依赖于标准Java加密(JCE)和安全套接字(JSE)扩展。

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协议的概念有一定程度的了解,其详细说明超出了本文档的范围。有关详细说明和相关工具,请参阅Java™安全套接字扩展(JSSE)参考指南javax.net.ssl.SSLContext

2.7.4。主机名验证

除了信任验证和在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 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);

可以使用null匹配器禁用对公共消息列表的验证。

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();
    }
}
     
     
共有 人打赏支持
粉丝 0
博文 60
码字总数 94479
×
诺岚
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: