深入学习 HttpClient 5:源码分析、封装工具类和注意点

随着现代应用程序对网络通信需求的增加,以及对更灵活、高性能和可定制的 HTTP 客户端的需求,HttpClient 5 成为了 Java 开发者们的首选工具。它提供了强大的功能和灵活的配置选项,使得发送 HTTP 请求和处理响应变得更加简单和高效。

在本篇技术博客中,我们将深入学习 HttpClient 5,并从以下几个方面入手:源码分析、封装工具类和最佳实践与注意点。通过这些深入的学习,你将能够更好地理解 HttpClient 5 的内部工作原理,灵活地封装和使用 HttpClient 5,以及遵循最佳实践来提升性能和稳定性。

1. 源码分析

httpclient官网:
https://hc.apache.org/httpcomponents-client-5.1.x/
源码版本5.1.3

HttpClient是顶级接口,有三个实现类CloseableHttpClientInternalHttpClientMinimalHttpClient
HttpClient:
image.png
CloseableHttpClient:
HttpClient的基本实现,也实现了ModalCloseable接口(立即/优雅的关闭资源)
InternalHttpClient:
CloseableHttpClient的内部实现
MinimalHttpClient:
CloseableHttpClient的最小实现。不支持高级HTTP协议功能,如通过执行代理请求、状态管理、身份验证和请求重定向。

HttpClients

HttpClientsCloseableHttpClient实现的工厂方法。
image.png
一般调用custom()方法创建HttpClientBuilder用builder模式创建CloseableHttpClient
HttpClientBuilder类中有各种set属性的方法,我们就从这作为切入点来看。
主要的set方法有

  • setConnectionManager
  • setKeepAliveStrategy
  • setRetryStrategy
  • setDefaultRequestConfig

setConnectionManager

public final HttpClientBuilder setConnectionManager(final HttpClientConnectionManager connManager) {
    this.connManager = connManager;
    return this;
}

setConnectionManager()方法入参是一个HttpClientConnectionManager。我们就来看一下这个接口相关的代码。有两个实现类BasicHttpClientConnectionManagerPoolingHttpClientConnectionManager
BasicHttpClientConnectionManager类是管理单个连接的连接管理器。这个管理器只维护一个活动连接。因此用于创建和管理一次只能由一个线程使用的单个连接。大部分场景还是使用PoolingHttpClientConnectionManager
PoolingHttpClientConnectionManager:是一个池化的连接管理器,能够服务来自多个执行线程的请求。连接基于每个路由进行池化。
PoolingHttpClientConnectionManager主要的方法有:

setKeepAliveStrategy

设置keepAlive策略一般使用默认的默认的new DefaultConnectionKeepAliveStrategy() 这个默认的keepAlive是3s。根据实际情况覆写getKeepAliveDuration()方法。

setRetryStrategy

设置重试策略默认是new DefaultHttpRequestRetryStrategy(),重试1次间隔1s。

setDefaultRequestConfig

设置请求信息。常用的RequestConfig

RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(Timeout.ofMilliseconds(MAX_TIMEOUT))                   // 建立连接超时时间
        .setResponseTimeout(Timeout.ofSeconds(MAX_TIMEOUT))                    // 传输超时
        .setConnectionRequestTimeout(Timeout.ofMilliseconds(MAX_TIMEOUT))         // 设置从连接池获取连接实例的超时
        .build();

execute执行流程

execute执行流程主要是责任链模式。在创建httpclient的时候初始化责任链。具体在HttpClients._custom_().build()时使用头插法创建双向链表,形成责任链。
主要代码

public CloseableHttpClient build() {
// ...
    final NamedElementChain<ExecChainHandler> execChainDefinition = new NamedElementChain<>();
        execChainDefinition.addLast(
                new MainClientExec(connManagerCopy, reuseStrategyCopy, keepAliveStrategyCopy, userTokenHandlerCopy),
                ChainElement.MAIN_TRANSPORT.name());
        execChainDefinition.addFirst(
                new ConnectExec(
                        reuseStrategyCopy,
                        new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)),
                        proxyAuthStrategyCopy),
                ChainElement.CONNECT.name());
// ...
        execChainDefinition.addFirst(
                new ProtocolExec(httpProcessor, targetAuthStrategyCopy, proxyAuthStrategyCopy),
                ChainElement.PROTOCOL.name());

        // Add request retry executor, if not disabled
        if (!automaticRetriesDisabled) {
            HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
            if (retryStrategyCopy == null) {
                retryStrategyCopy = DefaultHttpRequestRetryStrategy.INSTANCE;
            }
            execChainDefinition.addFirst(
                    new HttpRequestRetryExec(retryStrategyCopy),
                    ChainElement.RETRY.name());
        }
