1、httpclient
核心模块有
ManagedHttpClientConnection
PoolingHttpClientConnectionManager
链接池是由StrictConnPool维护, PoolingHttpClientConnectionManager不直接维护链接池
2、httpclient实现类
名称 | 说明 | 备注 |
---|---|---|
InternalHttpClient | 默认客户端 | |
MinimalHttpClient | 最新客户端,没有认证,代理的功能。(request execution via a proxy, state management, authentication and request redirects.) | |
CloseableHttpClient | 客户端的基础封装,提供公共流程 |
3、HttpClientConnectionManager实现类
- BasicHttpClientConnectionManager
- PoolingHttpClientConnectionManager
3.1、BasicHttpClientConnectionManager
单个连接的连接管理器,只支持一个链接。多线程使用时也只能一个线程执行,其他线程会等待
3.1、PoolingHttpClientConnectionManager
维护连接请求池,并能够为多个执行线程连接请求提供服务。根据路由进行区分,每个路由(域名+端口)默认最大链接数为2,总共链接数默认为20;
- 默认最大链接数20
- 默认最大路由链接数2
- 默认空闲链接存活时间 10s
- 默认空闲链接存活时间单位 s
- 默认链接存活时间 -1 (永久存活)
- 默认链接存活时间单位 s
备注:http中的链接不主动关闭,链接池中的链接由IdleConnectionEvictor进行维护管理;在未过期会一直保持链接状态;
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
// 在build时设置很多初始化配置,build方法是入口
1. httpclient
1.1、httpclient实现类
名称 | 说明 | 备注 |
---|---|---|
InternalHttpClient | 默认客户端 | 包含全部功能。 非公共类,在项目中无法直接通过new 创建客户端 |
MinimalHttpClient | 最新客户端,没有认证,代理的功能。(request execution via a proxy, state management, authentication and request redirects.) | 非公共类,在项目中无法直接通过new 创建客户端。 |
CloseableHttpClient | 客户端的基础封装,提供公共流程 | 抽象类,直接new 需要重写相关方法 |
1.2、httpclient创建方式说明
通过上述说明创建client只有两种方式
- 通过HttpClientBuilder build();
- 直接new 自定义实现CloseableHttpClient 抽象类方法;
httpClients提供快速创建的方式
- createDefault() 创建默认的连接器,默认为InternalHttpClient客户端,包含多个功能。常用方式
- createSystem() 通过系统参数创建连接器
- createMinimal() 创建默认最小客户端
- createMinimal(HttpClientConnectionManager) 创建默认带有链接池的客户端
创建demo 1
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// 整个连接池最大连接数
cm.setMaxTotal(100);
// 每路由最大连接数,默认值是2
cm.setDefaultMaxPerRoute(5);
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).disableAutomaticRetries().build();
创建demo 2
CloseableHttpClient httpClient1 = HttpClients.createDefault();
1.3、创建流程说明
创建client 必备的要素
- ClientExecChain execChain
- HttpClientConnectionManager connManager
- HttpRoutePlanner routePlanner
- Lookup<CookieSpecProvider> cookieSpecRegistry
- Lookup<AuthSchemeProvider> authSchemeRegistry
- CookieStore cookieStore
- CredentialsProvider credentialsProvider
- RequestConfig defaultConfig
- List<Closeable> closeables
最佳实践
1. 单线程测试场景
public class HttpClientPerformanceTest {
// 测试参数配置
private static final String TARGET_URL = "https://www.baidu.com";
private static final int TOTAL_REQUESTS = 1; // 总请求次数
private static final int WARMUP_ROUNDS = 10; // 预热轮次
public static void main(String[] args) throws Exception {
// 预热(避免首次请求的初始化影响)
// warmup();
// 测试共用Client模式
System.out.println("=== 测试共用HttpClient实例 ===");
testSharedClient();
// 测试每次新建Client模式
System.out.println("\n=== 测试每次新建HttpClient ===");
testNewClientEachTime();
}
private static void warmup() throws Exception {
CloseableHttpClient client = HttpClients.createDefault();
for (int i = 0; i < WARMUP_ROUNDS; i++) {
executeRequest(client);
}
}
private static void testSharedClient() throws Exception {
long totalTime = 0;
long startTime = System.currentTimeMillis();
CloseableHttpClient client = HttpClients.createDefault();
for (int i = 0; i < TOTAL_REQUESTS; i++) {
long requestStart = System.currentTimeMillis();
executeRequest(client);
long duration = (System.currentTimeMillis() - requestStart); // 毫秒
totalTime += duration;
if ((i + 1) % 20 == 0) {
System.out.printf("已完成 %d 次请求,平均耗时 %.2f ms%n",
i + 1, (double) totalTime / (i + 1));
}
}
long overallDuration = System.currentTimeMillis() - startTime;
printResult("共用实例", TOTAL_REQUESTS, overallDuration, totalTime);
}
private static void testNewClientEachTime() throws Exception {
long totalTime = 0;
long startTime = System.currentTimeMillis();
for (int i = 0; i < TOTAL_REQUESTS; i++) {
long requestStart = System.currentTimeMillis();
try (CloseableHttpClient client = HttpClients.createDefault()) {
executeRequest(client);
}
long duration = (System.currentTimeMillis() - requestStart); // 毫秒
totalTime += duration;
if ((i + 1) % 10 == 0) {
System.out.printf("已完成 %d 次请求,平均耗时 %.2f ms%n",
i + 1, (double) totalTime / (i + 1));
}
}
long overallDuration = System.currentTimeMillis() - startTime;
printResult("新建实例", TOTAL_REQUESTS, overallDuration, totalTime);
}
private static void executeRequest(CloseableHttpClient client) throws Exception {
HttpGet request = new HttpGet(TARGET_URL);
CloseableHttpResponse response = client.execute(request);
// 确保消费实体内容,才能释放链接资产,链接不会断开
// EntityUtils.consume(response.getEntity());
String result = parseHttpEntity(response.getEntity());
// 关闭连接,即断开tcp链接,释放linux层面的链接资源,这样的话,每次请求都需要重新建立链接,效率很低
// response.close();
}
private static void printResult(String mode, int count, long totalDuration, long sumOfIndividual) {
System.out.println("\n测试结果 [" + mode + "]:");
System.out.println("总请求次数: " + count);
System.out.println("总耗时: " + totalDuration + " ms");
System.out.printf("平均每次请求耗时: %.2f ms%n", (double) totalDuration / count);
System.out.printf("平均网络耗时: %.2f ms%n", (double) sumOfIndividual / count);
System.out.printf("吞吐量: %.2f 请求/秒%n",
count / (totalDuration / 1000.0));
}
public static String parseHttpEntity(HttpEntity httpEntity, String charset) throws IOException {
charset = charset == null ? "utf-8" : charset;
String entityStr = null;
if (httpEntity == null) {
return entityStr;
} else {
entityStr = EntityUtils.toString(httpEntity, charset);
return entityStr;
}
}
public static String parseHttpEntity(HttpEntity httpEntity) throws IOException {
return parseHttpEntity(httpEntity, "utf-8");
}
}
类别 | 请求1次 | 请求10次 | 请求100次 | 请求1000次 |
---|---|---|---|---|
共享实例网络平均耗时 | 325 | 60 | 24 | 16 |
每次创建实例网络平均耗时 | 80 | 86 | 90 | 78.89 |
通过多次对比看,共享实例在单线程时耗时在请求量大时,共享实例的效率更高;
2、多线程测试
package com.tianzehaoSpring;
import com.alibaba.fastjson.JSON;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
/**
* Hello world!
*
*/
public class HttpClientUtilThreadTest
{
public static void main( String[] args ) throws InterruptedException {
for(int i = 0; i < 1; i++){
httpClientTest();
}
}
private static void httpClientTest() throws InterruptedException {
int maxThreadTotal = 80;
// List<String> urls = Arrays.asList("https://www.baidu.com","https://cn.bing.com/");
List<String> urls = Arrays.asList("http://10.1.106.177");
// CloseableHttpClient httpClient = getHttpClient(maxThreadTotal,urls.size());
Map<String,List<Long>> wasteTimeMapWithHttpClient = new HashMap<>();
Map<String,List<Long>> wasteTimeMapNoHttpClient = new HashMap<>();
long startTime = System.currentTimeMillis();
CountDownLatch withShareClientCountDown = new CountDownLatch(maxThreadTotal);
CountDownLatch withNoShareClientCountDown = new CountDownLatch(maxThreadTotal);
for (String url : urls) {
List<Long> times = getBlankList(maxThreadTotal);
wasteTimeMapWithHttpClient.put(url, times);
CloseableHttpClient httpClient = getHttpClient(maxThreadTotal,urls.size());
for(int i = 0; i < maxThreadTotal; i++){
int index = i;
Thread thread = new Thread(()->{
long start = System.currentTimeMillis();
HttpGet httpGet = new HttpGet(url);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RuntimeException(e);
}
long wasteTime = System.currentTimeMillis() - start;
times.set(index, wasteTime);
withShareClientCountDown.countDown();
});
thread.setName("withShareClientCountDown" + i);
thread.start();
}
}
withShareClientCountDown.await();
System.out.println("With HttpClient: " + (System.currentTimeMillis() - startTime) + "ms. " + "avg:" + ((System.currentTimeMillis() - startTime)/maxThreadTotal) + "ms." );
for (String url : urls) {
List<Long> times = wasteTimeMapWithHttpClient.get(url);
long totalTime = times.stream().filter(wasteTime-> wasteTime!=0).mapToLong(Long::longValue).sum();
long avgTime = totalTime/times.size();
System.out.println(JSON.toJSONString(times));
System.out.println("With HttpClient: " + url + " avgTime: " + avgTime);
}
for (String url : urls) {
List<Long> times = getBlankList(maxThreadTotal);
wasteTimeMapNoHttpClient.put(url, times);
for(int i = 0; i < maxThreadTotal; i++){
int index = i;
new Thread(()->{
long start = System.currentTimeMillis();
HttpGet httpGet = new HttpGet(url);
CloseableHttpClient httpClient = HttpClients.createDefault();
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RuntimeException(e);
}
long wasteTime = System.currentTimeMillis() - start;
times.set(index, wasteTime);
withNoShareClientCountDown.countDown();
}).start();
}
}
withNoShareClientCountDown.await();
System.out.println("no HttpClient: " + (System.currentTimeMillis() - startTime) + "ms. " + "avg:" + ((System.currentTimeMillis() - startTime)/maxThreadTotal) + "ms." );
for (String url : urls) {
List<Long> times = wasteTimeMapNoHttpClient.get(url);
long totalTime = times.stream().filter(wasteTime-> wasteTime!=0).mapToLong(Long::longValue).sum();
long avgTime = totalTime/times.size();
System.out.println(JSON.toJSONString(times));
System.out.println("no HttpClient: " + url + " avgTime: " + avgTime);
}
System.out.println(" ");
}
private static List<Long> getBlankList(int size){
List<Long> list = new ArrayList<>();
for(int i = 0; i < size; i++){
list.add(0L);
}
return list;
}
public static CloseableHttpClient getHttpClient(Integer maxThread,Integer maxRoute){
int MAX_TOTAL_CONNECTIONS = maxThread * maxRoute * 2;
int DEFAULT_MAX_PER_ROUTE = maxThread * 2;
int CONNECT_TIMEOUT = 5000; // 建立与目标主机连接的最大时间 默认 -1不限制
int SOCKET_TIMEOUT = 10000; // 等待数据传输的最大时间 默认 -1不限制
int CONNECTION_REQUEST_TIMEOUT = 2000; // 从连接池中获取连接的超时时间 默认 -1不限制
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE);
// 配置全局请求参数
RequestConfig requestConfig = RequestConfig.custom()
// .setConnectTimeout(CONNECT_TIMEOUT)
// .setSocketTimeout(SOCKET_TIMEOUT)
// .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT)
.build();
// 创建 HttpClient 实例
return HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
.build();
}
}
开启多线程请求
类别 | 请求1次 | 请求10次 | 请求100次 | 请求500次 |
---|---|---|---|---|
共享实例请求耗时 | 393 | 197 | 207 | 611 |
每次创建实例请求耗时 | 297 | 123 | 408 | 1257 |
结论: 不管是多线程,还是单线程,共享实例的效率更高,因为共享实例可以复用tcp链接,而每次创建实例则会重新建立tcp链接,效率较低。
常见问题
1、内存泄漏检查
tcp 链接管理器会自动关闭链接,httpclient对象在失去tcp链接引用后可被自动回收;
2、共享实例阻塞人问题
response 不消费,会导致链接无法释放;无法再发起请求;
3、每次创建实例请求耗时高
共享实例测试时 关闭了链接,导致共享实例与每次创建实例测试结果相同,因为共享实例测试时,关闭了链接
// 执行该语句,会关闭链接,即断开tcp链接,释放linux层面的链接资源,这样的话,每次请求都需要重新建立链接,效率很低。
response.close();
附录: httpClientUtil
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class HttpClientUtil {
private static final String DEFAULT_CHARSET = "UTF-8";
private static final int CONNECT_TIMEOUT = 5000; // 连接超时时间(毫秒)
private static final int SOCKET_TIMEOUT = 5000; // 读取数据超时时间(毫秒)
/**
* 发送GET请求
* @param url 请求地址
* @param params 请求参数
* @return 响应内容
*/
public static String doGet(String url, Map<String, String> params) {
return doGet(null, url, params, null, DEFAULT_CHARSET);
}
/**
* 发送GET请求
* @param httpClient 可选的HttpClient实例
* @param url 请求地址
* @param params 请求参数
* @param headers 自定义请求头
* @param charset 字符编码
* @return 响应内容
*/
public static String doGet(CloseableHttpClient httpClient, String url,
Map<String, String> params, Map<String, String> headers,
String charset) {
// 构建带参数的URL
if (params != null && !params.isEmpty()) {
url = buildUrlWithParams(url, params);
}
HttpGet httpGet = new HttpGet(url);
// 设置自定义Header
if (headers != null && !headers.isEmpty()) {
setHeaders(httpGet, headers);
}
boolean isShareHttpClient = httpClient != null;
// 使用传入的HttpClient或创建新的
if (httpClient == null) {
httpClient = HttpClients.createDefault();
}
try {
CloseableHttpResponse response = httpClient.execute(httpGet)
HttpEntity entity = response.getEntity();
if (entity != null) {
return EntityUtils.toString(entity, charset);
}
return "";
} catch (IOException e) {
throw new RuntimeException("HTTP GET请求失败: " + e.getMessage(), e);
}finally {
if(!isShareHttpClient){
//非共享HttpClient,则关闭
try {
httpClient.close();
} catch (IOException e) {
// 忽略关闭异常
}
}
}
}
/**
* 发送POST请求(表单形式)
* @param url 请求地址
* @param params 请求参数
* @return 响应内容
*/
public static String doPost(String url, Map<String, String> params) {
return doPost(null, url, params, null, DEFAULT_CHARSET);
}
/**
* 发送POST请求(表单形式)
* @param httpClient 可选的HttpClient实例
* @param url 请求地址
* @param params 请求参数
* @param headers 自定义请求头
* @param charset 字符编码
* @return 响应内容
*/
public static String doPost(CloseableHttpClient httpClient, String url,
Map<String, String> params, Map<String, String> headers,
String charset) {
HttpPost httpPost = new HttpPost(url);
// 设置自定义Header
if (headers != null && !headers.isEmpty()) {
setHeaders(httpPost, headers);
}
// 设置表单参数
if (params != null && !params.isEmpty()) {
try {
List<NameValuePair> paramList = new ArrayList<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
paramList.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
httpPost.setEntity(new UrlEncodedFormEntity(paramList, charset));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("不支持的编码格式: " + charset, e);
}
}
boolean isShareHttpClient = httpClient != null;
// 使用传入的HttpClient或创建新的
if (httpClient == null) {
httpClient = HttpClients.createDefault();
}
try {
CloseableHttpResponse response = httpClient.execute(httpPost)
HttpEntity entity = response.getEntity();
if (entity != null) {
return EntityUtils.toString(entity, charset);
}
return "";
} catch (IOException e) {
throw new RuntimeException("HTTP POST请求失败: " + e.getMessage(), e);
}
finally {
if(!isShareHttpClient){
//非共享HttpClient,则关闭
try {
httpClient.close();
} catch (IOException e) {
// 忽略关闭异常
}
}
}
}
/**
* 发送POST请求(JSON形式)
* @param url 请求地址
* @param json JSON字符串
* @return 响应内容
*/
public static String doPostJson(String url, String json) {
return doPostJson(null, url, json, null, DEFAULT_CHARSET);
}
/**
* 发送POST请求(JSON形式)
* @param httpClient 可选的HttpClient实例
* @param url 请求地址
* @param json JSON字符串
* @param headers 自定义请求头
* @param charset 字符编码
* @return 响应内容
*/
public static String doPostJson(CloseableHttpClient httpClient, String url,
String json, Map<String, String> headers,
String charset) {
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/json");
// 设置自定义Header
if (headers != null && !headers.isEmpty()) {
setHeaders(httpPost, headers);
}
// 设置JSON请求体
if (json != null) {
httpPost.setEntity(new StringEntity(json, charset));
}
boolean isShareHttpClient = httpClient != null;
// 使用传入的HttpClient或创建新的
if (httpClient == null) {
httpClient = HttpClients.createDefault();
}
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
HttpEntity entity = response.getEntity();
if (entity != null) {
return EntityUtils.toString(entity, charset);
}
return "";
} catch (IOException e) {
throw new RuntimeException("HTTP POST JSON请求失败: " + e.getMessage(), e);
}
finally {
if(!isShareHttpClient){
//非共享HttpClient,则关闭
try {
httpClient.close();
} catch (IOException e) {
// 忽略关闭异常
}
}
}
}
/**
* 构建带参数的URL
*/
private static String buildUrlWithParams(String url, Map<String, String> params) {
StringBuilder paramStr = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> entry : params.entrySet()) {
if (first) {
first = false;
paramStr.append("?");
} else {
paramStr.append("&");
}
paramStr.append(entry.getKey()).append("=").append(entry.getValue());
}
return url + paramStr.toString();
}
/**
* 设置请求头
*/
private static void setHeaders(HttpRequestBase request, Map<String, String> headers) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
request.setHeader(entry.getKey(), entry.getValue());
}
}
}