Java HttpClient(二:连接与状态管理、认证与cache)

版权声明:转载 请标注 出处 https://blog.csdn.net/youyou1543724847/article/details/84661515

参考文献:http://hc.apache.org/httpcomponents-client-ga/tutorial/html/

2.连接管理

从一台主机到另一台主机建立链接的过程是非常复杂的,其中在两个端点之间有许多的包交换过程,而该过程非常耗时。建立连接时的握手产生的开销是比较明显的,特别是对于小的Http消息。如果你能够复用你建立的连接,那么这对提升整个系统的吞吐量是有很大帮助的。在HTTP/1.1 规范中,默认情况下,Http连接是可以复用的。对于使用HTTP/1.0的端点来说,可以通过keep-alive 机制来复用连接。

保持连接可用通常来说需要保证连接的持久化。HttpClient完全支持连接的持久化。

HttpClient能够直接和目标主机建立连接,同时也可以通过route(这里是指路由还是代理?)和目标主机建立连接,但这会建立许多的中间连接(也可被成为中间跳)。

2.1 Http连接路由

route类型连接的分类
HttpClient将route类型的连接分为简单的(plain)、使用隧道包装的(tunneled)、分层的(layer)连接。在tunnel connection 中使用的多个中间代理被称为代理链。

  1. plian route是通过直接和目标主机或是代理(和第一个也是唯一的一个代理)建立起来的;
  2. tunnelled route 是通过和第一个代理通信,消息通过代理链进行传输(Tunnelled routes are established by connecting to the first and tunnelling through a chain of proxies to the target.);如果不存在代理,则无法tunnelled( 隧道化包装?)
  3. Layered routes是指在已有的连接之上,在封装一层协议( Layered routes are established by layering a protocol over an existing connection. Protocols can only be layered over a tunnel to the target, or over a direct connection without proxies.)(常用的是ssl/tls协议)

2.2 Http连接管理

Http连接是有状态的(不同于协议规范中的http conection是无状态的,实际中的连接希望有状态,保存连接的信息,以提供更好的服务,如session,context等)、非线程安全的。HttpClient通过HttpClientConnectionManager 接口来进行连接管理。

HttpClient Connection Manager 的作用是:作为一个Http Connection的工厂,创建新的连接、管理持久化连接的生命周期、对持久化连接的访问做同步控制,确保同一时刻,只有一个线程能够访问持久化的连接(简而言之,一是创建连接;二是管理持久化连接,包括生命周期管理,同步访问管理)。

在http connection manager内部,mananger通过ManagedHttpClientConnection实例、代理机制来实现。当实际的连接实例对象被释放、或是关闭了,则该实例对象就会和它绑定的代理解绑,然后返回给manager。即使connection使用者 依然持有代理对象的实例,connection也无法执行任何io请求,或是改变状态了。

mananger对象使用的范例如下:

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.get() )可以被 ConnectionRequest.cancel()提前中断,中断后,阻塞在 ConnectionRequest.get() 上的线程会解除阻塞状态。

2.2.1 BasicHttpClientConnectionManager

BasicHttpClientConnectionManager 是简单的连接管理器,同一时刻,只维持一个连接。虽然这个类是线程安全的,但是它应该只能被一个执行线程调用。BasicHttpClientConnectionManager 会重新利用之前的连接(使用相同的route)。另外,它可能也会关闭之前的连接,然后重新连接。

2.2.2 PoolingHttpClientConnectionManager

PoolingHttpClientConnectionManager 可以管理多个连接(管理连接池)。当用户请求一个连接时,如果存在一个持久化的、相同的route的连接,则PoolingHttpClientConnectionManager 会返回一个之前的连接。

默认情况下,PoolingHttpClientConnectionManager 最大对于每个route维护2个连接,总共管理20个连接。可能这些限制太多了,你可以自己通过编程进行修改。

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

注意:当HttpClient不需要了后,一定要将Httpclient关闭(不仅仅将某个连接关闭),使得管理的connection manager关闭,同时关闭connection manager管理的连接。
(Httpclient 实例会绑定 connection manager,关闭httpclient时,会关闭绑定的connection manager, HttpClient connection manager默认实现是使用SimpleHttpConnectionManager。默认情况下,关闭连接不是真正的关闭连接,而是将连接还给 connectionmanager,该连接之后处于什么状态,由connectionmanager管理)。

关闭httpclient:

CloseableHttpClient httpClient = <...>
httpClient.close();

2.2.3 连接的分配管理


当使用PoolingClientConnectionManager后,则Httpclient可被多个线程使用,用于同时执行多个请求。PoolingClientConnectionManager会根据配置进行连接的分配,原则:

  1. 如果工作线程请求一个针对某个route的连接,且当前polingClientConnectionManager管理的该route点对应的连接都被租出去了,则该线程会阻塞,直到有对应的连接会还回来;
  2. 如果设置http.conn-manager.timeout为一个正数,则当没有连接可用时,阻塞的最大时间为该值,如果到了这个时间依然没有连接可用,则会抛出ConnectionPoolTimeoutException 异常。

使用实例:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

//get请求的url列表
String[] urisToGet = {
    "http://www.domain1.com/",
    "http://www.domain2.com/",
    "http://www.domain3.com/",
    "http://www.domain4.com/"
};

// 每个线程负责一个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();
}

//等待所有线程任务执行结束
for (int j = 0; j < threads.length; j++) {
    threads[j].join();
}

2.2.4 多线程访问httpclient, 每个线程维护自己的httpContext


注意:
虽然Httpclient对象是线程安全的,可以在多线程间共享,但是httpcontext不是线程安全的。建议每个线程维护自己的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 {
//在通过httpclient执行方法时,通过传入当前线程的context对象,而不是使用默认的公共的httpcontext
            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.2.5 删除过期的connection的最佳策略


当socket阻塞在io操作时,还是可以响应IO事件。但是,对于connection来说,即使connection被还回来了,状态是还是活跃的,但是它不能监控socket的状态,无法对IO事件做出反应。例如,当server端关闭了connection, client端的connection无法自身及时反馈这种变化(和及时的关闭client端关联的socket)。

在Httpclient中,处理上述问题的最好的办法是:新建一个独立线程,该线程的任务就是周期性的调用connectionmanager的closeExpiredConnections来关闭过期的connection(server端已经管理的连接)、调用closeIdleConnections来关闭长时间不用的连接。(同时你可以在使用connection之前,检查connection的stale状态,但是该方法不是100%可靠。)

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.2.6 连接keep-alive配置


Http规范没有明确规定一个持久化的connection应该存活多久。某些Http 服务器不是使用标准的keep-alive 请求头来保持连接的有效性;另外,有的server会关闭一段时间内不活跃的connection(且不会通知client) 来保证server的资源可用性。综上所述,你可能需要自定义提供怎么保持connection的可用性(keep-alive).

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.2.7 connection socket 工厂

httpconnection使用java.net.Socket对象来处理底层的数据传输。在Httpclient中,使用ConnectionSocketFactory 来负责socket的创建、初始化和socket连接。默认情况下,Httpclient使用的是PlainConnectionSocketFactory。

猜你喜欢

转载自blog.csdn.net/youyou1543724847/article/details/84661515