// ...
    if (!contentCompressionDisabled) {
            if (contentDecoderMap != null) {
                final List<String> encodings = new ArrayList<>(contentDecoderMap.keySet());
                final RegistryBuilder<InputStreamFactory> b2 = RegistryBuilder.create();
                for (final Map.Entry<String, InputStreamFactory> entry: contentDecoderMap.entrySet()) {
                    b2.register(entry.getKey(), entry.getValue());
                }
                final Registry<InputStreamFactory> decoderRegistry = b2.build();
                execChainDefinition.addFirst(
                        new ContentCompressionExec(encodings, decoderRegistry, true),
                        ChainElement.COMPRESS.name());
            } else {
                execChainDefinition.addFirst(new ContentCompressionExec(true), ChainElement.COMPRESS.name());
            }
        }

        // Add redirect executor, if not disabled
        if (!redirectHandlingDisabled) {
            RedirectStrategy redirectStrategyCopy = this.redirectStrategy;
            if (redirectStrategyCopy == null) {
                redirectStrategyCopy = DefaultRedirectStrategy.INSTANCE;
            }
            execChainDefinition.addFirst(
                    new RedirectExec(routePlannerCopy, redirectStrategyCopy),
                    ChainElement.REDIRECT.name());
        }

        // Optionally, add connection back-off executor
        if (this.backoffManager != null && this.connectionBackoffStrategy != null) {
            execChainDefinition.addFirst(new BackoffStrategyExec(this.connectionBackoffStrategy, this.backoffManager),
                    ChainElement.BACK_OFF.name());
        }
}

调用execChainDefinition.addLast()execChainDefinition.addFirst()方法初始化一个BackoffStrategyExec->RedirectExec->ContentCompressionExec->HttpRequestRetryExec->ProtocolExec->ConnectExec->MainClientExec。然后按顺序执行他们的execute()方法,完成自己的任务。

  • BackoffStrategyExec:Request execution handler in the classic request execution chain that is responsible for execution of an ConnectionBackoffStrateg(经典请求执行链中的请求执行处理程序,负责ConnectionBackoffStrateg的执行)。对出现连接或者响应超时异常的route进行降级,缩小该route上连接数,能使得服务质量更好的route能得到更多的连接。降级的速度可以通过因子设置,默认是每次降级减少一半的连接数,即降级因子是0.5。
  • RedirectExec:Request execution handler in the classic request execution chain responsible for handling of request redirects(经典请求执行链中的请求执行处理程序,负责处理请求重定向)。
  • ContentCompressionExec:Request execution handler in the classic request execution chain that is responsible for automatic response content decompression(经典请求执行链中的请求执行处理程序,负责自动响应内容解压缩)
  • HttpRequestRetryExec:Request executor in the request execution chain that is responsible for making a decision whether a request that failed due to an I/O exception or received a specific response from the target server should be re-executed.(请求执行链中的请求执行器,负责决定由于I/O异常而失败的请求或从目标服务器接收到特定响应的请求是否应该重新执行)
  • ProtocolExec:Request execution handler in the classic request execution chain that is responsible for implementation of HTTP specification requirements(经典请求执行链中的请求执行处理程序,负责HTTP规范需求的实现)对http消息编码/解码。这个操作在处理请求和相应之前。我们可以往HttpRequestInterceptor和HttpResponseInterceptor中添加我们自己定义的拦截器。
  • ConnectExec:Request execution handler in the classic request execution chain that is responsible for establishing connection to the target origin server as specified by the current connection route(经典请求执行链中的请求执行处理程序,该处理程序负责建立到由当前连接路由指定的目标源服务器的连接)。
  • MainClientExec:Usually the last request execution handler in the classic request execution chain that is responsible for execution of request / response exchanges with the opposite endpoint.(通常是经典请求执行链中的最后一个请求执行处理程序,负责执行与对面端点的请求/响应交换)连接的池化处理在这。

2. 代码片段

import com.google.common.base.Throwables;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpResponseException;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HeaderElements;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicHeaderElementIterator;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.ssl.TrustStrategy;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;

/**
 * @Description TODO
 * @Author 张弛
 * @Datee 2022/8/22
 * @Version 1.0
 **/
public class HttpUtil {
    private static final int MAX_TIMEOUT = 400;
    private static final CloseableHttpClient httpClient;

    static {
        // 自定义 SSL 策略
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", createSSLConnSocketFactory())
                .build();
        // 设置连接池
        PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(registry);
        connMgr.setMaxTotal(100); // 设置连接池大小
        connMgr.setDefaultMaxPerRoute(connMgr.getMaxTotal()); // 设置每条路由的最大并发连接数
        connMgr.setValidateAfterInactivity(TimeValue.ofSeconds(600)); // 设置长连接

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(Timeout.ofMilliseconds(MAX_TIMEOUT))                   // 建立连接超时时间
                .setResponseTimeout(Timeout.ofSeconds(MAX_TIMEOUT))                    // 传输超时
                .setConnectionRequestTimeout(Timeout.ofMilliseconds(MAX_TIMEOUT))         // 设置从连接池获取连接实例的超时
                .build();

        httpClient = HttpClients.custom()
                .setConnectionManager(connMgr)
                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
                .setRetryStrategy(new DefaultHttpRequestRetryStrategy())  // 重试 1 次,间隔1s
                .setDefaultRequestConfig(requestConfig)
                .build();
    }

