HttpClient 4.3.6教程 第2章 连接管理 【翻译】

第2章 连接管理

2.1 持久连接

    一个主机与另一端建立连接是十分复杂的,并且两个终端间要交换多个信息包,这会耗费不少时间。对于低级的HTTP消息来说握手连接是尤其重要的。如果在执行多个请求时重复使用公共的连接,那么就能大大提高数据吞吐率。
HTTP/1.1默认允许HTTP连接可以被多个请求复用。HTTP/1.0也兼容终端为了多个请求去使用一个明确的机制来优先保持活跃的连接。HTTP代理也能在一定的同期时间里保持活跃的空闲连接,以免同样的目标主机随后还要请求。这种保持活跃连接的能力通常都会涉及持续性连接。HttpClient完全支持“持续性连接”。

2.2 Http连接路由

    HttpClient可以建立连接给主机或路由[包含复杂的中间连接——也被称为hops(弹跳)]。HttpClient会区分不同的路由连接(平坦、路径和分层)。使用多个中间代理服务去打通目标主机连接的方式称为代理链接。
    正在连接中、第一次连接或只用代理连接都会建立“平坦路由”。第一次连接和通过代理链接都会建立“通道路由”。路由离开代理是不能产生路径的。当一个分层协议结束一个存在的连接就会建立“分层路由”。当结束一个目标路径或结束一个不再代理的连接后,协议就会建立分层。

2.2.1 路由计算

    RouteInfo接口代表一个确定的目标主机路径的信息,涉及一个或更多的中间步骤或hops(弹跳)。HttpRoute是一个具体的RouteInfo实现,它是不能被改变的(是不可变的)。HttpTracker是一个可变的RouteInfo执行情况,用于HttpClient在内部追踪剩余的指向最终路由目标的hops(弹跳)。如果下一次向着目标的hop(弹跳)执行成功,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连接每次只能被一个执行线程使用。HttpClientConnectionManager 接口是HttpClient用来管理HTTP连接的特别实体。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操作被阻塞时,网络socket只对I/O事件影响。当一个连接被释放回管理器,它会保持活跃,然而它不会监听socket的状态和任何I/O事件。如果这个连接在服务器端被关闭,客户端的连接在连接状态(和由于结束时正在关闭而作出的适当响应)下不会检测出这个改变。
    HttpClient通过测试连接是否为“陈腐的”而尝试去缓解这个问题,“陈腐的”是指不再是有效的,因为它会被服务器端关闭掉,并会在这之前为了正执行中的HTTP请求去使用连接。“陈腐的”连接检测不是百分之百有效的,并且会给每个请求执行增加10到10毫秒。为了空闲连接,唯一有效的解决办法是在每个socket模型里不包含一个线程,有一个专门的监听线程是被用来驱逐已过期的不活跃的长连接的。这个监听线程会周期性地调用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 连接socket工厂

    HTTP连接使用内部java.net.Socket对象去处理从电线传输过来的数据,然而他们依靠接口去创建、初始化和连接socket。在运行时允许HttpClient用户装备指定的socket初始化代码。PlainConnectionSocketFactory是一个默认的工作,用于创建和初始化平坦(未加密的)socket。

    创建socket的过程和将它连接去一个主机是脱钩的,所以当正在一个连接操作里阻塞的时候,应该闭关掉socket。

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 安全的socket分层

    LayeredConnectionSocketFactory 是一个ConnectionSocketFactory 接口的扩展。分层的socket工厂有能力在一个现存的平坦socket上创建分层的socket。分层的socket会首先会被代理服务使用来创建安全的socket。HttpClient包含了SSLSocketFactory,以实现SSL/TLS分层。请注意,HttpClient不会使用任何自定义的加密功能。它是完全依赖于标准的Java Cryptography (JCE) and Secure Sockets (JSEE)扩展。

2.7.2 整合连接管理

    自定义的连接socket工厂可以关联一个特别的协议体系,如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()
        .useTLS()
        .loadTrustMaterial(myTrustStore)
        .build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);

 定制SSLConnectionSocketFactory要对SSL/TLS协议有更深入的掌握,这已超出了本文档的说明范围。javax.net.ssl.SSLContext详细的说明和相关的工具使用,请参考Java Secure Socket Extension(链接:http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html)。

2.7.4 主机名检验

    除了在SSL/TLS协议级上托管检验和客户身份鉴定外,一旦连接被创建,HttpClient还能选择性地检验是否目标主机名与存放在服务器上的X.509证书相匹配。这个检验可以为服务器相信材料的可靠性提供额外的保证。X509HostnameVerifier 接口代表一个用于主机名检验的策略。HttpClient包含了三个 X509HostnameVerifier实现。注意:主机名检验不要被SSL托管检验给搞混淆了。

    StrictHostnameVerifier(精确主机名的检验器):精确的主机名检验器工作在类似于Sun Java 1.4, Sun Java 5, Sun Java 6里。它与IE6的关系也相当紧密。这个实现符合RFC 2818,因为要处理通配符。主机名必须要么匹配第一个CN,要么匹配任意的subject-alts。通配符可以出现在CN里,和任意的subject-alts里。
    BrowserCompatHostnameVerifier(浏览器兼容主机名的检验器):这个主机名检验器工作在类似于Curl和火狐浏览器里。主机名必须要么匹配第一个CN,要么匹配任意的subject-alts。通配符可以出现在CN里,和任意的subject-alts里。BrowserCompatHostnameVerifier和 StrictHostnameVerifier唯一的不同是,BrowserCompatHostnameVerifier的通配符(如"*.foo.com")会匹配所有的子域,包括"a.b.foo.com"。
    AllowAllHostnameVerifier(充许所有主机名的检验器):这个主机名检验器在本质上会关掉主机检验。这个实现是无操作的(no-op),并且永远会抛出javax.net.ssl.SSLException异常。


    默认的HttpClient使用BrowserCompatHostnameVerifier实现。如要需要,你可以指定不同的主机名检验器。

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

 

2.8 HttpClient代理服务器配置

    尽管HttpClient知道复杂的路由体系和代理服务链接,但它只支持简单的定位或一个离开的跳跃(hop)代理连接。

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

译者:lianghongge

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。