HttpClientUtil实践

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());
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,302评论 19 139
  • 1、Activity生命周期? onCreate() -> onStart() -> onResume() -> ...
    tgcity阅读 4,440评论 0 4
  • 1、java中==和equals和hashCode的区别 基本数据类型的==比较的值相等.类的==比较的内存的地址...
    Mr_Fly阅读 4,340评论 0 0
  • 1、java中==和equals和hashCode的区别 基本数据类型的==比较的值相等.类的==比较的内存的地址...
    快感的感知阅读 4,809评论 0 4
  • Android中高级面试题 1、Activity生命周期? onCreate() -> onStart() -> ...
    司文喰阅读 3,526评论 0 0