HttpClient 4.5教程学习与翻译——第2章:连接管理

原文链接

第2章:连接管理

2.1. 连接持久性

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

HTTP / 1.1声明HTTP连接可以在默认情况下重复用于多个请求。 符合HTTP / 1.0标准的端点还可以使用一种机制来显式传达其首选项,以保持连接活动并将其用于多个请求。 如果后续请求需要连接到同一目标主机,HTTP代理还可以使空闲连接保持一段时间。 保持连接活动的能力通常被称为连接持久性。 HttpClient完全支持连接持久性。

2.2. Http 连接路由

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

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

2.2.1. 路线计算

RouteInfo接口表示有关到目标主机的确定路由的信息,涉及一个或多个中间步骤或跳。 HttpRoute是RouteInfo的具体实现,无法更改(不可变)。 HttpTracker是一个可变的RouteInfo实现,由HttpClient内部使用,用于跟踪剩余的跳转到最终路由目标。在成功执行到路由目标的下一跳之后,可以更新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连接的工厂,管理持久连接的生命周期以及同步对持久连接的访问​​,确保一次只有一个线程可以访问连接。如果被管理的连接被释放或由其使用者显式关闭,则底层连接将从其代理中分离并返回给管理器。即使服务使用者仍然拥有对代理实例的引用,它也不再能够执行任何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附带SSLSocketFactory,可实现SSL / TLS分层。 请注意,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)参考指南

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匹配器禁用对公共suffic列表的验证。

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

上一章: 基础
下一章: 状态管理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容