1. HttpClient 简介
HttpClient 是Apache的一个子项目,是可以提供支持HTTP协议的Java客户端编程工具包。在实际项目的使用过程中,经常都是多线程访问,因此可能存在多个线程都需要调用HttpClient对象的情况,这类似于数据库连接,所以我们需要对连接进行池化管理,以便于提高性能。
HttpClient从4.2开始抛弃了先前的SingleClientConnManager
和ThreadSafeConnManger
,取而代之的是BasicClientConnectionManager
和PoolingClientConnectionManager
,本文使用的是HttpClient 4.5 版本。
2. 编码和测试
2.1 创建PoolingHttpClientConnectionManager
PoolingHttpClientConnectionManager实现了HttpClientConnectionManager接口,顾名思义,它就是用来对连接进行池化管理的,首先创建一个对象,并在类加载时初始化它:
public class HttpClientTest { // 池化管理 private static PoolingHttpClientConnectionManager poolConnManager = null; private static CloseableHttpClient httpClient; //请求器的配置 private static RequestConfig requestConfig; static { try { System.out.println("初始化HttpClientTest~~~开始"); SSLContextBuilder builder = new SSLContextBuilder(); builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( builder.build()); // 配置同时支持 HTTP 和 HTPPS Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create().register( "http", PlainConnectionSocketFactory.getSocketFactory()).register( "https", sslsf).build(); // 初始化连接管理器 poolConnManager = new PoolingHttpClientConnectionManager( socketFactoryRegistry); // 将最大连接数增加到200,实际项目最好从配置文件中读取这个值 poolConnManager.setMaxTotal(200); // 设置最大路由 poolConnManager.setDefaultMaxPerRoute(2); // 根据默认超时限制初始化requestConfig int socketTimeout = 10000; int connectTimeout = 10000; int connectionRequestTimeout = 10000; requestConfig = RequestConfig.custom().setConnectionRequestTimeout( connectionRequestTimeout).setSocketTimeout(socketTimeout).setConnectTimeout( connectTimeout).build(); // 初始化httpClient httpClient = getConnection(); System.out.println("初始化HttpClientTest~~~结束"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (KeyStoreException e) { e.printStackTrace(); } catch (KeyManagementException e) { e.printStackTrace(); } } ...... }
上面的代码中我们只保存了一个全局的HttpClient对象,在多线程情况下,每个线程都会用这个对象去发HTTP请求,乍一看似乎有线程安全问题,但是查看了官方文档并做实验验证后,发现这是没有问题的.
2.2 创建getConnection()连接方法
getConnection()
方法是根据我们创建好的PoolingHttpClientConnectionManager对象去创建一个线程安全的HttpClient对象,具体代码如下:
public static CloseableHttpClient getConnection() { CloseableHttpClient httpClient = HttpClients.custom() // 设置连接池管理 .setConnectionManager(poolConnManager) // 设置请求配置 .setDefaultRequestConfig(requestConfig) // 设置重试次数 .setRetryHandler(new DefaultHttpRequestRetryHandler(0, false)) .build(); if (poolConnManager != null && poolConnManager.getTotalStats() != null) { System.out.println("now client pool " + poolConnManager.getTotalStats().toString()); } return httpClient; }
可以看到我们用一些HttpClients的静态方法配置好了ConnectionManager和RequestConfig,最后build了一个HttpClient的对象,这个就是一个全局带池化管理的对象,假如其中的某个线程调用了HttpClient的close()
方法,那么接下来的线程就别想再用这个对象去发起HTTP请求了。
2.3创建httpGet()连接方法
创建好了对象,我们再提供一个发起GET请求的方法,其它线程可以直接调用这个方法去发起一个GET请求,代码如下:
public static void httpGet(String url) { HttpGet httpGet = new HttpGet(url); CloseableHttpResponse response = null; try { response = httpClient.execute(httpGet); HttpEntity entity = response.getEntity(); String result = EntityUtils.toString(entity, "utf-8"); EntityUtils.consume(entity); System.out.println(result); } catch (IOException e) { e.printStackTrace(); } finally { try { if (response != null) response.close(); } catch (IOException e) { e.printStackTrace(); } } }
2.4 创建GetThread测试的线程类
再创建一个用于测试的线程类,具体代码和上面的httpGet()
方法类似,只不过增加了一些循环和打印的语句等:
static class GetThread extends Thread { private CloseableHttpClient httpClient; private String url; public GetThread(CloseableHttpClient client, String url) { httpClient = client; this.url = url; } public void run() { for(int i = 0; i < 3; i++) { HttpGet httpGet = new HttpGet(url); CloseableHttpResponse response = null; try { response = httpClient.execute(httpGet); HttpEntity entity = response.getEntity(); String result = EntityUtils.toString(entity, "utf-8"); // EntityUtils.consume(entity); System.out.println(Thread.currentThread().getName() + " Finished"); } catch (IOException e) { e.printStackTrace(); } finally { try { if (response != null) { response.close(); } if (httpGet != null) { httpGet.releaseConnection(); } } catch (IOException e) { e.printStackTrace(); } } } } }
现在,只要在main函数中添加测试代码就可以完成测试了:
public static void main(String[] args) {
// Get请求
HttpClientTest.httpGet("https://kmg343.gitbooks.io/httpcl-ient4-4-no2/content/233_lian_jie_chi_guan_li_qi.html");
// 多线程请求测试
String[] urisToGet = {
"https://kmg343.gitbooks.io/httpcl-ient4-4-no2/content/24_duo_xian_cheng_zhi_xing_qing_qiu.html",
"https://kmg343.gitbooks.io/httpcl-ient4-4-no2/content/24_duo_xian_cheng_zhi_xing_qing_qiu.html",
"https://kmg343.gitbooks.io/httpcl-ient4-4-no2/content/24_duo_xian_cheng_zhi_xing_qing_qiu.html",
"https://kmg343.gitbooks.io/httpcl-ient4-4-no2/content/24_duo_xian_cheng_zhi_xing_qing_qiu.html",
"https://kmg343.gitbooks.io/httpcl-ient4-4-no2/content/24_duo_xian_cheng_zhi_xing_qing_qiu.html"
};
GetThread[] threads = new GetThread[urisToGet.length];
for (int i = 0; i < threads.length; i++) {
threads[i] = new GetThread(httpClient, urisToGet[i]);
}
for (Thread tmp : threads) {
tmp.start();
}
}
这里先试着用了一次静态的httpGet()
方法去发送GET请求,然后启动了5个线程去发送请求,假如我们把static初始化块中的poolConnManager.setMaxTotal(200);
改为 poolConnManager.setMaxTotal(2);
我们可以在控制台看到如下的输出:
15:59:40,628 DEBUG PoolingHttpClientConnectionManager:314 - Connection [id: 0][route: {s}->https://kmg343.gitbooks.io:443] can be kept alive indefinitely
15:59:40,629 DEBUG PoolingHttpClientConnectionManager:320 - Connection released: [id: 0][route: {s}->https://kmg343.gitbooks.io:443][total kept alive: 1; route allocated: 1 of 2; total allocated: 1 of 2]
......
15:59:47,401 DEBUG PoolingHttpClientConnectionManager:314 - Connection [id: 0][route: {s}->https://kmg343.gitbooks.io:443] can be kept alive indefinitely
15:59:47,401 DEBUG PoolingHttpClientConnectionManager:320 - Connection released: [id: 0][route: {s}->https://kmg343.gitbooks.io:443][total kept alive: 1; route allocated: 2 of 2; total allocated: 2 of 2]
Thread-4 Finished
而假如仍然是分配200个连接的话,total allocated:就会显示为2 of 200,可以看到连接池生效了,多线程并发调用没有问题。假如我们在测试线程的for循环中添加了
httpClient.close();
语句,则可以看到控制台在发起了一些连接后抛出异常,提示连接池已经关闭。请记得每个请求返回处理后,调用EntityUtils.toString或者EntityUtils.consume()关闭流.
由于我们在实际应用中,访问的链接可能都是事先在代码中定义好的,所以我们没必要关闭连接,只需要把流关闭或消费完,让连接keep-alive,这样下一个线程重用这个连接时就可以省去TCP的三次握手过程了。
3. 一些总结
3.1 每个路由(route)最大连接数
上面我们有一行这样的代码:
poolConnManager.setDefaultMaxPerRoute(2);
用于设置最大路由,这里route的概念可以理解为 运行环境机器 到 目标机器 的一条线路。举例来说,我们使用HttpClient的实现来分别请求 www.baidu.com 的资源和 www.bing.com 的资源那么他就会产生两个route。以下是网上对设置这个参数的一些解释:
这里为什么要特别提到route最大连接数这个参数呢,因为这个参数的默认值为2,如果不设置这个参数,值默认情况下对于同一个目标机器的最大并发连接只有2个!这意味着如果你正在执行一个针对某一台目标机器的抓取任务的时候,哪怕你设置连接池的最大连接数为200,但是实际上还是只有2个连接在工作,其他剩余的198个连接都在等待,都是为别的目标机器服务的。
3.2 BasicClientConnectionManager
BasicClientConnectionManager内部只维护一个活动的connection,尽管这个类是线程安全的,但是最好在一个单独的线程中重复使用它。如果在同一个BasicClientConnectionManager对象中,多次执行http请求,后继请求与先前请求是同一个route,那么BasicClientConnectionManager会使用同一个连接完成后续请求,否则,BasicClientConnectionManager会将先前的connection关闭,然后为后续请求创建一个新的连接。换句话说,BasicClientConnectionManager会尽力复用先前的连接(注意:创建连接和销毁连接都是不小的开销)