    public static byte[] doGet(String url) throws IOException, ParseException {
        HttpGet httpGet = new HttpGet(url);
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
//            int statusCode = response.getStatusLine().getStatusCode();
            int statusCode = response.getCode();
            if (statusCode != HttpStatus.SC_OK) {
                String message = EntityUtils.toString(response.getEntity());
                throw new HttpResponseException(statusCode, message);
            }
            byte[] bytes = EntityUtils.toByteArray(response.getEntity());
            return bytes;
        } catch (Exception e) {
            throw e;
        } finally {
            if (response != null) {
                try {
                    EntityUtils.consume(response.getEntity());
                } catch (IOException e) {
                }
            }
        }
    }

    private static SSLConnectionSocketFactory createSSLConnSocketFactory() {
        SSLConnectionSocketFactory sslsf = null;
        try {
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    return true;
                }
            }).build();
            sslsf = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() {
                @Override
                public boolean verify(String s, SSLSession sslSession) {
                    return true;    // 信任所有证书
                }
            });
        } catch (GeneralSecurityException e) {
            throw Throwables.propagate(e);
        }
        return sslsf;
    }
}

3. 注意点

  1. 设置合理的超时时间:连接超时、读取超时、从连接池中获取连接的超时时间。
  2. 设置合理的连接池大小:连接池大小和读取耗时、QPS 有关,一般等于峰值 QPS * 耗时(单位是秒)。
  3. 设置合理的长连接有效时间:使用连接池就默认使用长连接,长连接有效时间应该和服务端的长连接有效时间保持一致。如果客户端设置的有效时间过长,则会在服务端连接断开的时客户端依然去请求时导致NoHttpResponseException。也可以通过设RequestConfig.setStaleConnectionCheckEnabled 参数让客户端每次请求之前都检查长连接有效性,但是这样会导致性能的下降。
  4. 设置合理的重试策略:合理的重试,可以提升应用的可用性。默认的重试策略不会对超时进行重试,然而超时是十分从常见的问题,服务端异常或网络抖动都可能导致超时。

长连接

使用长连接能够减少建立和销毁连接的消耗,三次握手和四次挥手对性能影响还是很大的。一般RPC都是使用长连接。

一般Http服务前面都会挂nginx做负载均衡,那么长连接的设置也就分为从客户端到nginx,nginx到服务端两部分。

apache httpclient 5.x直接使用DefaultConnectionKeepAliveStrategy这个类就行,不行根据这个类重写策略。大部分的博客写的都是3.下或者4.x版本的。5.x版本的看这个类就行。

nginx请求后端服务时启用长连接配置:

proxy_http_version 1.1;                    
proxy_set_header Connection "";
keepalive_timeout  120s 120s;
keepalive_requests 10000;

keepalive_timeout:不活跃多长时间后断开。

keepalive_requests:keepalive的requests的最大数量。当请求达到最大数量时,连接被关闭。nginx和tomcat的默认数查一下,好像是100。对于 QPS 较高的应用来说 100 有些太小了。

springboot的tomcat配置在这个里面找。

image.png

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HttpClient是一个开源的HTTP客户端库,它是 Apache 的一个子项目,并且是目前广泛使用的请求工具类之一。 使用HttpClient,我们可以方便地发送HTTP请求,并且获取到服务器返回的数据。它支持各种HTTP方法,如GET、POST、PUT等,也可以设置请求头、请求体、cookie等信息。 使用HttpClient发送请求的步骤如下: 1. 创建HttpClient对象:可以通过HttpClientBuilder来创建一个HttpClient对象。 2. 创建Http请求对象:可以根据具体需求创建HttpGet或者HttpPost等对象。 3. 设置请求参数:可以设置请求头、请求体、cookie等。 4. 执行请求:通过HttpClient的execute方法来发送请求并获取到响应对象HttpResponse。 5. 处理响应:根据具体需求,可以获取到响应的状态码、响应头、响应体等信息。 HttpClient不仅可以简化HTTP请求的发送,还提供了一些高级功能,如连接池管理、SSL/TLS 安全协议支持、身份验证等。这些功能使得HttpClient在实际项目中非常受欢迎。 然而,需要注意的是,HttpClient在最新的版本中已经停止了维护,并且官方不再推荐使用。取而代之的是HttpURLConnection,它是Java自带的API,可以完成同样的功能。另外,还有其他第三方的HTTP请求库,如OkHttp,也是一个非常好用的选择。 总而言之,HttpClient是一个非常方便的HTTP请求工具类,可以简化请求的发送和响应的处理。但在使用时,最好选择更加新的替代方案,以确保项目的稳定性和安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值